feat(campaign,cli): permit --select plateau in the walk_forward stage (#215)
The campaign walk_forward stage picked its per-window in-sample winner by optimize_deflated (argmax) unconditionally, dropping the stage's `select` field, and both the preflight and the `walkforward` verb's `--real` campaign sub-branch refused plateau:* with a forward-pointer here (the #210 decision-4 deferral; the synthetic non-`--real` sub-branch has accepted plateau since #173). Honour plateau selection when no gate precedes the wf stage. - Preflight: the plateau-in-wf refusal is narrowed to gate-preceded stages. With no preceding gate the survivors are the full sweep grid, so the parameter lattice (grid.axis_lens) is exact and optimize_plateau is sound; a gate filters survivors below the grid, breaking the lattice, so plateau after any gate stays refused. The refusal is static — document validity must be data-independent (a gate's filtering depends on the data) — and keeps the existing "a gated survivor subset has no parameter lattice" message, which now describes exactly the only case it fires. - Executor: run_walk_forward_stage threads the per-cell grid.axis_lens and picks each window's in-sample winner via select_sweep_winner — the same dispatch the sweep stage uses. (Argmax, deflate=true) is byte-identical to the old optimize_deflated call, so the argmax grade and content-id pins are unchanged; the plateau arms read axis_lens and ignore deflate. - CLI: the walkforward verb threads --select through translate_walkforward / run_walkforward_sugar (a walkforward-local param, not the shared four-verb SugarInvocation), mapping the parsed Selection to the research SelectRule via select_rule_of; the refusal and its select_is_plateau helper are deleted. Scope is walkforward-only — the mc verb's wf-stage select is untouched. The plateau:worst grade anchor (stitched_total_pips = -9683776.67) is distinct from the argmax anchor (-10398606.67), proving the plateau path genuinely changes winner selection rather than falling back to argmax. A gate-free plateau process validates clean while a gate-preceded one is refused (a new campaign-validate e2e). Argmax defaults stay byte-identical. closes #215
This commit is contained in:
+21
-17
@@ -686,6 +686,16 @@ fn parse_select(s: &str) -> Result<Selection, ()> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Map the CLI `--select` value to the research selection rule the campaign
|
||||
/// document carries.
|
||||
fn select_rule_of(sel: Selection) -> aura_research::SelectRule {
|
||||
match sel {
|
||||
Selection::Argmax => aura_research::SelectRule::Argmax,
|
||||
Selection::Plateau(PlateauMode::Mean) => aura_research::SelectRule::PlateauMean,
|
||||
Selection::Plateau(PlateauMode::Worst) => aura_research::SelectRule::PlateauWorst,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a comma-separated list of `T` (each item parsed via `FromStr`), rejecting
|
||||
/// any item that fails to parse — the shared validator for the r-sma grid flags
|
||||
/// (#137). The caller folds the `Err` into the subcommand `usage()` so a malformed
|
||||
@@ -2300,13 +2310,6 @@ fn wf_ms_sizes() -> (u64, u64, u64) {
|
||||
)
|
||||
}
|
||||
|
||||
/// True iff `--select` names a plateau mode (refused on the dissolved path, Fork B,
|
||||
/// until #215 ships the wf-stage plateau relaxation). Reuses `parse_select` so the
|
||||
/// vocabulary stays single-sourced.
|
||||
fn select_is_plateau(select: Option<&str>) -> bool {
|
||||
matches!(select.map(parse_select), Some(Ok(Selection::Plateau(_))))
|
||||
}
|
||||
|
||||
/// Resolve a single optional stop knob (`--stop-length`/`--stop-k`): an absent flag
|
||||
/// defaults to `default`; a present one parses via [`parse_csv_list`] and refuses
|
||||
/// unless it holds exactly one value (the stop is a risk regime, not a swept axis).
|
||||
@@ -2938,16 +2941,16 @@ fn dispatch_walkforward(a: WalkforwardCmd, env: &project::Env) {
|
||||
// The campaign path (#220): the real-archive execution routes
|
||||
// through the one campaign executor, over the user's own
|
||||
// blueprint and axes (formerly the welded r-sma branch).
|
||||
if let Some(s) = a.select.as_deref()
|
||||
&& parse_select(s).is_err()
|
||||
{
|
||||
eprintln!("aura: {}", usage());
|
||||
std::process::exit(2);
|
||||
}
|
||||
if select_is_plateau(a.select.as_deref()) {
|
||||
eprintln!("aura: --select plateau is not yet available on the campaign path; see #215");
|
||||
std::process::exit(2);
|
||||
}
|
||||
let select = match a.select.as_deref() {
|
||||
Some(s) => match parse_select(s) {
|
||||
Ok(sel) => select_rule_of(sel),
|
||||
Err(()) => {
|
||||
eprintln!("aura: {}", usage());
|
||||
std::process::exit(2);
|
||||
}
|
||||
},
|
||||
None => aura_research::SelectRule::Argmax,
|
||||
};
|
||||
let (name, symbol, stop_length, stop_k, from, to) =
|
||||
walkforward_args_from(&a).unwrap_or_else(|m| {
|
||||
eprintln!("aura: {m}");
|
||||
@@ -2988,6 +2991,7 @@ fn dispatch_walkforward(a: WalkforwardCmd, env: &project::Env) {
|
||||
out_of_sample_ms: oos_ms,
|
||||
step_ms,
|
||||
},
|
||||
select,
|
||||
env,
|
||||
)
|
||||
.unwrap_or_else(|m| {
|
||||
|
||||
@@ -375,6 +375,7 @@ pub(crate) fn translate_walkforward(
|
||||
inv: &SugarInvocation,
|
||||
metric: &str,
|
||||
w: WfWindows,
|
||||
select: SelectRule,
|
||||
) -> Result<GeneratedWalkforward, String> {
|
||||
let process = ProcessDoc {
|
||||
format_version: FORMAT_VERSION,
|
||||
@@ -395,7 +396,7 @@ pub(crate) fn translate_walkforward(
|
||||
step_ms: w.step_ms,
|
||||
mode: WfMode::Rolling,
|
||||
metric: metric.to_string(),
|
||||
select: SelectRule::Argmax,
|
||||
select,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -445,9 +446,10 @@ pub(crate) fn run_walkforward_sugar(
|
||||
inv: &SugarInvocation,
|
||||
metric: &str,
|
||||
w: WfWindows,
|
||||
select: SelectRule,
|
||||
env: &crate::project::Env,
|
||||
) -> Result<(), String> {
|
||||
let generated = translate_walkforward(inv, metric, w)?;
|
||||
let generated = translate_walkforward(inv, metric, w, select)?;
|
||||
let probe_params = probe_params_from(inv.axes);
|
||||
validate_before_register(&generated.process, &generated.campaign, inv.blueprint_canonical, &probe_params, env)?;
|
||||
let reg = env.registry();
|
||||
@@ -817,8 +819,8 @@ mod tests {
|
||||
fn translate_walkforward_is_deterministic_and_carries_the_regime() {
|
||||
let ax = wf_axes();
|
||||
let i = inv(&ax, "walkforward", &["GER40"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}");
|
||||
let a = translate_walkforward(&i, "sqn_normalized", WF_W).unwrap();
|
||||
let b = translate_walkforward(&i, "sqn_normalized", WF_W).unwrap();
|
||||
let a = translate_walkforward(&i, "sqn_normalized", WF_W, SelectRule::Argmax).unwrap();
|
||||
let b = translate_walkforward(&i, "sqn_normalized", WF_W, SelectRule::Argmax).unwrap();
|
||||
assert_eq!(
|
||||
content_id_of(&campaign_to_json(&a.campaign)),
|
||||
content_id_of(&campaign_to_json(&b.campaign)),
|
||||
@@ -866,9 +868,9 @@ mod tests {
|
||||
#[test]
|
||||
fn translate_walkforward_diverging_regimes_do_not_collide_content_ids() {
|
||||
let ax = wf_axes();
|
||||
let base = translate_walkforward(&inv(&ax, "walkforward", &["GER40"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}"), "sqn_normalized", WF_W).unwrap();
|
||||
let different_k = translate_walkforward(&inv(&ax, "walkforward", &["GER40"], Some(VolStop { length: 14, k: 3.0 }), "{\"bp\":1}"), "sqn_normalized", WF_W).unwrap();
|
||||
let different_length = translate_walkforward(&inv(&ax, "walkforward", &["GER40"], Some(VolStop { length: 20, k: 2.0 }), "{\"bp\":1}"), "sqn_normalized", WF_W).unwrap();
|
||||
let base = translate_walkforward(&inv(&ax, "walkforward", &["GER40"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}"), "sqn_normalized", WF_W, SelectRule::Argmax).unwrap();
|
||||
let different_k = translate_walkforward(&inv(&ax, "walkforward", &["GER40"], Some(VolStop { length: 14, k: 3.0 }), "{\"bp\":1}"), "sqn_normalized", WF_W, SelectRule::Argmax).unwrap();
|
||||
let different_length = translate_walkforward(&inv(&ax, "walkforward", &["GER40"], Some(VolStop { length: 20, k: 2.0 }), "{\"bp\":1}"), "sqn_normalized", WF_W, SelectRule::Argmax).unwrap();
|
||||
let base_id = content_id_of(&campaign_to_json(&base.campaign));
|
||||
let k_id = content_id_of(&campaign_to_json(&different_k.campaign));
|
||||
let length_id = content_id_of(&campaign_to_json(&different_length.campaign));
|
||||
@@ -893,6 +895,7 @@ mod tests {
|
||||
&inv(&ax, "walkforward", &["GER40"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}"),
|
||||
"sqn_normalized",
|
||||
WF_W,
|
||||
SelectRule::Argmax,
|
||||
)
|
||||
.unwrap();
|
||||
let (process_id, campaign_id) = register_generated_wf(®, &generated).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user