final
This commit is contained in:
parent
44bb9dcd13
commit
158d0c1bfe
5 changed files with 297 additions and 511 deletions
294
Cargo.lock
generated
294
Cargo.lock
generated
|
|
@ -13,41 +13,6 @@ dependencies = [
|
||||||
"winapi",
|
"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]]
|
[[package]]
|
||||||
name = "hermit-abi"
|
name = "hermit-abi"
|
||||||
version = "0.1.19"
|
version = "0.1.19"
|
||||||
|
|
@ -57,12 +22,6 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "indoc"
|
|
||||||
version = "2.0.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.10"
|
version = "1.0.10"
|
||||||
|
|
@ -76,59 +35,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
|
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lyagushka"
|
||||||
version = "0.4.11"
|
version = "0.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"atty",
|
||||||
"scopeguard",
|
"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]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.78"
|
version = "1.0.78"
|
||||||
|
|
@ -138,67 +52,6 @@ dependencies = [
|
||||||
"unicode-ident",
|
"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]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.35"
|
version = "1.0.35"
|
||||||
|
|
@ -208,57 +61,12 @@ dependencies = [
|
||||||
"proc-macro2",
|
"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]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
version = "1.0.16"
|
version = "1.0.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c"
|
checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "scopeguard"
|
|
||||||
version = "1.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.196"
|
version = "1.0.196"
|
||||||
|
|
@ -290,12 +98,6 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "smallvec"
|
|
||||||
version = "1.13.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.48"
|
version = "2.0.48"
|
||||||
|
|
@ -307,41 +109,12 @@ dependencies = [
|
||||||
"unicode-ident",
|
"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]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.12"
|
version = "1.0.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
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]]
|
[[package]]
|
||||||
name = "winapi"
|
name = "winapi"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
|
|
@ -363,60 +136,3 @@ name = "winapi-x86_64-pc-windows-gnu"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
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"
|
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,7 @@ name = "lyagushka"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[lib]
|
|
||||||
name = "lyagushka"
|
|
||||||
crate-type = ["cdylib"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
atty = "0.2.14"
|
atty = "0.2.14"
|
||||||
pyo3 = "0.20.2"
|
|
||||||
rand = "0.8.5"
|
|
||||||
serde = { version = "1.0.196", features = ["derive"] }
|
serde = { version = "1.0.196", features = ["derive"] }
|
||||||
serde_json = "1.0.113"
|
serde_json = "1.0.113"
|
||||||
|
|
|
||||||
61
readme.md
Normal file
61
readme.md
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
216
src/lib.rs
216
src/lib.rs
|
|
@ -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<f32>, // Z-score, to be calculated later
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fn load_dataset(filename: &str) -> io::Result<Vec<Point>> {
|
|
||||||
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<ClusterGapInfo> {
|
|
||||||
let mut results: Vec<ClusterGapInfo> = Vec::new();
|
|
||||||
if dataset.len() < 2 {
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mean_distance = dataset.windows(2)
|
|
||||||
.map(|w| distance(&w[0], &w[1]) as f32)
|
|
||||||
.sum::<f32>() / (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::<f32>() / 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<String> = env::args().collect();
|
|
||||||
let mut dataset: Vec<Point> = 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: {} <filename> <factor> <min_cluster_size>", args[0]);
|
|
||||||
eprintln!("Or pipe in a list of integers and provide <factor> <min_cluster_size>");
|
|
||||||
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<String> {
|
|
||||||
// Convert Python list to Rust Vec<Point>
|
|
||||||
let mut dataset: Vec<Point> = 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(())
|
|
||||||
}
|
|
||||||
231
src/main.rs
Normal file
231
src/main.rs
Normal file
|
|
@ -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<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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::<f32>() / 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<ClusterGapInfo> {
|
||||||
|
|
||||||
|
// 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::<f32>() / (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<ClusterGapInfo> = Vec::new(); // Stores the resulting clusters and gaps.
|
||||||
|
let mut current_cluster: Vec<Point> = 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<Point>, 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::<f32>() / (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::<f32>() / (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<String> = env::args().collect();
|
||||||
|
|
||||||
|
// Input handling
|
||||||
|
let dataset: Vec<Point> = if atty::is(atty::Stream::Stdin) {
|
||||||
|
if args.len() != 4 {
|
||||||
|
eprintln!("Usage: {} <filename> <factor> <min_cluster_size>", 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::<u32>().ok())
|
||||||
|
.map(Point::new)
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
stdin().lock().lines().filter_map(Result::ok)
|
||||||
|
.filter_map(|line| line.trim().parse::<u32>().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(())
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue