feat(engine,registry): ListSpace + campaign-run realization store (0107 tasks 2-3)
aura-engine: ListSpace — explicit point set implementing Space beside GridSpace/RandomSpace (a gate's survivor subset has no cartesian structure); GridSpace-identical arity/kind validation reusing the existing SweepError variants (KindMismatch.value_index carries the point ordinal for a list); empty point list is valid and sweeps to an empty family; re-exported at the crate root. aura-registry: CampaignRunRecord/CellRealization/StageRealization/ StageSelection — the thin campaign-level linking record over untouched family records (#198 decision 4) — with append_campaign_run (assigns run via next_campaign_run, a parallel counter beside next_run) and load_campaign_runs (missing file => empty) on the campaign_runs.jsonl sibling store; blueprint_path and find_blueprint_by_identity promoted to pub (the campaign executor resolves the same refs the referential tier validates). Gates: engine 290/0, registry 54/0, clippy -D warnings clean. refs #198
This commit is contained in:
@@ -75,7 +75,8 @@ pub use report::{
|
||||
RunReport, SelectionMode,
|
||||
};
|
||||
pub use sweep::{
|
||||
sweep, GridSpace, ParamRange, RandomSpace, Space, SweepError, SweepFamily, SweepPoint,
|
||||
sweep, GridSpace, ListSpace, ParamRange, RandomSpace, Space, SweepError, SweepFamily,
|
||||
SweepPoint,
|
||||
};
|
||||
pub use mc::{
|
||||
monte_carlo, r_bootstrap, resample_block, McAggregate, McDraw, McFamily, MetricStats,
|
||||
|
||||
+163
-14
@@ -1,11 +1,13 @@
|
||||
//! Param-sweep (C12.1): enumerate a blueprint's param-space — either a cartesian
|
||||
//! `GridSpace` (a discrete per-slot lattice) or a seeded `RandomSpace` (`N` draws
|
||||
//! over typed continuous ranges) — and run each point disjointly (C1). Both
|
||||
//! enumerations implement the `Space` trait that `sweep` is generic over, so
|
||||
//! either runs through one execution path. This module owns enumeration
|
||||
//! (`GridSpace` / `RandomSpace` / the `Space` trait), execution (`sweep`), and
|
||||
//! collection (`SweepFamily`); the per-point run-to-metrics closure is the
|
||||
//! author's (harness-specific sink glue the engine cannot generically own — C8/C18).
|
||||
//! Param-sweep (C12.1): enumerate a blueprint's param-space — a cartesian
|
||||
//! `GridSpace` (a discrete per-slot lattice), a seeded `RandomSpace` (`N` draws
|
||||
//! over typed continuous ranges), or an explicit `ListSpace` (an arbitrary
|
||||
//! validated point set, e.g. a gate's survivor subset) — and run each point
|
||||
//! disjointly (C1). All three enumerations implement the `Space` trait that
|
||||
//! `sweep` is generic over, so each runs through one execution path. This module
|
||||
//! owns enumeration (`GridSpace` / `RandomSpace` / `ListSpace` / the `Space`
|
||||
//! trait), execution (`sweep`), and collection (`SweepFamily`); the per-point
|
||||
//! run-to-metrics closure is the author's (harness-specific sink glue the engine
|
||||
//! cannot generically own — C8/C18).
|
||||
|
||||
use aura_core::{zip_params, Cell, ParamSpec, Scalar, ScalarKind};
|
||||
use crate::RunReport;
|
||||
@@ -125,15 +127,19 @@ impl Space for GridSpace {
|
||||
}
|
||||
}
|
||||
|
||||
/// A structural fault constructing a `GridSpace` or a `RandomSpace` — the shared
|
||||
/// typed gate before any run (grid faults: `Arity` / `KindMismatch` / `EmptyAxis`;
|
||||
/// random faults: `NonNumericRange` / `RangeKindMismatch` / `EmptyRange`).
|
||||
/// A structural fault constructing a `GridSpace`, a `RandomSpace`, or a
|
||||
/// `ListSpace` — the shared typed gate before any run (grid faults: `Arity` /
|
||||
/// `KindMismatch` / `EmptyAxis`; random faults: `NonNumericRange` /
|
||||
/// `RangeKindMismatch` / `EmptyRange`; list faults reuse `Arity` /
|
||||
/// `KindMismatch`, with `value_index` carrying the point ordinal).
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum SweepError {
|
||||
/// The number of axes does not equal the param-space slot count.
|
||||
/// The number of axes (grid/random) — or of a single point's values (list)
|
||||
/// — does not equal the param-space slot count.
|
||||
Arity { expected: usize, got: usize },
|
||||
/// A grid value's kind does not match its slot's declared kind. `slot` is the
|
||||
/// flat param-space index; `value_index` is the position within that axis.
|
||||
/// A grid or list value's kind does not match its slot's declared kind.
|
||||
/// `slot` is the flat param-space index; `value_index` is the position
|
||||
/// within that axis (grid) or the point ordinal (list).
|
||||
KindMismatch { slot: usize, value_index: usize, expected: ScalarKind, got: ScalarKind },
|
||||
/// A slot was given no values (would collapse the product to zero points).
|
||||
EmptyAxis { slot: usize },
|
||||
@@ -282,6 +288,58 @@ impl Space for RandomSpace {
|
||||
}
|
||||
}
|
||||
|
||||
/// An explicit, validated point set over a blueprint's param-space — the
|
||||
/// enumeration for arbitrary member subsets (e.g. a gate's survivor set) that
|
||||
/// no cartesian `GridSpace` can represent. Points are validated against
|
||||
/// `space` at construction (the `GridSpace::new` contract: arity per point,
|
||||
/// kind per slot); an empty point list is valid and yields an empty family.
|
||||
#[derive(Debug)]
|
||||
pub struct ListSpace {
|
||||
space: Vec<ParamSpec>,
|
||||
points: Vec<Vec<Scalar>>,
|
||||
}
|
||||
|
||||
impl ListSpace {
|
||||
/// Validate `points` against `space` (the blueprint's `param_space()`):
|
||||
/// every point carries one value per slot (`Arity`), every value the
|
||||
/// slot's declared kind (`KindMismatch`, whose `value_index` is the point
|
||||
/// ordinal here). An empty point list is allowed — a legitimately-empty
|
||||
/// survivor set is representable, it just enumerates zero points.
|
||||
pub fn new(space: &[ParamSpec], points: Vec<Vec<Scalar>>) -> Result<Self, SweepError> {
|
||||
for (point_index, point) in points.iter().enumerate() {
|
||||
if point.len() != space.len() {
|
||||
return Err(SweepError::Arity { expected: space.len(), got: point.len() });
|
||||
}
|
||||
for (slot, (v, ps)) in point.iter().zip(space).enumerate() {
|
||||
if v.kind() != ps.kind {
|
||||
return Err(SweepError::KindMismatch {
|
||||
slot,
|
||||
value_index: point_index,
|
||||
expected: ps.kind,
|
||||
got: v.kind(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Self { space: space.to_vec(), points })
|
||||
}
|
||||
}
|
||||
|
||||
impl Space for ListSpace {
|
||||
/// The points exactly as given, in input order (no enumeration structure —
|
||||
/// determinism is the caller's declared order, C1), lowered to tag-free
|
||||
/// cells in the declared kinds (the kind lives once, in `param_specs()`).
|
||||
fn points(&self) -> Vec<Vec<Cell>> {
|
||||
self.points
|
||||
.iter()
|
||||
.map(|p| p.iter().map(|s| s.cell()).collect())
|
||||
.collect()
|
||||
}
|
||||
fn param_specs(&self) -> &[ParamSpec] {
|
||||
&self.space
|
||||
}
|
||||
}
|
||||
|
||||
/// One enumerated point and the full `RunReport` its run produced.
|
||||
/// Self-describing: `params` is the point's coordinate in `param_space()` order,
|
||||
/// and `report` carries the run's `(manifest, metrics)` — the unit the run
|
||||
@@ -810,4 +868,95 @@ mod tests {
|
||||
.collect();
|
||||
assert_eq!(family.named_params(0), expected);
|
||||
}
|
||||
|
||||
/// A fake report whose manifest `commit` encodes the input cells — enough to
|
||||
/// prove the sweep closure ran on exactly the given point, without a harness
|
||||
/// run. A free `fn` (Sync) so it serves directly as the `sweep` closure.
|
||||
fn tagged_report(point: &[Cell]) -> RunReport {
|
||||
RunReport {
|
||||
manifest: RunManifest {
|
||||
commit: point.iter().map(|c| c.i64().to_string()).collect::<Vec<_>>().join(","),
|
||||
params: Vec::new(),
|
||||
window: (Timestamp(0), Timestamp(0)),
|
||||
seed: 0,
|
||||
broker: "test".to_string(),
|
||||
selection: None,
|
||||
instrument: None,
|
||||
topology_hash: None,
|
||||
project: None,
|
||||
},
|
||||
metrics: summarize(&[], &[]),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_space_runs_exactly_the_given_points_in_order() {
|
||||
let space = i64_space(2);
|
||||
// deliberately non-odometer order: the list enumerates the input order,
|
||||
// not any canonical order
|
||||
let ls = ListSpace::new(
|
||||
&space,
|
||||
vec![
|
||||
vec![Scalar::i64(3), Scalar::i64(5)],
|
||||
vec![Scalar::i64(2), Scalar::i64(4)],
|
||||
vec![Scalar::i64(3), Scalar::i64(4)],
|
||||
],
|
||||
)
|
||||
.expect("valid explicit point set");
|
||||
let expected = vec![
|
||||
vec![Cell::from_i64(3), Cell::from_i64(5)],
|
||||
vec![Cell::from_i64(2), Cell::from_i64(4)],
|
||||
vec![Cell::from_i64(3), Cell::from_i64(4)],
|
||||
];
|
||||
assert_eq!(ls.points(), expected);
|
||||
let family = sweep(&ls, tagged_report);
|
||||
assert_eq!(family.points.len(), 3);
|
||||
assert_eq!(family.space, space, "family carries the validated param-space");
|
||||
for (pt, cells) in family.points.iter().zip(&expected) {
|
||||
assert_eq!(&pt.params, cells, "points carried tag-free, in input order");
|
||||
let tag: String =
|
||||
cells.iter().map(|c| c.i64().to_string()).collect::<Vec<_>>().join(",");
|
||||
assert_eq!(pt.report.manifest.commit, tag, "the closure ran on exactly this point");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_space_refuses_arity_and_kind_mismatch() {
|
||||
let space = i64_space(2);
|
||||
// arity: the second point carries 1 value for a 2-slot space
|
||||
let err = ListSpace::new(
|
||||
&space,
|
||||
vec![vec![Scalar::i64(1), Scalar::i64(2)], vec![Scalar::i64(3)]],
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(err, SweepError::Arity { expected: 2, got: 1 });
|
||||
// kind: point 1, slot 1 carries an F64 in an I64 slot
|
||||
let err = ListSpace::new(
|
||||
&space,
|
||||
vec![
|
||||
vec![Scalar::i64(1), Scalar::i64(2)],
|
||||
vec![Scalar::i64(3), Scalar::f64(0.5)],
|
||||
],
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
err,
|
||||
SweepError::KindMismatch {
|
||||
slot: 1,
|
||||
value_index: 1,
|
||||
expected: ScalarKind::I64,
|
||||
got: ScalarKind::F64,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_space_allows_empty_point_list() {
|
||||
let space = i64_space(2);
|
||||
let ls = ListSpace::new(&space, vec![]).expect("an empty point list is valid");
|
||||
assert!(ls.points().is_empty(), "zero points enumerated");
|
||||
let family = sweep(&ls, tagged_report);
|
||||
assert!(family.points.is_empty(), "an empty ListSpace sweeps to an empty family");
|
||||
assert_eq!(family.space, space, "the empty family still carries the param-space");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user