Browse documentation
Docs/Language

Structural Causal Models

A CAUSAL definition keeps mechanism identity. Each body line is one endogenous mechanism over explicit exogenous inputs and other mechanisms, so an intervention can replace a whole equation…

Structural causal models

A CAUSAL definition keeps mechanism identity. Each body line is one endogenous mechanism over explicit exogenous inputs and other mechanisms, so an intervention can replace a whole equation rather than overwrite a value.

market = CAUSAL(noise: number, shock: number)
  spend = 100 + 20 * noise
  sales = 500 + 2 * spend + 50 * noise + shock
END

baseline = market(0.2, -3)
changed = INTERVENE(market, {spend: 150}, {noise: 0.2, shock: -3})
effect = changed.sales - baseline.sales

The baseline world has spend 104 and sales 715. The intervened world fixes spend at 150 and reads sales 807, so the paired effect is 92. Both worlds use the same supplied noise and shock: the shared cause stays visible in the authored structure.

CAUSAL market(noise: number, shock: number) … END is the equivalent declaration spelling. A model with no inputs omits the parentheses. The definition binds a pure callable plus GRAPH, REPORT, INTERVENE and BATCH members. Reading or importing it evaluates nothing, samples nothing and starts no study. Every member runs on the ordinary compiled evaluator.

Inputs and mechanisms

The header names exogenous inputs with ordinary Grid types. Exogenous means supplied from outside the declared mechanisms, not random or unobserved: fixed coefficients and measured covariates are explicit inputs too.

Each body line name = expression or name is Type = expression defines one mechanism. Forward references are allowed; evaluation order is inferred from the dependencies, not from authored order. Mechanisms may use arithmetic, comparisons, records, choices, Option/Result values, bounded collections, pure helpers and decisions. Cycles, including conditional cycles, are compile errors (GRID_CAUSAL_CYCLE); a mutable cell read, a volatile function or an effectful call is refused (GRID_CAUSAL_CAPTURE, GRID_CAUSAL_EFFECT). Distribution constructors keep their point estimates inside a mechanism; NORMAL does not become a hidden draw. LET, DO and lambda locals may shadow mechanism or input names without creating graph edges. A binding's right-hand side still reads its outer scope, so a genuine self-reference there remains a cycle error.

Input, mechanism and member names are unique case-insensitively. The names inputs and replacements are reserved for the generated member parameters.

Calling and intervening

Form Meaning
market(noise, shock) Evaluate every mechanism once in dependency order and return the world record
INTERVENE(market, replacements, inputs) Replace each targeted equation, then evaluate; inputs is the complete named exogenous record
market.INTERVENE(replacements, inputs) The same member spelled directly
market.REPORT(inputs, replacements) One evaluation plus grid.causal.report.v1 explanation data; pass RECORD() for no replacements
market.GRAPH() Authored structure without evaluation: inputs, mechanisms, dependency order and edges

A hard intervention removes the targeted equation and all of its incoming edges before evaluation. The replaced equation never runs, so an expensive or failing original mechanism cannot affect the intervened world. Replacement values are evaluated in the caller's scope; {price: baseline.price * 0.9} reads a previously computed world, never the new one. Unknown targets, input names used as targets and incomplete or oversized input records are errors (CAUSAL_TARGET, CAUSAL_INPUTS). An empty replacement record is the baseline on the same inputs.

An error in any returned mechanism fails the call; a world is never returned with a partly evaluated record. Declared types are checked on inputs (CAUSAL_INPUT_TYPE) and mechanism results (CAUSAL_MECHANISM_TYPE).

Reports and graphs

REPORT returns outcome as OK(world) or ERR(cause), the definition identity, the replaced targets, the dependency order and every authored edge. Failures identify the mechanism, input/evaluation stage and source origin, with the exact evaluated prefix. Reports retain the input-generation identity and identify whether the explanation is complete. Imported component origins point to the composing USE site. Portable owning syntax omits source offsets, so recompiling it reports absent origins; it never invents offsets without the source metadata. Runtime bytecode retains the origins compiled into its report. GRAPH returns grid.causal.graph.v1 with each mechanism's inputs and parents. These describe the authored model; they do not validate causal assumptions, identify effects from data or infer unobserved causes.

Components and qualified interventions

A component remains part of the causal graph. Import its definition normally, then map each exogenous argument explicitly:

USE "causal-demand-component.grid" AS economics

store = CAUSAL(taste: number, unit_cost: number)
  USE demand = economics.demand_model(taste)
  profit = (demand.price - unit_cost) * demand.quantity
END

