diff --git a/Cargo.lock b/Cargo.lock index 4165424..e5abff6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,41 +13,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "getrandom" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - [[package]] name = "hermit-abi" version = "0.1.19" @@ -57,12 +22,6 @@ dependencies = [ "libc", ] -[[package]] -name = "indoc" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" - [[package]] name = "itoa" version = "1.0.10" @@ -76,59 +35,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] -name = "lock_api" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +name = "lyagushka" +version = "0.1.0" dependencies = [ - "autocfg", - "scopeguard", + "atty", + "serde", + "serde_json", ] -[[package]] -name = "memoffset" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" -dependencies = [ - "autocfg", -] - -[[package]] -name = "once_cell" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" - -[[package]] -name = "parking_lot" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets", -] - -[[package]] -name = "ppv-lite86" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" - [[package]] name = "proc-macro2" version = "1.0.78" @@ -138,67 +52,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "pyo3" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a89dc7a5850d0e983be1ec2a463a171d20990487c3cfcd68b5363f1ee3d6fe0" -dependencies = [ - "cfg-if", - "indoc", - "libc", - "memoffset", - "parking_lot", - "pyo3-build-config", - "pyo3-ffi", - "pyo3-macros", - "unindent", -] - -[[package]] -name = "pyo3-build-config" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07426f0d8fe5a601f26293f300afd1a7b1ed5e78b2a705870c5f30893c5163be" -dependencies = [ - "once_cell", - "target-lexicon", -] - -[[package]] -name = "pyo3-ffi" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbb7dec17e17766b46bca4f1a4215a85006b4c2ecde122076c562dd058da6cf1" -dependencies = [ - "libc", - "pyo3-build-config", -] - -[[package]] -name = "pyo3-macros" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f738b4e40d50b5711957f142878cfa0f28e054aa0ebdfc3fd137a843f74ed3" -dependencies = [ - "proc-macro2", - "pyo3-macros-backend", - "quote", - "syn", -] - -[[package]] -name = "pyo3-macros-backend" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc910d4851847827daf9d6cdd4a823fbdaab5b8818325c5e97a86da79e8881f" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "quote" version = "1.0.35" @@ -208,57 +61,12 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags", -] - [[package]] name = "ryu" version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - [[package]] name = "serde" version = "1.0.196" @@ -290,12 +98,6 @@ dependencies = [ "serde", ] -[[package]] -name = "smallvec" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" - [[package]] name = "syn" version = "2.0.48" @@ -307,41 +109,12 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "target-lexicon" -version = "0.12.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69758bda2e78f098e4ccb393021a0963bb3442eac05f135c30f61b7370bbafae" - -[[package]] -name = "traktorpy" -version = "0.1.0" -dependencies = [ - "atty", - "pyo3", - "rand", - "serde", - "serde_json", -] - [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" -[[package]] -name = "unindent" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - [[package]] name = "winapi" version = "0.3.9" @@ -363,60 +136,3 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" diff --git a/Cargo.toml b/Cargo.toml index 0a937b1..e97ec34 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,13 +3,7 @@ name = "lyagushka" version = "0.1.0" edition = "2021" -[lib] -name = "lyagushka" -crate-type = ["cdylib"] - [dependencies] atty = "0.2.14" -pyo3 = "0.20.2" -rand = "0.8.5" serde = { version = "1.0.196", features = ["derive"] } serde_json = "1.0.113" diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..31e63bc --- /dev/null +++ b/readme.md @@ -0,0 +1,61 @@ +# lyagushka + +(Russian лягушка: frog) + +Cluster and Gap Analysis Tool inspired by Fatum Project's 'Zhaba' algorithm (Russian 'жаба': toad) that finds attractor clusters in lists of integers. + +This Rust command-line tool analyzes a dataset of integers to identify clusters of closely grouped points and significant gaps between these clusters. It calculates z-scores for each cluster or gap to measure their statistical significance relative to the dataset's mean distance. The analysis results, including clusters, gaps, and their z-scores, are output as a JSON string. + +## Features + +- **Cluster Identification**: Identifies groups of points that are closely spaced together based on a customizable threshold. +- **Gap Detection**: Detects significant gaps between clusters, providing insights into the dataset's distribution. +- **Z-Score Calculation**: Calculates z-scores for both clusters and gaps, offering a statistical measure of their deviation from the mean distance. +- **Flexible Input**: Accepts input data either from a file specified as a command-line argument or piped directly into stdin. +- **JSON Output**: Outputs the analysis results in a readable JSON format, making it easy to interpret or use in further processing. + +## Usage + +### From a File + +To analyze a dataset from a file, provide the filename as an argument along with two additional parameters: the factor for adjusting clustering and gap detection thresholds, and the minimum cluster size. + +```sh +cargo run -- filename.txt 0.5 2 +``` + +### From Stdin + +Alternatively, you can pipe a list of integers into the tool, followed by the factor and minimum cluster size. + +```sh +echo "1\n2\n10\n20" | cargo run -- 0.5 2 +``` + +#### Parameters + +* `filename.txt` (optional): A file containing a newline-separated list of integers to analyze. If not provided, the program expects input from stdin. +* `factor`: A floating-point value used to fine-tune the sensitivity of cluster and gap detection. Lower values result in tighter clusters and wider gaps, while higher values do the opposite. +* `min_cluster_size`: An integer specifying the minimum number of contiguous points required to be considered a cluster. + +### Output + +The tool outputs a JSON string that includes details about the identified clusters and gaps, along with their respective z-scores. Here's an example of the JSON output format: + +```json + +[ + { + "span_length": 1.0, + "num_elements": 2, + "centroid": 1.5, + "z_score": -1.23 + }, + { + "span_length": 8.0, + "num_elements": 0, + "centroid": 6.0, + "z_score": 2.45 + } +] +``` \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 57ac035..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,216 +0,0 @@ -use pyo3::prelude::*; -use pyo3::wrap_pyfunction; -use pyo3::types::PyList; -use std::fs::File; -// use std::io::{self, BufRead, BufReader, stdin, Read}; -use std::io::{self, BufRead, BufReader}; -use std::env; -use std::process; -use serde::Serialize; -use serde_json; - -#[derive(Clone, Debug, Serialize)] -struct Point { - value: u32, -} - -impl Point { - fn new(value: u32) -> Self { - Point { value } - } -} - -#[derive(Debug, Clone, Serialize)] -struct ClusterGapInfo { - span_length: f32, // Full span length - num_elements: usize, // Number of elements, 0 for gaps - centroid: f32, // Centroid value - z_score: Option, // Z-score, to be calculated later -} - - -fn load_dataset(filename: &str) -> io::Result> { - let file = File::open(filename)?; - let reader = BufReader::new(file); - let mut dataset = Vec::new(); - - for line in reader.lines() { - let value: u32 = line?.trim().parse().unwrap(); - dataset.push(Point::new(value)); - } - - dataset.sort_by_key(|p| p.value); - Ok(dataset) -} - -// Define the ClusterGapInfo struct as described above - -fn calculate_densities_and_gaps(dataset: &[Point], factor: f32, min_cluster_size: usize) -> Vec { - let mut results: Vec = Vec::new(); - if dataset.len() < 2 { - return results; - } - - let mean_distance = dataset.windows(2) - .map(|w| distance(&w[0], &w[1]) as f32) - .sum::() / (dataset.len() - 1) as f32; - - let cluster_threshold = 1.0 / factor * mean_distance; - let gap_threshold = factor * mean_distance * 2.0; - - let mut current_cluster = Vec::new(); - for window in dataset.windows(2) { - let gap_distance = distance(&window[0], &window[1]) as f32; - if gap_distance <= cluster_threshold { - current_cluster.push(window[1].clone()); - } else { - // Before clearing the current_cluster, check if it meets the size requirement - if !current_cluster.is_empty() && current_cluster.len() >= min_cluster_size { - let cluster_info = create_cluster_info(¤t_cluster); - results.push(cluster_info); - } - current_cluster.clear(); - - // Add a gap if the distance exceeds the gap threshold - if gap_distance > gap_threshold { - results.push(ClusterGapInfo { - span_length: gap_distance, - num_elements: 0, - centroid: (window[0].value as f32 + window[1].value as f32) / 2.0, - z_score: None, - }); - } - } - } - - // Handle the last cluster if it meets the size requirement - if !current_cluster.is_empty() && current_cluster.len() >= min_cluster_size { - let cluster_info = create_cluster_info(¤t_cluster); - results.push(cluster_info); - } - - results -} - -// Additional helper function to create cluster information -fn create_cluster_info(cluster: &[Point]) -> ClusterGapInfo { - let num_elements = cluster.len(); - let span_length = (cluster.last().unwrap().value as f32) - (cluster.first().unwrap().value as f32); - let centroid = cluster.iter().map(|p| p.value as f32).sum::() / num_elements as f32; - - ClusterGapInfo { - span_length, - num_elements, - centroid, - z_score: None, // Placeholder, to be calculated later - } -} - -fn distance(p1: &Point, p2: &Point) -> u32 { - if p1.value > p2.value { p1.value - p2.value } else { p2.value - p1.value } -} - - -fn main() -> io::Result<()> { - let args: Vec = env::args().collect(); - let mut dataset: Vec = Vec::new(); - - // Check if data is being piped into the program - if atty::is(atty::Stream::Stdin) { - // Not receiving piped input, expect filename as argument - if args.len() < 4 { - eprintln!("Usage: {} ", args[0]); - eprintln!("Or pipe in a list of integers and provide "); - process::exit(1); - } - - let filename = &args[1]; - dataset = load_dataset(filename)?; - } else { - // Receiving piped input, read from stdin - let stdin = io::stdin(); - let reader = stdin.lock(); - for line in reader.lines() { - let value: u32 = line?.trim().parse().unwrap(); - dataset.push(Point::new(value)); - } - dataset.sort_by_key(|p| p.value); - } - - let factor: f32 = args[args.len() - 2].parse().expect("Factor must be a float"); - let min_cluster_size: usize = args[args.len() - 1].parse().expect("Min cluster size must be an integer"); - - let mut cluster_gap_infos = calculate_densities_and_gaps(&dataset, factor, min_cluster_size); - - // Calculate mean distance for Z-score computation - let total_distances: f32 = dataset.windows(2) - .map(|w| (w[1].value as f32 - w[0].value as f32)) - .sum(); - let mean_distance = total_distances / (dataset.len() as f32 - 1.0); - - // Calculate Z-scores for clusters and gaps - for info in cluster_gap_infos.iter_mut() { - if info.num_elements == 0 { - // Z-score for gaps - info.z_score = Some((info.span_length - mean_distance) / mean_distance); // Simplified deviation measure - } else { - // Z-score for clusters, based on density deviation - let density = info.num_elements as f32 / info.span_length; - let expected_density = 1.0 / mean_distance; // Expected: one element per mean distance - info.z_score = Some((density - expected_density) / expected_density); // Simplified deviation measure - } - } - - // Convert cluster_gap_infos to JSON - let json = serde_json::to_string_pretty(&cluster_gap_infos).expect("Failed to serialize to JSON"); - - // Output the JSON string - println!("{}", json); - - Ok(()) -} - -#[pyfunction] -fn traktor(py: Python, int_list: &PyList, factor: f32, min_cluster_size: usize) -> PyResult { - // Convert Python list to Rust Vec - let mut dataset: Vec = Vec::new(); - for py_any in int_list.into_iter() { - let value: u32 = py_any.extract()?; - dataset.push(Point::new(value)); - } - dataset.sort_by_key(|p| p.value); - - // Proceed with your existing logic - let mut cluster_gap_infos = calculate_densities_and_gaps(&dataset, factor, min_cluster_size); - - // Calculate mean distance for Z-score computation - let total_distances: f32 = dataset.windows(2) - .map(|w| (w[1].value as f32 - w[0].value as f32)) - .sum(); - let mean_distance = total_distances / (dataset.len() as f32 - 1.0); - - // Calculate Z-scores for clusters and gaps - for info in cluster_gap_infos.iter_mut() { - if info.num_elements == 0 { - // Z-score for gaps - info.z_score = Some((info.span_length - mean_distance) / mean_distance); // Simplified deviation measure - } else { - // Z-score for clusters, based on density deviation - let density = info.num_elements as f32 / info.span_length; - let expected_density = 1.0 / mean_distance; // Expected: one element per mean distance - info.z_score = Some((density - expected_density) / expected_density); // Simplified deviation measure - } - } - - // Serialize to JSON and return - let json = serde_json::to_string_pretty(&cluster_gap_infos) - .expect("Failed to serialize to JSON"); - - Ok(json) -} - -#[pymodule] -fn lyagushka(py: Python, m: &PyModule) -> PyResult<()> { - m.add_function(wrap_pyfunction!(traktor, m)?)?; - Ok(()) -} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..fced572 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,231 @@ +use std::fs::File; +use std::io::{self, BufRead, BufReader, stdin}; +use std::env; +use std::process; +use serde::Serialize; +use serde_json; + +#[derive(Clone, Debug, Serialize)] +struct Point { + value: u32, +} + +impl Point { + fn new(value: u32) -> Self { + Point { value } + } +} + +#[derive(Debug, Clone, Serialize)] +struct ClusterGapInfo { + span_length: f32, + num_elements: usize, + centroid: f32, + z_score: Option, +} + +fn create_cluster_info(cluster: &[Point]) -> ClusterGapInfo { + + let num_elements = cluster.len(); + let span_length = (cluster.last().unwrap().value as f32) - (cluster.first().unwrap().value as f32); + let centroid = cluster.iter().map(|p| p.value as f32).sum::() / num_elements as f32; + + ClusterGapInfo { + span_length, + num_elements, + centroid, + z_score: None, + } +} + +/// Calculates the densities (clusters) and significant gaps between points in a dataset. +/// +/// This function iterates over a dataset of points, identifying clusters based on a distance threshold +/// (calculated from the mean distance between points and adjusted by a given factor) and identifying significant gaps +/// that exceed a certain threshold. Each cluster or significant gap identified is summarized in a `ClusterGapInfo` object. +/// +/// # Arguments +/// * `dataset`: A slice of `Point` objects representing the dataset to be analyzed. +/// * `factor`: A multiplier used to define the thresholds for clustering and gap identification. +/// A lower factor tightens the cluster threshold and widens the gap threshold, and vice versa. +/// * `min_cluster_size`: The minimum number of points required for a group of points to be considered a cluster. +/// +/// # Returns +/// A vector of `ClusterGapInfo` objects, each representing either a cluster of points or a significant gap between points. +/// +fn calculate_densities_and_gaps(dataset: &[Point], factor: f32, min_cluster_size: usize) -> Vec { + + // Return early if the dataset is too small to form any clusters or gaps. + if dataset.len() < 2 { return Vec::new(); } + + // Calculate the mean distance between consecutive points in the dataset. + let mean_distance = dataset.windows(2) + .map(|w| w[1].value as f32 - w[0].value as f32) + .sum::() / (dataset.len() - 1) as f32; + + // Define thresholds for clustering and gap identification based on the mean distance and factor. + let cluster_threshold = mean_distance / factor; + let gap_threshold = factor * mean_distance * 2.0; + + let mut results: Vec = Vec::new(); // Stores the resulting clusters and gaps. + let mut current_cluster: Vec = Vec::new(); // Temporary storage for points in the current cluster. + + // Iterate through pairs of consecutive points to find clusters and significant gaps. + for window in dataset.windows(2) { + let gap_distance = window[1].value as f32 - window[0].value as f32; + + // If the distance between points is within the cluster threshold, add to current cluster. + if gap_distance <= cluster_threshold { + if current_cluster.is_empty() { + current_cluster.push(window[0].clone()); // Start a new cluster with the first point. + } + current_cluster.push(window[1].clone()); // Add the second point to the cluster. + } else { + // If the current cluster is large enough, finalize it and prepare for a new cluster. + if !current_cluster.is_empty() && current_cluster.len() >= min_cluster_size { + results.push(create_cluster_info(¤t_cluster)); + current_cluster.clear(); + } + + // If the gap between points is significant, record it as a gap. + if gap_distance > gap_threshold { + results.push(ClusterGapInfo { + span_length: gap_distance, + num_elements: 0, // Indicating this is a gap, not a cluster. + centroid: (window[0].value as f32 + window[1].value as f32) / 2.0, + z_score: None, // Z-score will be calculated later if necessary. + }); + } + } + } + + // Finalize the last cluster if it meets the size requirement. + if !current_cluster.is_empty() && current_cluster.len() >= min_cluster_size { + results.push(create_cluster_info(¤t_cluster)); + } + + results +} + +/// Analyzes a dataset of points to identify clusters and significant gaps, calculates z-scores for each, +/// and serializes the results to a JSON string. +/// +/// This function takes a vector of `Point` structs, a factor for adjusting clustering and gap detection thresholds, +/// and a minimum cluster size. It performs an analysis to identify clusters of points that are closely grouped +/// together and significant gaps between these clusters. For each cluster or gap, it calculates a z-score that +/// indicates how far the centroid or span length deviates from the mean distance of the dataset. The results +/// of this analysis are then serialized into a JSON string. +/// +/// # Arguments +/// * `dataset` - A vector of `Point` structs representing the dataset to be analyzed. +/// * `factor` - A floating-point value used to adjust the sensitivity of cluster and gap detection. Lower values +/// result in tighter clustering and wider gaps, while higher values do the opposite. +/// * `min_cluster_size` - The minimum number of contiguous points required to be considered a cluster. +/// +/// # Returns +/// Returns a `String` containing the JSON-serialized analysis results, including clusters and gaps with their z-scores. +/// +fn lyagushka(dataset: Vec, factor: f32, min_cluster_size: usize) -> String { + + // Analyze the dataset to identify clusters and significant gaps. + let mut cluster_gap_infos = calculate_densities_and_gaps(&dataset, factor, min_cluster_size); + + // Calculate the mean distance between consecutive points in the dataset. + let mean_distance: f32 = if dataset.len() > 1 { + dataset.windows(2) + .map(|w| w[1].value as f32 - w[0].value as f32) + .sum::() / (dataset.len() - 1) as f32 + } else { + 0.0 + }; + + // Calculate the standard deviation of distances between consecutive points. + let std_deviation: f32 = if dataset.len() > 1 { + (dataset.windows(2) + .map(|w| w[1].value as f32 - w[0].value as f32 - mean_distance) + .map(|d| d.powi(2)) + .sum::() / (dataset.len() - 1) as f32) + .sqrt() + } else { + 0.0 + }; + + // Calculate and assign z-scores for each cluster/gap based on their centroid or span length. + for info in cluster_gap_infos.iter_mut() { + info.z_score = Some(if info.num_elements > 0 { + // For clusters, use the centroid for z-score calculation. + (info.centroid - mean_distance) / std_deviation + } else { + // For gaps, use the span length for z-score calculation. + (info.span_length - mean_distance) / std_deviation + }); + } + + serde_json::to_string_pretty(&cluster_gap_infos).unwrap_or_else(|_| "Failed to serialize data".to_string()) +} + +/// The entry point for the command-line tool that reads a dataset of integers from either a file or stdin, +/// performs cluster and gap analysis using specified parameters, and prints the results as a JSON string. +/// +/// This tool expects either a filename as an argument or a list of integers piped into stdin. It also requires +/// two additional command-line arguments: a factor for adjusting clustering and gap detection thresholds, +/// and a minimum cluster size. The tool reads the dataset, performs the analysis by identifying clusters +/// and significant gaps, calculates z-scores for each, and prints the JSON-serialized results to stdout. +/// +/// # Usage +/// To read from a file: +/// ``` +/// cargo run -- filename.txt 0.5 2 +/// ``` +/// +/// To read from stdin: +/// ``` +/// echo "1\n2\n10\n20" | cargo run -- 0.5 2 +/// ``` +/// +/// # Arguments +/// - A filename (if not receiving piped input) to read the dataset from. +/// - `factor`: A floating-point value used to adjust the sensitivity of cluster and gap detection. +/// - `min_cluster_size`: The minimum number of contiguous points required to be considered a cluster. +/// +/// # Exit Codes +/// - `0`: Success. +/// - `1`: Incorrect usage or failure to parse the input data. +/// +/// # Errors +/// This tool will exit with an error if the required arguments are not provided, if the specified file cannot be opened, +/// or if the input data cannot be parsed into integers. +/// +/// # Note +/// This function does not return a value but directly exits the process in case of failure. +/// +fn main() -> io::Result<()> { + let args: Vec = env::args().collect(); + + // Input handling + let dataset: Vec = if atty::is(atty::Stream::Stdin) { + if args.len() != 4 { + eprintln!("Usage: {} ", args[0]); + process::exit(1); + } + let filename = &args[1]; + let file = File::open(filename)?; + BufReader::new(file).lines().filter_map(Result::ok) + .filter_map(|line| line.trim().parse::().ok()) + .map(Point::new) + .collect() + } else { + stdin().lock().lines().filter_map(Result::ok) + .filter_map(|line| line.trim().parse::().ok()) + .map(Point::new) + .collect() + }; + + let factor: f32 = args[args.len() - 2].parse().expect("Factor must be a float"); + let min_cluster_size: usize = args[args.len() - 1].parse().expect("Min cluster size must be an integer"); + + // Analysis and output + println!("{}", lyagushka(dataset, factor, min_cluster_size)); + + Ok(()) +} \ No newline at end of file