DeepCausality · CFD

Counterfactual Fluid Dynamics

Counterfactual Fluid Dynamics and multidisciplinary analysis and optimization, by coupling fluid dynamics, multiple physics, navigation and control in one typed dynamic process.

01 / 07Counterfactual Dynamics

Counterfactual Dynamics

Fork a running simulation. The physics decides when the fork happens and how, and every alternate scenario continues.

CfdFlow is a language with two levels. At the trajectory level, march advances a coupled run until a predicate fires and hands back a pause you can resume. At the campaign level,study runs a family of cases forked from that pause and ends in a Verdict. Here is the plasma-blackout corridor, condensed:

// Trajectory level: march until the evolved sheath's n_e crosses the GPS L1 cutoff.
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))?;

// Campaign level: fork that pause once per candidate bank command, fly every
// branch concurrently, score the rows — then refine around the coarse winner
// from the same onset, and gate the two-round result.
let corridor = CfdFlow::study("bank-angle corridor")
    .cases(model::coarse_commands())
    .fork(&onset)                            // the shared flow-resolved fork point
    .branch(model::bank_world)               // one alternated world per command
    .continue_for(constants::BRANCH_STEPS)   // concurrent, copy-on-write
    .reduce_all(model::score_branches)
    .refine(&onset, model::fine_candidates)  // second round, same paused onset
    .branch(model::bank_world)
    .continue_for(constants::BRANCH_STEPS)
    .reduce_all(model::score_branches)
    .gates(model::corridor_gates())          // steering beats ballistic; fine >= coarse
    .verdict()?;

The fork itself shares the marched tensor state in O(1) and copies nothing. What follows is copy-on-write: each branch takes a single clone at its first field write, and continuing a march always writes. So the honest per-branch cost is O(cells), not O(1). That clone covers the per-cell scalar vectors, the navigation engine and the provenance log. Each branch then continues in a world of its own.

This is why forking state is not the same as sweeping parameters. A sweep reruns from initial conditions. A fork starts from the plume, the sheath and the filter as they stand at that instant, so the branches answer the decision the vehicle actually faces.

  • Fork setup 42 ns; branches enter by reference, with no tensor copied at fork time.
  • Powered continuation costs 1.00–1.04× an unforked trunk; a coasting branch is cheaper at 0.67×.
  • The corridor commits the best of seventeen worlds (six coarse, eleven fine) mid-descent: 2.07 m off the aim point against 20.00 m ballistic.
  • A mid-burn fork of a marched, plume-coupled state departs a frozen-drag prediction by 139.4 m/s.

Branch fan-outs run on scoped threads and produce bits identical to the sequential run. Every branch stamps a!!ContextAlternation!! marker into its provenance log naming the baseline it replaced.

A campaign takes a second form. Instead of forking one pause, it alternates whole worlds from a baseline and flies an ensemble of each:

let table = CfdFlow::study("weather-dispersion table")
    .cases(model::weather_cases())
    .baseline(model::standard_day)
    .alternate(model::weather_world)
    .ensemble(constants::MC_DRAWS)
    .couple(|case, draw| world::corridor_coupling(model::bias_departure(case.d_temp), draw))
    .march_for(constants::STEPS, world::initial_field)
    .reduce_ensemble(model::world_row)
    .gates(model::weather_gates())
    .verdict()?;

Six atmospheres, eight receiver-noise draws apiece, forty-eight descents, one gated table. verdict() returns data; the DSL never prints and never exits, so the caller decides what an exit code means.

Walk 1: how the fork is builtBlueprint: fork a running simulation

02 / 07Dynamic Regime Change

Dynamic Regime Change

The regime is classified dynamically from the evolving state. The physics decides the regime, and the regime governs the physics.

A vehicle entering or leaving orbit crosses several regimes on the way, and they do not change together. Three axes are tracked independently.

Flow regime on freestream Knudsen number
RegimeClassify bands the Knudsen number into a governing model: continuum Navier–Stokes, slip-corrected continuum, transitional, or free-molecular. Every transition is logged.Today this is a diagnostic carried on the evolved state. The crate does not switch closures on it, and no slip, transitional or free-molecular closure is implemented.
Dynamics regime on force ratio ε = a_aero/a_grav
While gravity dominates, a trajectory advances on the exact KS-conformal core with aero applied as a between-step kick. Once aero dominates, direct Cowell integration is the accurate choice. This ratio is the criterion for entering and leaving orbit.RegimeSwitch and aero_gravity_ratio are public API, but the shipped navigation engine does not call them. Applying the switch is the caller’s job.
Link regime on evolved electron density
The electron density sets the plasma frequency, and the plasma frequency decides whether the GNSS link exists. The Kalman filter’s measurement gating follows it.

The classifier is a stage in the coupling stack. It re-runs each step and writes its result onto the evolved field, and a march predicate reads it back. A transition is therefore an event the run finds, not a station it was told to stop at:

// RegimeClassify sits in the coupling stack and re-runs every step.
Coupling::between_steps()
    .then(FiniteRateIonizationStage::new(n_tot))   // writes "n_e"
    .then(RegimeClassify::new(l_char, trigger))    // reads it, classifies
    .build()

// field.regime() -> Option<RegimeClass<R>> {
//     model,            // Continuum | Slip | Transitional | FreeMolecular
//     knudsen,          // the Kn the model was selected from
//     plasma_frequency, // omega_p at the peak electron density
//     gnss_denied,      // omega_p above the configured comms band
//     mach_regime, thrust_state, touchdown,   // the powered-descent axes
// }

// A transition is a leg boundary: march until the classification changes.
let onset = CfdFlow::march(&nominal)
    .couple(world::corridor_coupling(1.0, 0))
    .from_field(world::initial_field())
    .until(|f, _| f.regime().map(|r| r.gnss_denied).unwrap_or(false))?;   // link lost

let exit = CfdFlow::march(&nominal)
    .alternate_context(&committed)
    .couple(world::corridor_coupling(1.0, 0))
    .from(peak.state())
    .until(|f, _| f.regime().map(|r| !r.gnss_denied).unwrap_or(false))?;  // link back

Blackout becomes an interval the run discovers. March to the onset event, fly the committed world through the dark, continue to the recovery event. Every transition lands in the provenance log. From a committed corridor run:

regime -> slip (GNSS-available), Kn=0.07829109848665225
regime -> slip (GNSS-denied), Kn=0.012690837165407727
regime -> continuum (GNSS-denied), Kn=0.00993838892165156
regime -> continuum (GNSS-available), Kn=0.0002551442196046344

One descent moves through orbit-like dynamics, slip flow, continuum flow, comms blackout and reacquisition, in one uninterrupted program.

Walk 1: the classifier in the stackBlueprint: handle a regime change

03 / 07Dynamic Multiphysics

Dynamic Multiphysics

Physics is coupled between steps via a fluent API that exchanges effects across steps over one shared field.

Coupling::between_steps()
    .then(VibrationalLagStage::new(/* Millikan-White bath */))
    .then(FiniteRateIonizationStage::new(n_tot).with_density_field("n_tot"))
    .then(RegimeClassify::new(l_char, trigger))
    .then(BankSteeredLift::new(rho_ref, cda_over_m, l_over_d))
    .then(TrajectoryNav::new(q_diag, gnss_var, optical_var).with_imu(imu))
    .then(CyberneticCorrect::new(SafetyEnvelope::new(q_max, g_max, bank_max)))
    .build()

Vibrational relaxation, reacting plasma chemistry, regime classification, steered aerodynamic force, a 17-state error-state Kalman filter and a bounded-correction safety gate, in one loop body. Stages talk to each other through named fields on the evolved state. An Err from any stage short-circuits the whole step.

Read the loop once and the circuit is visible. The evolved electron density gates which measurements the Kalman filter may fold. The Knudsen number selects the governing model. The safety gate’s clamped bank command is flown by the aero stage, which steers the trajectory that sets the next step’s freestream. Fluid dynamics, estimation and control close one loop, in one process.

That one stack is the loop body at both levels of the language. A trajectory march couples it directly. A campaign couples it per case and per draw, threading the ensemble index into the stack, and flies the whole matrix concurrently:

// Trajectory: couple the stack, march a fixed horizon, get one report.
let report = CfdFlow::march(&world).couple(stack).from_field(field0).run_for(steps)?;

// Campaign: the same stack, coupled per case and per draw, flown concurrently.
let table = CfdFlow::study("weather-dispersion table")
    .cases(model::weather_cases())
    .couple(|case, draw| world::corridor_coupling(model::bias_departure(case.d_temp), draw))
    .march_for(constants::STEPS, world::initial_field)
    .reduce_ensemble(model::world_row)
    .verdict()?;

