History-aware planning qualification

The new ammm.scenario_planner.planning.PlanningEvaluator provides a bounded, opt-in numerical foundation for the GriffinLab planner. It does not replace the existing scenario-v1 service or production optimizer.

Supported contract

The initial profile is static weekly time series, additive Normal likelihood, geometric alpha adstock with causal After convolution, and logistic saturation. Both transform orders are tested. Panel estimators, time-varying media, other transforms and other likelihoods fail explicitly before compilation.

Construct an evaluator with a retained PanelMMM, aligned future start date and number of spending weeks. If the start follows a gap, supply every intervening week as a date/channel DataArray and label its provenance observed or assumed. The evaluator uses fitted spending plus this bridge to establish the required lag history. It never silently substitutes zero history.

evaluate(spend) accepts a labelled weekly matrix in the model’s input units and returns original-target-scale incremental and history_only arrays with chain, draw, date and channel coordinates. Incremental response is the planned response minus zero-future-spend response under the same history and posterior. All retained draws are used. No observation or spending noise is added. The first spending_weeks dates define the primary outcome; the remaining l_max - 1 dates contain the separately reported carry-out with zero new spend.

objective_and_gradient(spend) returns the negative expected primary outcome and its date/channel gradient. It uses the same graph and history reference as the reported draws. Channel-total optimization with fixed weights applies the chain rule to this gradient; it must not reinterpret a horizon total as weekly spend.

A context fingerprint binds posterior values/identities, transforms, scales, reporting dates and history assumptions. planning_summary.paired_difference rejects mismatched contexts and aligns coordinates explicitly. Contribution and comparison summaries use linear 94% equal-tailed intervals. The probability of beating baseline counts paired differences strictly greater than zero, with the retained draw count; samples are not asserted independent. Revenue ROAS retains negative outcomes. CPA is spend / expected conversions, and its draw-level interval is unavailable if any conversion draw is nonpositive. Zero spend makes efficiency unavailable.

Executable currency budgets

ammm.scenario_planner.rounding.round_allocation takes continuous channel totals, an integer minor-unit target and bounds, channel IDs, integer schedule weights, and locked channels. It applies bounded largest remainders to channel totals, then exact rational largest remainders across weeks. Channel IDs break channel ties; ascending week breaks weekly ties. Locks are preserved exactly and zero-weight weeks stay zero. A funded channel needs an explicit nonzero profile.

All monetary arguments to rounding are in minor units, not model units. Convert using an explicitly confirmed spend-unit scale before evaluation. Keep both continuous and executable matrices and their differences. Reevaluate the executable matrix; if it underperforms a feasible baseline, do not recommend it merely because the continuous solver converged.

Reproduce the retained-model qualification

From the repository root, using a trusted saved time-series model:

OPENBLAS_NUM_THREADS=1 OMP_NUM_THREADS=1 \
.venv/bin/python scripts/qualify_planning_contract.py \
  --model results/timeseries_20260916_181416/20_model_fit/model.nc \
  --output .planning/planning-contract/new-run

The example file is local evidence, not a packaged fixture. Output directories must be new. The runner retains JSON inputs/results, source/code hashes and NetCDF executable draws, verifies serialization round trips, and checks source model bytes remain unchanged. It records failures without relabelling them as successful evidence. It uses all posterior draws, 13 weeks, explicit bridge history, asymmetric schedules, an initially unfunded channel and a locked row. An independent NumPy finite convolution plus the logistic/tanh identity checks the compiled response. SLSQP uses the new objective; its rounded allocation is checked against the feasible baseline and reevaluated.

The test suite additionally covers zero/pulse spend, both transform orders, coordinate permutations, currency and target-unit scaling, missing/duplicate bridge dates, a known symmetric concave optimum, skewed uncertainty, nonpositive CPA draws, cent residuals and exact locked schedules.

Qualification boundary

These checks establish numerical agreement for the declared profile and fixture. They do not establish causal identification, fit approval or a globally optimal allocation. Source models are not refitted. Production remains unchanged. The versioned development adapter below now connects this evaluator to GriffinLab; release and UI qualification remain separate gates.

The implementation rejects more than 104 spending weeks, bridges longer than 104 weeks, or more than 10 million draw/time/channel response cells. These are computational guards. The worker contract below narrows the maximum spending horizon to 52 weeks and retained draws to 20,000. Capacity measurements cover specific workload shapes, not every permitted model or deployed concurrency.

Version 2 development worker API

AMMM 3.3.3.dev0 provides ammm.scenario_planner.planning_contract.PlanningRequestV2 and ammm.scenario_planner.planning_job.evaluate_planning_job(model, request). This development version is not a published release. GriffinLab retains a schema mirror without importing statistical dependencies into its server; its opt-in integration test compares the installed wheel and server JSON schemas.

The request carries labelled baseline/proposal weekly matrices, currency minor units, explicit model-spend and target-unit conversions, exact budget bounds, locks, and observed/assumed bridge spending. The job evaluates all retained draws, uses four deterministic feasible SLSQP starts for optional channel-total optimisation, preserves the proposed weekly proportions, and reevaluates the exact rounded allocation. It rejects failed solvers, invalid budgets, objective mismatches and allocations worse than a feasible baseline. A previously unfunded channel needs an explicit nonzero weekly profile. Fully locked allocations bypass the solver and still undergo numerical and executable checks.

Results contain named draw datasets and bounded JSON summaries. Unit declarations participate in comparison identity. Returned history arrays own their memory, so converting result units cannot mutate the evaluator’s history reference. The job does not fit models, authorize users or approve model evidence; GriffinLab owns those application boundaries.

tests/mmm/test_planning_job.py covers the worker contract, paired comparisons, integer conservation, locks, zero-spend profiles, fully locked plans and target unit conversion. scripts/qualify_planning_workload.py measures synthetic retained parameter draws without sampling. For example:

python scripts/qualify_planning_workload.py \
  --channels 30 --weeks 10 --draws 20000 \
  --output /tmp/planning-capacity-new.json

Use a fresh output path. To qualify a resource envelope, run the script inside the matching wheel image under explicit CPU/memory limits and retain container exit and OOM status. An unconstrained local invocation is not container qualification. The GriffinLab integration also exercises a real saved 12,000-draw model through submission, worker execution and publication, using synthetic test-only approval reports. No MCMC fit, causal validation or production deployment is implied.