Blueprints
Detect and act on a regime change
A classifier reads the evolved field each step, bands the continuous quantities into a discrete key, and logs only key transitions. Downstream stages and march predicates read the class.
RegimeClassify is a PhysicsStage. It reads the evolved field, derives the
governing model and the comms state, and stores a RegimeClass on the field.
From deep_causality_cfd/src/types/flow/corridor.rs:299-336:
impl<const D: usize, R: CfdScalar> PhysicsStage<D, R> for RegimeClassify<R> {
fn apply(
&self,
_ctx: &StepContext<'_, D, R>,
field: &mut CoupledField<R>,
) -> Result<(), PhysicsError> {
let Some(mfp) = field.scalar(self.mfp_field) else {
return Ok(());
};
// Rarefaction is worst where the mean free path is largest, so classify off its peak.
let mfp_peak = peak(mfp);
let length = Length::new(self.characteristic_length)?;
let kn = knudsen_number_kernel(mfp_peak, &length)?;
let model = self.model_for(kn);
// The comms side: peak electron density → plasma frequency → GNSS-denial decision.
let ne_peak = field
.scalar(self.ne_field)
.map(peak)
.unwrap_or_else(R::zero);
let blackout = self.trigger.evaluate(ElectronDensity::new(ne_peak)?)?;
let class = RegimeClass {
model,
knudsen: kn,
plasma_frequency: blackout.plasma_frequency,
gnss_denied: blackout.denied,
mach_regime: self.mach_regime_of(field),
thrust_state: self.thrust_state_of(field),
touchdown: self.touchdown_of(field),
};
// Log only genuine regime transitions (model band, comms-denial, or a flight-phase change).
let changed = field.regime().map(|prev| prev.key()) != Some(class.key());
if changed {
Two independent axes
The rarefaction axis takes the peak mean free path, forms Kn = λ / L through
knudsen_number_kernel, and bands it into a GoverningModel: continuum, slip,
transitional, or free-molecular. model_for holds the band edges
(corridor.rs:286-296).
The comms axis takes the peak electron density, maps it through the
BlackoutTrigger to a plasma frequency, and compares against the receiver band
to produce gnss_denied.
Both are derived from the evolved field each step. Neither is scheduled.
Transition detection
RegimeClass::key() (corridor.rs:140-156) returns the discrete 5-tuple
(model, gnss_denied, mach_regime, thrust_state, touchdown). It excludes the
continuous knudsen, plasma_frequency and Mach values by design.
Comparing the previous key against the new one means a provenance entry is
emitted on band crossings only, not on every step where Kn moved slightly.
The class is stored with field.set_regime(class) at corridor.rs:362.
Downstream reads
TrajectoryNav::apply gates the GNSS fold on the class
(corridor.rs:463):
let denied = field.regime().map(|r| r.gnss_denied).unwrap_or(false);
The same field drives the trunk march predicate in the corridor example
(main.rs:86):
.until(|field, _| field.regime().map(|r| r.gnss_denied).unwrap_or(false))
The blackout window is therefore located by the chemistry, and the pause the
counterfactual study forks from is the step where the classifier first raised
gnss_denied. The corridor run reports onset at 74.7 km and exit at 47.0 km
with a 58.4 s dwell.
The Mach, thrust and touchdown axes are opt-in through FlightAxes
(corridor.rs:163-188).
Worked examples using this