Blueprints

Fork a running simulation into counterfactual worlds

March until the physics fires an event, pause, then continue every candidate from that shared marched state. The fork is O(1) copy-on-write and each branch is alternated into its own audited world.

march ... untilforkbranchcontinue_forrefinereduce_all

Two levels. The trunk marches to an event; the study fans out from the pause it returns.

Trunk: march until the field satisfies a predicate

examples/avionics_examples/cfd/plasma_blackout/corridor/main.rs:81-89:

let onset = CfdFlow::march(&nominal)
    .couple(world::corridor_coupling(1.0, 0))
    .trigger(trigger)
    .from_field(world::initial_field())
    .until(|field, _| field.regime().map(|r| r.gnss_denied).unwrap_or(false))?;

The predicate reads the evolved field, so the pause lands where the chemistry crossed the receiver cutoff. See detect and act on a regime change.

Campaign: fork the pause

main.rs:91-112:

let corridor = CfdFlow::study("bank-angle corridor")
    .cases(model::coarse_commands())
    .fork(&onset)
    .branch(model::bank_world)
    .continue_for(constants::BRANCH_STEPS)
    .reduce_all(model::score_branches) // aim point from the ballistic branch first
    .inspect(|rows| utils_print::print_branches(COARSE_TITLE, rows))
    .refine(&onset, model::fine_candidates) // 0.5-deg bracket around the coarse winner
    .branch(model::bank_world)
    .continue_for(constants::BRANCH_STEPS)
    .reduce_all(model::score_branches) // same aim point: the rounds stay comparable
    .record(table_path())
    .gates(model::corridor_gates())
    .verdict()?;

refine forks the same onset pause a second time. Both rounds call reduce_all(model::score_branches), so they score against one shared aim point and remain comparable.

Per-branch command injection

Branches differ only in a world-published constant. model.rs:107-130:

pub fn descent_world(
    name: &'static str,
    bank_deg: FloatType,
) -> Result<CompressibleMarchConfig<FloatType>, PhysicsError> {
    world::descent_world(
        name,
        world::standard_atmosphere(),
        STEPS,
        &[("commanded_bank", utils::rad(bank_deg))],
    )
}

pub fn bank_world(cmd: &BankCommand) -> Result<CompressibleMarchConfig<FloatType>, PhysicsError> {
    descent_world(cmd.name, cmd.deg)
}

The constant reaches the marcher through the builder (shared/world.rs:127-129):

    for &(cname, value) in constants {
        builder = builder.publish_constant(cname, value);
    }

CommandedBank (shared/stages.rs:146-156) reads the published scalar into the control channel each step, and CyberneticCorrect clamps it against the envelope.

World alternation is applied by the driver

The example passes a world constructor to .branch(..). The study driver calls alternate_context for any branch whose world name differs from the baseline (deep_causality_cfd/src/types/flow/study/mod.rs:815-825):

                let world = &c.worlds[i];
                let stack = (c.coupling)(&c.cases[i], draw);
                let mut run = CfdFlow::march(&c.baseline);
                if world.name() != c.baseline.name() {
                    run = run.alternate_context(world);
                }

The audit rejoin (study/mod.rs:833-840) detects the !!ContextAlternation!! marker and labels each branch alternated or baseline.

The corridor example also calls alternate_context directly at main.rs:127 and main.rs:143, on diagnostic legs outside the study.

Measured fork cost

Sharing is O(1). Branches share the trunk read-only through Arc and take a copy-on-write clone at first divergent write. On a plume-coupled state (studies/qtt_rank_plume): fork setup 42 ns, per-branch continuation 0.67 to 1.04× the trunk step cost. The 0.67 case is a coast branch performing less work.

fork does not fail. A broken pause is carried through and surfaces as each branch’s error at continue_for, which keeps the failure attributable to the branch.

State forks compared with parameter forks

A parameter fork re-runs a model from different inputs; each branch is an independent world. The weather table has that shape.

A state fork pauses one marched coupled field and continues every branch from it, so each intervention feeds back into the flow it inherited. The corridor has that shape.

For a state fork, compare each branch against a frozen-field prediction of the same intervention: the same command schedule with the flow closure held at the fork’s value. If simple kinematics reproduces the divergence, the flow did not contribute to it.

The retropulsion descent runs that comparison as gate 4c. Its branches depart the frozen-drag prediction by up to 139.4 m/s against a 100 m/s threshold, and its flow observables spread 0.0202 across the roster where the corridor’s bank branches agreed to three digits.

Worked examples using this