From b761cac2742218b9c9876c93d89a4583e0422baf Mon Sep 17 00:00:00 2001 From: welsberr Date: Sat, 11 Apr 2026 07:29:59 -0400 Subject: [PATCH] Initialize Track 2 Rust workspace --- .gitignore | 2 + Cargo.toml | 10 +++ Makefile | 9 ++ README.md | 19 +++- docs/MIGRATION.md | 8 +- docs/TRACK2_RUST.md | 72 +++++++++++++++ rust/track2-core/Cargo.toml | 12 +++ rust/track2-core/src/lib.rs | 17 ++++ rust/track2-core/src/threshold.rs | 140 ++++++++++++++++++++++++++++++ 9 files changed, 283 insertions(+), 6 deletions(-) create mode 100644 Cargo.toml create mode 100644 docs/TRACK2_RUST.md create mode 100644 rust/track2-core/Cargo.toml create mode 100644 rust/track2-core/src/lib.rs create mode 100644 rust/track2-core/src/threshold.rs diff --git a/.gitignore b/.gitignore index 74a7abe..7550397 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ venv/ build/ dist/ *.egg-info/ +target/ runs/state/* !runs/state/.gitkeep @@ -23,3 +24,4 @@ runs/scratch/* *.log *.tmp *.swp +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..992c650 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[workspace] +members = ["rust/track2-core"] +resolver = "2" + +[workspace.package] +edition = "2024" +license = "MIT" +authors = ["welsberr "] +repository = "https://git.cns.fyi/welsberr/renunney" + diff --git a/Makefile b/Makefile index 2c39ceb..f1904cf 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,7 @@ FIG1_M10 := $(REPO_ROOT)/config/track1_figure1_paper_M_1_0.json FIG1_M100 := $(REPO_ROOT)/config/track1_figure1_paper_M_10_0.json .PHONY: help init doctor list-jobs run-one run-loop run-loop-one collate-figure1 track1-sim-smoke \ + rust-check rust-test \ submit-figure1-m005 submit-figure1-m025 submit-figure1-m05 submit-figure1-m10 submit-figure1-m100 \ submit-all-figure1 status results-tree @@ -24,6 +25,8 @@ help: @echo " doctor Show key paths and verify local orchestration and Track 1 paths" @echo " list-jobs List jobs in the local registry" @echo " track1-sim-smoke Run one local Track 1 simulation through renunney runner" + @echo " rust-check Run cargo check for the Track 2 Rust workspace" + @echo " rust-test Run cargo test for the Track 2 Rust workspace" @echo " run-one Claim and run one queued job" @echo " run-loop Run worker loop until queue empty" @echo " run-loop-one Run exactly one queued job through the worker loop" @@ -59,6 +62,12 @@ track1-sim-smoke: mkdir -p $(MPLCONFIGDIR) MPLCONFIGDIR=$(MPLCONFIGDIR) $(PYTHON) $(TRACK1) --mode simulate --K 5000 --N0 50 --n 1 --u 5e-6 --R 10 --T 40 --epochs 8 --seed 1 +rust-check: + cargo check --manifest-path $(REPO_ROOT)/Cargo.toml + +rust-test: + cargo test --manifest-path $(REPO_ROOT)/Cargo.toml + run-one: mkdir -p $(MPLCONFIGDIR) MPLCONFIGDIR=$(MPLCONFIGDIR) $(PYTHON) $(ORCH) run-one --db $(DB) --result-root $(RESULT_ROOT) --scratch-root $(SCRATCH_ROOT) diff --git a/README.md b/README.md index b209d92..e69ae61 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ runtime now lives in `renunney`. - [docs/MIGRATION.md](/mnt/CIFS/pengolodh/Docs/Projects/renunney/docs/MIGRATION.md) - [docs/WORKFLOW.md](/mnt/CIFS/pengolodh/Docs/Projects/renunney/docs/WORKFLOW.md) - [docs/NUNNEY_ANALYSIS.md](/mnt/CIFS/pengolodh/Docs/Projects/renunney/docs/NUNNEY_ANALYSIS.md) +- [docs/TRACK2_RUST.md](/mnt/CIFS/pengolodh/Docs/Projects/renunney/docs/TRACK2_RUST.md) ## Layout @@ -107,6 +108,13 @@ Run one local Track 1 simulation: make track1-sim-smoke ``` +Verify the Track 2 Rust workspace: + +```bash +make rust-check +make rust-test +``` + Submit a paper-scale Figure 1 treatment: ```bash @@ -127,9 +135,12 @@ make collate-figure1 ## Status -The Track 1 runtime and orchestration stack are now local to `renunney`. The -next major step is no longer migration of Track 1 code; it is either: +The Track 1 runtime and orchestration stack are now local to `renunney`. +Track 2 has also started: the repo now includes a Rust workspace and an +initial `track2-core` crate for threshold-centered abstractions. The current +next major steps are: -- hardening multi-host orchestration, +- hardening multi-host orchestration for Track 1 replication, - organizing publication-quality replication outputs, -- or starting the Rust-backed Track 2 path. +- and expanding the Rust Track 2 core from threshold abstractions into a + simulation-state and estimation kernel. diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md index c682466..8d41be5 100644 --- a/docs/MIGRATION.md +++ b/docs/MIGRATION.md @@ -14,9 +14,9 @@ Operational code still lives in: `renunney` currently acts as: - a clean git repository, -- a run-control wrapper, +- the local home of the Track 1 runtime and orchestration stack, - a stable place for deployment and orchestration commands, -- the eventual destination for migrated code. +- and the starting point for the Track 2 Rust implementation. ## Recommended Migration Order @@ -44,6 +44,10 @@ Operational code still lives in: 11. Track 1 runtime path is now fully local to `renunney`. 12. Reduce or remove any remaining compatibility-layer imports outside the Track 1 runtime path. 13. Migrate docs and example configs last, after path references are updated. +14. Start Track 2 in-repo as a Rust workspace: + - `Cargo.toml` + - `rust/track2-core` + - `docs/TRACK2_RUST.md` ## Constraint diff --git a/docs/TRACK2_RUST.md b/docs/TRACK2_RUST.md new file mode 100644 index 0000000..73a2957 --- /dev/null +++ b/docs/TRACK2_RUST.md @@ -0,0 +1,72 @@ +# Track 2 Rust Plan + +Updated: 2026-04-11 + +## Purpose + +This note defines the initial Rust entry point for Track 2 in `renunney`. + +Track 2 is not a line-by-line translation of Nunney's published threshold +heuristic. It is the modern path: explicit threshold definitions, clearer +simulation contracts, and a performant kernel. + +## Why Rust + +Rust is the preferred Track 2 kernel language because it directly addresses the +main engineering problems revealed by Track 1: + +- heavy repeated stochastic simulation, +- threshold sweeps over many independent jobs, +- need for clear data structures and reproducible binaries, +- and a likely future need for Python bindings or service backends. + +## Initial Scope + +The first Rust step is intentionally narrow: + +- create a Rust workspace, +- define a Track 2 core crate, +- and encode threshold-centered abstractions rather than immediately porting + the entire biological simulator. + +This is the correct order because Track 2 should start from a clean statement +of what is being estimated. + +## Current Crate + +The initial crate is: + +- `rust/track2-core` + +Current contents: + +- `ExtinctionCount` +- `ThresholdPoint` +- `ThresholdBracket` +- `ThresholdEstimate` +- `bracket_threshold(...)` +- `midpoint_threshold(...)` + +These are placeholders for a modern threshold-estimation path where: + +- extinction probability is explicit, +- bracketing is explicit, +- and the estimator is separate from the simulation kernel. + +## Next Rust Steps + +1. Add a simulation-state model for Track 2. +2. Add a trait or function contract for producing extinction probabilities from + repeated stochastic runs. +3. Add a threshold search strategy that consumes those estimates. +4. Add serialization-friendly input/output structs for orchestration. +5. Only then start porting heavy simulation loops from Python. + +## Operational Targets + +The repo Makefile should treat Rust as a first-class build/test surface: + +- `make rust-test` +- `make rust-check` + +That keeps Track 2 visible in daily workflow from the start. diff --git a/rust/track2-core/Cargo.toml b/rust/track2-core/Cargo.toml new file mode 100644 index 0000000..8b510f4 --- /dev/null +++ b/rust/track2-core/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "track2-core" +version = "0.1.0" +edition = "2024" +license = "MIT" +authors = ["welsberr "] +description = "Track 2 threshold-centered cost-of-substitution simulation core" + +[lib] +name = "track2_core" +path = "src/lib.rs" + diff --git a/rust/track2-core/src/lib.rs b/rust/track2-core/src/lib.rs new file mode 100644 index 0000000..b9758f3 --- /dev/null +++ b/rust/track2-core/src/lib.rs @@ -0,0 +1,17 @@ +//! Track 2 core for the cost-of-substitution project. +//! +//! Track 2 is intentionally not a line-by-line reproduction of Nunney's +//! published threshold heuristic. Its goal is to provide a performant, +//! explicitly specified simulation and threshold-estimation substrate that can +//! later support richer kernels and cleaner inference. + +pub mod threshold; + +pub use threshold::{ + ExtinctionCount, + ThresholdBracket, + ThresholdEstimate, + ThresholdPoint, + bracket_threshold, + midpoint_threshold, +}; diff --git a/rust/track2-core/src/threshold.rs b/rust/track2-core/src/threshold.rs new file mode 100644 index 0000000..8db4749 --- /dev/null +++ b/rust/track2-core/src/threshold.rs @@ -0,0 +1,140 @@ +//! Threshold-centered helpers for Track 2. +//! +//! These functions do not assume Nunney's published 20-run acceptance rule. +//! They work with explicit extinction probabilities or extinction counts over a +//! set of trials, which is the right abstraction for the modern Track 2 path. + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ExtinctionCount { + pub extinct: u32, + pub total: u32, +} + +impl ExtinctionCount { + pub fn new(extinct: u32, total: u32) -> Self { + assert!(total > 0, "total trials must be positive"); + assert!(extinct <= total, "extinct count cannot exceed total trials"); + Self { extinct, total } + } + + pub fn survival_probability(self) -> f64 { + 1.0 - (self.extinct as f64 / self.total as f64) + } + + pub fn extinction_probability(self) -> f64 { + self.extinct as f64 / self.total as f64 + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct ThresholdPoint { + pub t_value: f64, + pub extinction_probability: f64, +} + +impl ThresholdPoint { + pub fn new(t_value: f64, extinction_probability: f64) -> Self { + assert!(t_value > 0.0, "T must be positive"); + assert!( + (0.0..=1.0).contains(&extinction_probability), + "extinction probability must lie in [0, 1]" + ); + Self { + t_value, + extinction_probability, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct ThresholdBracket { + pub lower: ThresholdPoint, + pub upper: ThresholdPoint, + pub target_extinction_probability: f64, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct ThresholdEstimate { + pub target_extinction_probability: f64, + pub estimated_t: f64, + pub lower_t: f64, + pub upper_t: f64, +} + +pub fn bracket_threshold( + lower: ThresholdPoint, + upper: ThresholdPoint, + target_extinction_probability: f64, +) -> Option { + assert!( + (0.0..=1.0).contains(&target_extinction_probability), + "target extinction probability must lie in [0, 1]" + ); + assert!( + lower.t_value < upper.t_value, + "lower T must be strictly less than upper T" + ); + + let lower_delta = lower.extinction_probability - target_extinction_probability; + let upper_delta = upper.extinction_probability - target_extinction_probability; + + if lower_delta == 0.0 || upper_delta == 0.0 || lower_delta.signum() != upper_delta.signum() { + Some(ThresholdBracket { + lower, + upper, + target_extinction_probability, + }) + } else { + None + } +} + +pub fn midpoint_threshold(bracket: ThresholdBracket) -> ThresholdEstimate { + ThresholdEstimate { + target_extinction_probability: bracket.target_extinction_probability, + estimated_t: 0.5 * (bracket.lower.t_value + bracket.upper.t_value), + lower_t: bracket.lower.t_value, + upper_t: bracket.upper.t_value, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn extinction_count_probabilities_are_consistent() { + let count = ExtinctionCount::new(3, 10); + assert!((count.extinction_probability() - 0.3).abs() < 1e-12); + assert!((count.survival_probability() - 0.7).abs() < 1e-12); + } + + #[test] + fn bracket_threshold_detects_crossing() { + let lower = ThresholdPoint::new(8.0, 0.8); + let upper = ThresholdPoint::new(12.0, 0.2); + let bracket = bracket_threshold(lower, upper, 0.5).expect("expected bracket"); + assert_eq!(bracket.lower.t_value, 8.0); + assert_eq!(bracket.upper.t_value, 12.0); + } + + #[test] + fn bracket_threshold_rejects_non_crossing_points() { + let lower = ThresholdPoint::new(8.0, 0.8); + let upper = ThresholdPoint::new(12.0, 0.7); + assert!(bracket_threshold(lower, upper, 0.5).is_none()); + } + + #[test] + fn midpoint_threshold_returns_bracket_midpoint() { + let bracket = ThresholdBracket { + lower: ThresholdPoint::new(8.0, 0.8), + upper: ThresholdPoint::new(12.0, 0.2), + target_extinction_probability: 0.5, + }; + let estimate = midpoint_threshold(bracket); + assert_eq!(estimate.estimated_t, 10.0); + assert_eq!(estimate.lower_t, 8.0); + assert_eq!(estimate.upper_t, 12.0); + } +}