changed = INTERVENE(store, {"demand.price": 18}, {taste: 2, unit_cost: 8})

The replacement cuts only demand.price's incoming edges. Its descendants, including demand.quantity and profit, evaluate from the replaced value. Component instances retain their own names and imported helper scope. Record mechanisms are replaced as whole mechanisms; a record field is not an equation merely because it has a dotted access path.

Paired batches

BATCH compares scenarios against the same identified exogenous rows. It does not draw randomness or infer a Cartesian product:

draws = {sample_id: VECTOR("low", "middle", "high"), taste: VECTOR(-2, 0, 2), unit_cost: VECTOR(8, 8, 8)}
comparison = store.BATCH(draws, {
  id: "sample_id",
  inputs: {taste: "taste", unit_cost: "unit_cost"},
  scenarios: {baseline: {}, fixed_price: {"demand.price": 18}},
  observe: VECTOR("demand.quantity", "profit"),
  on_error: :error
})

Column lengths must agree and IDs must be unique bounded text or exact integers, with type-sensitive equality. Columns retain their declared row sequence. A complete inline Frame carries its own immutable columns; external Frame handles are admitted through the selected model Frame provider. Declared stable order is preserved, or canonical ID order is chosen once. Cross-model external Frame references are refused. The input mapping names every exogenous port; scenarios name fixed hard replacements and observations name known mechanisms, including qualified component mechanisms.

Only the observation dependency cone runs. Every requested scenario uses the same row; an empty replacement record selects the baseline explicitly. on_error: :error returns ERR on any world failure. :collect keeps a failed world at its exact sample/scenario position. Request errors, resource limits and cancellation still fail the whole operation. Empty input is a complete empty result, not a zero estimated effect.

The grid.causal.batch.v1 result retains definition/input identity, scenario contracts, counts and an immutable results Frame with id, scenario and outcome columns. Each outcome remains a native Result, including complete failure information. Empty results retain the same declared columns. Constructing the Frame is pure; ordinary Frame queries use the existing query owner, and explicit Host studies publish through the existing Frame owner. The CAUSAL implementation record distinguishes functional verification from the separately deferred performance measurements. Use sample IDs when pairing successful outcomes and report excluded pairs; never silently compare averages from different successful subsets.

Reusable scenario source library

Validation pending: these new source recipes pass parser/import admission, but their added end-to-end LIR tests have not run successfully. The shared native cache changed during validation and no coherent Host dependency set remains identified for this check. Treat this section as a proposed recipe until the acceptance receipt records a passing run.

market_scenarios.gs packages an illustrative demand component, a store model and paired comparison functions. Supply the file to the importing model's source resolver:

USE "shared/market_scenarios.gs" AS market
draws = {sample_id: VECTOR("low", "middle", "high"), taste: VECTOR(-2, 0, 2), unit_cost: VECTOR(8, 8, 8)}
comparison = market.compare(draws, 18)

The complete example also compares a direct intervention against the same exogenous row. The helper returns the existing Result-wrapped BATCH report. It preserves sample IDs and row order; baseline and fixed-price scenarios share each row. Profits for the three baseline worlds are 580, 720 and 868; fixed-price profits are 580, 640 and 700. These are consequences of the authored equations, not causal estimates identified from data. The equations are illustrative and do not impose demand floors, currency units or business validity bounds.

compare fails on a world error. compare_collect keeps world failures in place and reports paired/excluded counts; malformed requests and duplicate IDs still fail the batch. Neither helper samples new inputs or publishes a retained job. Retained CAUSAL studies and external Frame admission remain separate from this pure source-library recipe. This module is a supplied .gs asset, not a compiler-embedded or Registry-published library.

Retained studies and replay

An EXPERIMENT may select CAUSAL store USING request and declare RESULTS FRAME name. Reading that study definition starts no job. Explicit study admission freezes the callable, scenario order, observation paths and actual exogenous rows before dispatch through the existing experiment owner. A supplied Frame is captured at admission, so later Frame deletion or source changes cannot change replay inputs.

Supplied rows require provenance. Sampled requests specify the native source distributions, seed, row count and dependence: either explicit independence or an admitted joint-normal correlation matrix. The retained artifact contains the realized draws, not just a seed. Replay uses those values without sampling again and checks the callable identity. Portable verification retains the same input/output evidence; changed definitions cannot be presented as an identical replay.

See the composed-store example for a complete definition, paired rows and explicit study declaration. These are consequences of the supplied structural assumptions. Sampling and a results Frame do not establish causal identification, population representativeness or valid confidence intervals. Soft interventions, abduction and causal discovery are outside this profile.