Initialize Track 2 Rust workspace

This commit is contained in:
welsberr 2026-04-11 07:29:59 -04:00
parent 4f36070a7f
commit b761cac274
9 changed files with 283 additions and 6 deletions

2
.gitignore vendored
View File

@ -10,6 +10,7 @@ venv/
build/ build/
dist/ dist/
*.egg-info/ *.egg-info/
target/
runs/state/* runs/state/*
!runs/state/.gitkeep !runs/state/.gitkeep
@ -23,3 +24,4 @@ runs/scratch/*
*.log *.log
*.tmp *.tmp
*.swp *.swp
Cargo.lock

10
Cargo.toml Normal file
View File

@ -0,0 +1,10 @@
[workspace]
members = ["rust/track2-core"]
resolver = "2"
[workspace.package]
edition = "2024"
license = "MIT"
authors = ["welsberr <welsberr@gmail.com>"]
repository = "https://git.cns.fyi/welsberr/renunney"

View File

@ -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 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 \ .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-figure1-m005 submit-figure1-m025 submit-figure1-m05 submit-figure1-m10 submit-figure1-m100 \
submit-all-figure1 status results-tree 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 " doctor Show key paths and verify local orchestration and Track 1 paths"
@echo " list-jobs List jobs in the local registry" @echo " list-jobs List jobs in the local registry"
@echo " track1-sim-smoke Run one local Track 1 simulation through renunney runner" @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-one Claim and run one queued job"
@echo " run-loop Run worker loop until queue empty" @echo " run-loop Run worker loop until queue empty"
@echo " run-loop-one Run exactly one queued job through the worker loop" @echo " run-loop-one Run exactly one queued job through the worker loop"
@ -59,6 +62,12 @@ track1-sim-smoke:
mkdir -p $(MPLCONFIGDIR) 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 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: run-one:
mkdir -p $(MPLCONFIGDIR) mkdir -p $(MPLCONFIGDIR)
MPLCONFIGDIR=$(MPLCONFIGDIR) $(PYTHON) $(ORCH) run-one --db $(DB) --result-root $(RESULT_ROOT) --scratch-root $(SCRATCH_ROOT) MPLCONFIGDIR=$(MPLCONFIGDIR) $(PYTHON) $(ORCH) run-one --db $(DB) --result-root $(RESULT_ROOT) --scratch-root $(SCRATCH_ROOT)

View File

@ -73,6 +73,7 @@ runtime now lives in `renunney`.
- [docs/MIGRATION.md](/mnt/CIFS/pengolodh/Docs/Projects/renunney/docs/MIGRATION.md) - [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/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/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 ## Layout
@ -107,6 +108,13 @@ Run one local Track 1 simulation:
make track1-sim-smoke make track1-sim-smoke
``` ```
Verify the Track 2 Rust workspace:
```bash
make rust-check
make rust-test
```
Submit a paper-scale Figure 1 treatment: Submit a paper-scale Figure 1 treatment:
```bash ```bash
@ -127,9 +135,12 @@ make collate-figure1
## Status ## Status
The Track 1 runtime and orchestration stack are now local to `renunney`. The The Track 1 runtime and orchestration stack are now local to `renunney`.
next major step is no longer migration of Track 1 code; it is either: 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, - 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.

View File

@ -14,9 +14,9 @@ Operational code still lives in:
`renunney` currently acts as: `renunney` currently acts as:
- a clean git repository, - 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, - 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 ## Recommended Migration Order
@ -44,6 +44,10 @@ Operational code still lives in:
11. Track 1 runtime path is now fully local to `renunney`. 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. 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. 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 ## Constraint

72
docs/TRACK2_RUST.md Normal file
View File

@ -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.

View File

@ -0,0 +1,12 @@
[package]
name = "track2-core"
version = "0.1.0"
edition = "2024"
license = "MIT"
authors = ["welsberr <welsberr@gmail.com>"]
description = "Track 2 threshold-centered cost-of-substitution simulation core"
[lib]
name = "track2_core"
path = "src/lib.rs"

View File

@ -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,
};

View File

@ -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<ThresholdBracket> {
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);
}
}