Forecast and run what-ifs
Goal: simulate a world forward, ask "what if?", and put honest uncertainty bands around the forecast.
## Two intervention semantics — read this first
>
- CLI
--intervene NAME=VALUE@TIME(forecast) and--scenario NAME:k=v@t(scenarios) schedule a change to a parameter or non-state input at timet. Template worlds (lawsynth new) carry named parameters; discovered worlds do not (coefficients are inlined constants), so--interveneon a discovered world errors with *"intervention target is neither a parameter nor a non-state input."* On a discovered world, change the starting point with--initialinstead. - SDKStudy.forecast(...)/add_scenario(...)override initial conditions on state variables — same dynamics, different starting point.
CLI: point forecast
# forecast a world 40 time units ahead from a chosen starting state
lawsynth discover prey.csv --time time --state x,y --output prey.lsworld
lawsynth forecast prey.lsworld \
--horizon 40 --step 0.05 \
--initial x=10 --initial y=5 \
--output forecast.csv
Defaults if omitted: --horizon 20, --start 0, --step 0.1, and each state starts at 1.0. Expected shape (with --output, only the summary prints):
wrote forecast: forecast.csv (<rows> rows)
forecast horizon t in [0, 40], <rows> samples
final state:
x 10 -> <float>
y 5 -> <float>
Without --output, the trajectory CSV is printed first, then the summary.
CLI: scheduled what-ifs (--intervene / scenarios)
These need named parameters, so use a template world:
lawsynth new lotka-volterra --output lv.lsworld
# one forecast with a scheduled parameter change: raise beta to 0.6 at t=10
lawsynth forecast lv.lsworld --horizon 40 --step 0.05 \
--initial x=10 --initial y=5 \
--intervene beta=0.6@10 \
--output whatif.csv
# compare several named scenarios against an implicit baseline
lawsynth scenarios lv.lsworld --horizon 40 --step 0.05 \
--initial x=10 --initial y=5 \
--scenario hunt:beta=0.6@10 \
--scenario recover:gamma=0.2@10,delta=0.05@20 \
--html scenarios.html
scenarios prints a comparison table — final state per variable and its signed divergence from baseline — and, with --html, overlays every scenario on one chart per state:
Scenarios over t in [0, 40], step 0.05 (<n> samples); world lv.lsworld
2 scenario(s) + baseline, 2 state variable(s)
scenario interventions x dx y dy
baseline (baseline) <..> - <..> -
hunt beta=0.6@10 <..> +<..> <..> -<..>
recover gamma=0.2@10, ... <..> -<..> <..> +<..>
wrote scenario report: scenarios.html (<bytes> bytes)
CLI: confidence bands (--confidence)
Bands are estimated by bootstrapping the model's residuals on observed data, so they need --data — without observations there is nothing to bound:
lawsynth forecast prey.lsworld \
--horizon 40 --step 0.05 --initial x=10 --initial y=5 \
--confidence --data prey.csv --time time \
--level 0.9 --replicates 512 --seed 7 \
--output bands.csv --html bands.html
Defaults: --level 0.95, --replicates 512, a fixed --seed. Expected shape:
wrote forecast bands: bands.csv (<rows> rows)
confidence forecast t in [0, 40], <rows> samples, 90% band from residual bootstrap (512 replicates, seed 0x...)
state residuals offset_lower offset_upper offset_se
x <n> <e> <e> <e>
y <n> ...
wrote band report: bands.html (<bytes> bytes)
The band CSV columns are time, x_lower, x_median, x_upper, y_lower, …. The whole estimate is deterministic for a fixed seed.
Python SDK: what-ifs are initial-condition overrides
import lawsynth
study = lawsynth.Study.from_csv("prey.csv", time="time", state=["x", "y"])
result = study.discover()
# forecast: start y from a different initial value, compare to baseline
fc = result.forecast({"y": 8.0}, horizon=40, step=0.05) # -> Forecast
print(fc.divergence) # {'x': |Δx_final|, 'y': |Δy_final|}
print(fc.interventions) # {'y': 8.0} (the initial-condition override)
# fc.baseline and fc.counterfactual are TrajectoryData
Only state variables may be intervened on; naming anything else raises a ValidationError listing the valid states.
Compare many scenarios (scenario board)
comparison = (
study
.add_scenario("more_prey", interventions={"x": 20.0})
.add_scenario("fewer_pred", interventions={"y": 2.0})
.compare_scenarios(horizon=40, step=0.05)
)
print(comparison.table()) # baseline + each scenario, final state + divergence
comparison.distance("more_prey") # Euclidean norm of final-state divergence
add_scenario returns self, so scenarios chain fluently; the baseline (no-intervention) run is always implicit. In a notebook, comparison renders as an overlaid multi-series chart per state plus a divergence table.
For ensemble forecast bands (uncertainty from model structure, not just residuals), see recipe 5.
See also
- How much can I trust it?
- Use LawSynth as a service for the
Client.forecastendpoint.