Configuration is a separate layer from execution. Theflow_config layer holds owned descriptions: grids, schedules, seeds, stop conditions, observables, world-published constants. The flow layer materializes runs from them. A counterfactual is then the same flow handed a different description.

  • Plasma-retropulsion descent, blackout exit to touchdown: 2516 coupled steps across 4 legs, 16 gates, 355.6 s wall clock.
  • Weather-dispersion table: 6 atmospheres × 8 receiver-noise draws, 48 descents, flown concurrently to one gated table in 184.0 s.
  • Precision is a parameter of the whole stack: f32, f64 or 106-bit, changed at one type alias.

The three-walk tutorialBlueprint: couple multiphysicsBlueprint: couple navigation to physics

04 / 07Multiple Solver Paradigms

Multiple Solver Paradigms

Calculus-based, compression-based and analytic. All three sit behind the same CfdFlow language and the same scalar type, so you pick the best fit for your problem: the DEC solver for an incompressible cavity, the QTT marcher for a reentry layer, a fitted closure for the stagnation line.

CalculusDEC

DEC-native Navier–Stokes

Incompressible flow on a fixed mesh.

Velocity lives as an edge 1-form on a discrete exterior calculus. Each step marches the Leray-projected rate, so the field is divergence-free at every step. The SolenoidalField type-state carries that: there is no public constructor, every constructing path is a projection, and the type implements no arithmetic, so two projected fields cannot be added into an unprojected one.

Two wall-bounded escape hatches are public. constrain_edges and with_lift re-wrap a modified tensor without re-projecting, because the solver re-enters them on the output of a projection that already pinned those edges. Off that path the caller carries the invariant.

CompressionQTT

Quantized tensor-train marchers

Compressible Euler, 1-D through 3-D, including a body-fitted variant.

A 2^L grid stores order χ²·L: logarithmic in point count for a bounded bond dimension χ, with sharp structure paid for in χ. Whether χ stays bounded is the question the rank studies answered, and they found the driver to be coordinate alignment rather than sharpness. The compressible carrier acts on that with a shock-fitted inflow strip, which imposes the exact Rankine–Hugoniot state as the boundary of the marched layer, so the shock is never captured at all.

Storage is not runtime. On the incompressible immersed harness, per-step wall clock rose far faster than χ²·L while the achieved bond stayed flat, so a non-compression bottleneck dominates that path.

PointwiseAnalytic

Fitted closures

Gridless stagnation-line and relaxation problems.

Exact Rankine–Hugoniot jumps, Park two-temperature relaxation, the finite-rate ionization network, and pointwise Navier–Stokes regime evaluators with their causal-effect wrappers. A stagnation line with a fitted shock runs entirely on these, with no grid.

Precision as a Parameter

Every theory, solver, stage and observable is generic over one real scalar. A program fixes a single alias and the whole computation runs at that precision: f32 for speed, f64 for the industry-standard choice, or Float106 for reference-grade results with up to 30 significant digits.

/// Working precision.
pub type FloatType = f64; // or f32, or deep_causality_num::Float106

Specification constants stay exact f64 literals and are lifted into the working precision, so changing the alias reruns the whole program somewhere else on the ladder. The plasma-blackout corridor was flown at f64 and again at 106-bit: every gate and every discrete event step came out identical, the continuous witnesses agreed to 15 or 16 significant digits, and it cost about 11× the wall clock.

05 / 07Provenance Across Boundaries

Provenance for Comparison Across Boundaries

The append-only effect log continues across regimes, across physics, and across a counterfactual fan-out.

Every stage that changes the story of a run appends an entry: a regime transition, a navigation mode change, a pause, a re-seed, a world replacement. The corridor descent carries sixteen of them. Here are thirteen, in order, from the committed run:

regime -> slip (GNSS-available), Kn=0.07829109848665225
nav: aided (position fix folded)
regime -> slip (GNSS-denied), Kn=0.012690837165407727
nav: dead reckoning (no usable fix)
march paused at step 131
leg re-seeded from step 131 in world 'fine_bank_08': coupled field carried, ...
!!ContextAlternation!!: world 'nominal_descent' replaced with 'fine_bank_08'
regime -> continuum (GNSS-denied), Kn=0.00993838892165156
march paused at step 96
leg re-seeded from step 96 in world 'fine_bank_08': coupled field carried, ...
!!ContextAlternation!!: world 'nominal_descent' replaced with 'fine_bank_08'
regime -> continuum (GNSS-available), Kn=0.0002551442196046344
nav: aided (position fix folded)

Two things are worth reading closely. First, theregime -> entries and the nav: entries are interleaved in one sequence. That is what "across boundaries" means: the flow regime changed, the link died, the filter fell back to dead reckoning, and it is one chain rather than four logs to correlate afterwards. Second, the !!ContextAlternation!! entries name both sides of the intervention, the world replaced and the world that replaced it, so a counterfactual is auditable rather than implied.

Comparing across a transition

Because the log spans the transition, the same quantity can be read on both sides of it. The corridor's navigation error through the blackout, as committed:

LegLinkError vs truthPosition variance
Onset, 73.2 kmGNSS denied0.1823 m2.290e-1 m²
Peak passage, 60.9 kmGNSS denied1.5637 m2.671e1 m²
Flow-resolved exit, 46.8 kmGNSS available2.2879 m2.999e0 m²
Reacquisition, 47.4 kmGNSS available0.2804 m3.184e-1 m²

Variance grows by two orders of magnitude in the dark, then comes back once fixes resume. The same structure supports the comparison you usually want across a Mach boundary: the retropulsion descent logs supersonic, transonic and subsonic entries in one sequence with the thrust state on each line, so the flow parameters before and after the transition sit in the same record.

Comparing across branches

Each branch of a fan-out writes its own scenario log, so a variant that failed can be diffed against one that passed rather than re-run under a debugger. The weather table gates on exactly this: every dispersion world must carry the marker naming its baseline. Where a campaign uses the coupled march_for path, an optionalsave_log(path) flushes provenance to disk, one file per branch plus a .main.log naming every spawn and rejoin. Thefork / branch / continue_for chain does not thread that sink and writes no files; those branches still carry their full provenance in the returned report.

  • Corridor descent: 16 entries covering four legs, two regime bands and one committed counterfactual.
  • Retropulsion descent: 22 entries covering blackout, ignition commit, a carrier rebuild, the stopping burn and touchdown.
  • Weather table: 48 descents, each carrying its own log, gated on the alternation marker naming its baseline.

Walk 2: one baseline, six worldsBlueprint: fork a running simulationThe committed run output

06 / 07Evidence

Validation status

Thirteen verification targets. Each checks a run against a published reference or a discretization invariant, and exits nonzero when the check fails.

TargetReferenceMeasuredAgainstKind
qtt_sodExact Riemann solution (canonical star pressure p* = 0.3031)0.01750 (exact)Quantitative
qtt_ramc_staglineRAM-C II flight experiment, NASA Langley (1970)2.251e19 m⁻³~1e19 m⁻³Flight-anchored
dec_lid_cavity_re1000_verificationGhia, U0.1370Quantitative
dec_cylinder_verificationWilliamson (1996); Dröge & Verstappen (2005); Lehmkuhl, Rodríguez, Borrell & Oliva (2013)0.17140.164–0.165Quantitative
  • Quantitative 4
  • Anchored / invariant 6
  • Structural 3
Full validation status

Structural targets gate rank and cost. They do not gate physical accuracy. The status column records which of the two applies to each target.

07 / 07Worked runs

Worked examples

Four executable examples, ordered by the solver and DSL surface each requires. Every entry has a committed output file. The plasma-blackout family has its own three-stage tutorial.

  1. 01Flight envelope placardFeed a Mach–altitude test matrix in, get a gated placard table out.sweepGatesread_table / write_tableFittedNormalShockwell under a second
  2. 02Nozzle operating mapSweep back-pressure over a converging–diverging nozzle and get the operating map.duct_marchsweepGatesparallel fan-outunder a second
  3. 03VIV resonance marginSweep airspeed over a circular member and get the shedding-frequency margin table.DEC marchsweepGatesStrouhal extractionabout two and a half minutes
  4. 04Forecast horizon under precisionHow far ahead can a chaotic flow be forecast before roundoff destroys it?Arrow calculus (Rk4)causal monadprecision as a type parameterseconds