feat(aura-engine): seeded source + live RunManifest.seed (C12 seed-as-input)
A seeded source whose stream is fully determined by a u64 seed, making RunManifest.seed a live captured input instead of the dead 0 it was. This is C12's reconciliation of stochastic runs with C1: a run is bit-identical for a fixed seed (the seed is a captured input, not hidden nondeterminism), and a different seed gives a different-but-reproducible run. Precondition for the Monte-Carlo family (#68) and random param-sweep (#52). Engine (aura-engine): - SplitMix64: a ~10-line private, dependency-free PRNG. Chosen over a rand-family crate for guaranteed bit-stability across toolchains/crate versions — the whole value of seed-as-input is reproducibility, and a crate whose algorithm is not version-stable would silently break a recorded seed on a dep bump. No new direct dep. - SyntheticSpec + source(seed) -> impl Source: the Fn(u64) -> impl Source contract (Fork B — seed at the data-generation edge, outside the engine graph). Returns a Source, never a Vec, so the MC family can re-seed N times without materializing N streams. First cut collects to a VecSource internally; the signature does not name Vec, so a lazy generator is a drop-in replacement. Re-exported from lib.rs beside VecSource. CLI (aura-cli): - sim_optimal_manifest gains a seed: u64 param recorded into the manifest (the single Fork B capture site). All four existing seed-free callers (run_sample, run_sample_real, the sweep grid-report, run_macd) pass 0 unchanged — their determinism tests stay green. - A test-only run_sample_seeded + SeededTrace exercise a live seed and expose the drained sink trace, so the three e2e tests assert bit-identity at the trace level (strictly stronger than the folded 3-field metrics). Tests (RED-first): 2 engine producer tests (same-seed identical / different-seed differs) + 3 cli e2e tests (bit-identical trace, different-seed metrics differ, seed recorded in manifest). Full workspace green; clippy -D warnings clean. Errata vs plan 0042 (two compile-forced deviations, neither alters the spec contract): - source returns `impl Source + use<>`: under edition 2024 a bare `impl Source` captures the &self lifetime, which blocks boxing as Box<dyn Source + 'static> for run() (E0597). The source owns its data, so use<> (captures nothing) is correct. - SyntheticSpec is imported inside `#[cfg(test)] mod tests` in main.rs: it is used only by the seeded test vehicle, so a top-level import is unused in the non-test build under clippy -D warnings. closes #66
This commit is contained in:
@@ -122,12 +122,16 @@ fn sample_harness() -> (
|
||||
/// built-in harnesses — only `params` and `window` vary. Centralizing the broker
|
||||
/// label keeps it a single source (and a single edit point for #22's per-asset
|
||||
/// pip work).
|
||||
fn sim_optimal_manifest(params: Vec<(String, f64)>, window: (Timestamp, Timestamp)) -> RunManifest {
|
||||
fn sim_optimal_manifest(
|
||||
params: Vec<(String, f64)>,
|
||||
window: (Timestamp, Timestamp),
|
||||
seed: u64,
|
||||
) -> RunManifest {
|
||||
RunManifest {
|
||||
commit: option_env!("AURA_COMMIT").unwrap_or("unknown").to_string(),
|
||||
params,
|
||||
window,
|
||||
seed: 0,
|
||||
seed,
|
||||
broker: "sim-optimal(pip_size=0.0001)".to_string(),
|
||||
}
|
||||
}
|
||||
@@ -158,6 +162,7 @@ fn run_sample() -> RunReport {
|
||||
("exposure_scale".to_string(), 0.5),
|
||||
],
|
||||
window,
|
||||
0,
|
||||
),
|
||||
metrics,
|
||||
}
|
||||
@@ -220,6 +225,7 @@ fn run_sample_real(symbol: &str, from_ms: Option<i64>, to_ms: Option<i64>) -> Ru
|
||||
("exposure_scale".to_string(), 0.5),
|
||||
],
|
||||
window,
|
||||
0,
|
||||
),
|
||||
metrics,
|
||||
}
|
||||
@@ -382,7 +388,7 @@ fn sweep_family() -> SweepFamily {
|
||||
.map(|(ps, v)| (ps.name.clone(), scalar_as_param_f64(v)))
|
||||
.collect();
|
||||
RunReport {
|
||||
manifest: sim_optimal_manifest(params, window),
|
||||
manifest: sim_optimal_manifest(params, window, 0),
|
||||
metrics: summarize(&equity, &exposure),
|
||||
}
|
||||
})
|
||||
@@ -569,6 +575,7 @@ fn run_macd() -> RunReport {
|
||||
("exposure_scale".to_string(), 0.5),
|
||||
],
|
||||
window,
|
||||
0,
|
||||
),
|
||||
metrics,
|
||||
}
|
||||
@@ -607,6 +614,70 @@ fn main() {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
// `SyntheticSpec` is used only by the seeded test vehicle below, so it is
|
||||
// imported here rather than at module top (a top-level import would be
|
||||
// `unused` in the non-test binary build under `clippy -D warnings`).
|
||||
use aura_engine::SyntheticSpec;
|
||||
|
||||
/// The drained sink trace of a seeded run — the recorded rows of the equity
|
||||
/// and exposure sinks. Compared row-for-row so the C1 seed-determinism
|
||||
/// property is tested at the trace level (strictly stronger than the folded
|
||||
/// 3-field metrics). `PartialEq` not `Eq`: `Scalar` carries `f64`.
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct SeededTrace {
|
||||
equity: Vec<(Timestamp, Vec<Scalar>)>,
|
||||
exposure: Vec<(Timestamp, Vec<Scalar>)>,
|
||||
}
|
||||
|
||||
/// A seeded run of the sample harness: the synthetic stream is generated
|
||||
/// from `seed`, that same seed is recorded into the manifest, and the
|
||||
/// drained sink trace is returned alongside the report. Every byte of both
|
||||
/// is a function of `seed`.
|
||||
fn run_sample_seeded(seed: u64) -> (RunReport, SeededTrace) {
|
||||
let (mut h, rx_eq, rx_ex) = sample_harness();
|
||||
let spec = SyntheticSpec { start: 1.0, len: 64, step: 1 };
|
||||
let window = (Timestamp(1), Timestamp((spec.len as i64 - 1) * spec.step + 1));
|
||||
h.run(vec![Box::new(spec.source(seed))]);
|
||||
let eq_rows: Vec<(Timestamp, Vec<Scalar>)> = rx_eq.try_iter().collect();
|
||||
let ex_rows: Vec<(Timestamp, Vec<Scalar>)> = rx_ex.try_iter().collect();
|
||||
let metrics = summarize(&f64_field(&eq_rows, 0), &f64_field(&ex_rows, 0));
|
||||
let report = RunReport {
|
||||
manifest: sim_optimal_manifest(
|
||||
vec![
|
||||
("sma_fast".to_string(), 2.0),
|
||||
("sma_slow".to_string(), 4.0),
|
||||
("exposure_scale".to_string(), 0.5),
|
||||
],
|
||||
window,
|
||||
seed,
|
||||
),
|
||||
metrics,
|
||||
};
|
||||
(report, SeededTrace { equity: eq_rows, exposure: ex_rows })
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_seed_bit_identical_trace() {
|
||||
// Bit-identical sink trace for a fixed seed (acceptance bullet 1, C1).
|
||||
let (_, trace_a) = run_sample_seeded(42);
|
||||
let (_, trace_b) = run_sample_seeded(42);
|
||||
assert_eq!(trace_a, trace_b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn different_seed_different_trace() {
|
||||
// Different seeds perturb the trace (acceptance bullet 2).
|
||||
let (a, _) = run_sample_seeded(1);
|
||||
let (b, _) = run_sample_seeded(2);
|
||||
assert_ne!(a.metrics, b.metrics);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seed_recorded_in_manifest() {
|
||||
// The seed that drove the run is recorded (acceptance bullet 3).
|
||||
let (report, _) = run_sample_seeded(7);
|
||||
assert_eq!(report.manifest.seed, 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sample_blueprint_with_sinks_bootstraps_runs_and_drains() {
|
||||
|
||||
Reference in New Issue
Block a user