Initialize Track 2 Rust workspace
This commit is contained in:
parent
4f36070a7f
commit
b761cac274
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
9
Makefile
9
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)
|
||||
|
|
|
|||
19
README.md
19
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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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"
|
||||
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue