Maintainer Playbook¶
Use this page before you make or review a change. It turns the Repository map and the Design decisions into checklists.
Every change¶
- Read the user-facing page of the feature in the User Guide.
- Find the owner in the Repository map.
- Read the tests next to that code before you change it.
- Make the change in the narrowest owner.
- Run both test suites (Quick commands).
- Update the documentation as the mandatory definition of done requires.
- Run
python .github/scripts/affected.py --closes <number>and update each open issue it lists whose current state, paths or assumptions the change alters: its body, and a comment that says what changed (Planning, step 4).
Guardrails¶
- Do not duplicate an engine rule in Python. Add a binding instead, and
call it from
context/_model/engine_state.py. - Do not identify compartments by name. Use labels (see Compartments are labels).
- Do not drop invalid input silently. Reject it with a message that names the transition, compartment or parameter.
- Do not change saved formats by accident. Model JSON and report key order are part of the contract.
- Do not trust a closed proposal as specification. A closed issue records past work; the code and this guide describe the present.
Checklists by type of change¶
Process (bins, stratifications, accumulators) and the aggregate structure¶
- [ ] Change the Rust type in
crates/commol-core/src/types/(process.rs,aggregate.rs) and the Pydantic model inpy-commol/commol/context/(process.py,aggregate.py) together. A check of the process by itself goes inProcess; a check against the structure goes inAggregate.check_model. - [ ] Update the PyO3 binding and
_commol_rs.pyiif the type crosses the boundary. - [ ] Update expansion in
builder.rs(expand) if compartments change. - [ ] Check that
test_expansion_matches_the_engine_output_namesstill passes. - [ ] Update Building models and Model structure.
Transitions and rates¶
- [ ] Plan the flows in
commol-model/src/flows.rs; reject what cannot be simulated. - [ ] Check the rate semantics.
- [ ] Add a case to
tests/transitions/test_flows.py, and check thedynamicssection ofModel.describe(tests/description/). - [ ] Update Building models.
Expressions (functions, variables)¶
- [ ] Change the language in
crates/commol-core/src/math_expression/: a function is one entry ofFUNCTIONS(language.rs), which the check, the JIT and Python (commol_rs.core.FUNCTIONS) read; an operator is a token and a level of the parser (jit/parser.rs), a case ofcheck(language.rs) and of the JIT (jit/codegen.rs). - [ ] Keep every walk of a tree (
Expr) recursive only into the operands of a node: a chain of one level is one node, andMAX_NESTINGbounds the depth. A new walk at the limit runs incrates/commol-core/tests/expressions.rs::every_level_of_operators_at_the_limit_reads_on_a_worker_thread. - [ ] Give a new function its value in
crates/commol-core/tests/expressions.rs::every_function_of_the_guide_has_its_valueand its unit rule in_FUNCTION_UNITS(utils/equations.py). - [ ] Update Mathematical expressions.
Initial conditions¶
- [ ] Change the rule of an amount or a fraction in
rules.rs(commol-model), whichcontext/initial_conditions.pyandinitial_statecall. - [ ] Check that
Aggregate.initial_state(process)reports the new state. - [ ] If calibration can set it, update
commol-model/src/initial_conditions.rs(InitialConditionPlan) and the write-back (context/_model/calibration_write_back.py). - [ ] Check that
test_applied_model_reproduces_the_loss_the_optimizer_scoredstill passes.
Calibration¶
- [ ] Put the change in its layer of
crates/commol-calibration/: running the engine inforward.rs, reading observations inobservation.rs, bounds and constraints infeasibility.rs, the loss indiscrepancy.rs, penalties only inobjective.rs. Use onlySimulationEngine: a rule of one model kind goes behindplan_values,apply_valuesoroutput_sumsof its engines. - [ ] Convert new options once, in
api/calibrator.py. - [ ] Check that
tests/simulation/test_windows.py::TestAgreementWithTheCalibrationLossstill passes if the loss changes. - [ ] Update Calibration.
Uncertainty method¶
A method is three parts, and plugs into Calibrator.uncertainty without
changing it (Design decisions):
- [ ] The algorithm in
crates/commol-inference/. It runs the engine only throughForwardModel, reads observations only throughObservationOperator, sees aViolationas outside the support (discrepancy_in_support), and draws every random number with the samplers ofcommol-random, from acommol_random::Generatorseeded from a stream of the problem's seed: a newStreamofcrates/commol-random/src/streams.rs, with a number of its own, andStream::replicatefor each draw or replicate; never the seed itself. It takes its numerical helpers and log densities fromcommol_statistics::numericand its quantiles and weighted statistics fromcommol_statistics::statistics, and writes no copy of them. - [ ] A method that samples a posterior is a
TemperedTargetonSmcSampler(crates/commol-inference/src/smc/), not a sampler of its own, and its configuration takes requiredpriors(WithPriors) (Design decisions). - [ ] The runner, bound in
py-commol/src/(inference.rsor the module of its family), which runs the method throughparallel(py-commol/src/parallelism.rs), and its stub in_commol_rs.pyi(tests/suite/test_engine_rules.py::test_the_stub_declares_what_the_extension_has); a row in the table ofdocs/guide/parallelism.mdand a case ofMETHODSintests/parallelism/test_threads.py. - [ ] The configuration, a subclass of
UncertaintyMethod[YourResult], and the result, a subclass ofResultModel, incontext/inference/; a result with draws holds aParameterSample. - [ ] A method that fits several times fits its perturbations with
ReplicateRunner::fit_alland checks them withReplicateFits::require; its result hasfailed(the entries ofFitUnit::failed, logged as warnings) andnot_convergedat the top (Design decisions). - [ ] Every public function of the method returns
MethodError, of the kindInvalidfor a setting, an input, the data or the model that does not fit the method, andFailedfor a computation that fails; the binding maps it withmethod_error, and the "Raises" section of its docstring listsValueErrorandRuntimeErrorby that rule (Design decisions). - [ ] The Python runner in the module of its family under
api/inference/, which callsregister(YourMethod, runner)(api/inference/registry.py);api/inference/__init__.pyimports every such module. - [ ] A coverage study (
tests/support/coverage.py, markercoverage_study) that shows its intervals cover at their level. - [ ] A guide page under
docs/guide/uncertainty/, a row in the table "Which method answers which question" of its index, and an API entry.
An identifiability method follows the same steps with
IdentifiabilityMethod[IdentifiabilityReport] and Calibrator.identifiability;
it reads derivatives only through SensitivitySource, adds its section to
IdentifiabilityReport, and needs no coverage study.
A sensitivity method subclasses SensitivityMethod[YourResult] and runs
through SensitivityAnalysis.run. Its Rust side is a method of
ForwardAnalysis (crates/commol-inference/src/global_sensitivity/), which
builds the design from InputSpace (design.rs), runs it with
BatchRunner and calls the estimator; the binding and the Python runner only
convert. A method of independent inputs refuses a sample through
InputSpace::independent, and a random number comes from a stream of the
seed of the analysis (Stream, crates/commol-random/src/streams.rs).
Model type (engine)¶
To add an engine to the list:
- [ ] Add the entry to
EngineTypeincrates/commol-core/src/types/engine.rsand toALL. The compiler then names everymatchto complete in that file: its name, the model kinds it runs (kinds), whether it is stochastic (is_stochastic), its settings (EngineSettings, parsed and validated withdeny_unknown_fields), the form of its dynamics (dynamics_form) and the meaning of a rate, which sets the positions it reports at (check_position; see One list of engines). - [ ] Write the engine in its own crate and implement
SimulationEngine(for an engine of the aggregate kind, alsoCompartmentalEnginefromcommol-model: it gives its compiled model,delegate_to_compiled_model!writes the methods that read it, andrecord_stepsruns an engine that reports only at integer steps);check_positionis the rule ofEngineType,engine_typereturns its entry (sois_stochasticand the messages of a calibration read it), andtry_from_modelrejects a model of another engine. A stochastic engine draws every random number from onecommol_random::Generator, implementsset_seed, and restores the generator inreset, so a run repeats from its seed. - [ ] Add a variant to
Engineinpy-commol/src/engine.rs: one arm inbuildper kind it runs (the pair of the engine and the kind),engine_type,dispatch!,plan_valuesandapply_values(andcompartmental!for an engine of the aggregate kind). Add the crate topy-commol/Cargo.toml. - [ ] Add the name to the
EngineNameLiteralincontext/engine.py; if the engine has settings, add their Pydantic class toEngineSettings. - [ ] Run the tests of
crates/commol-core/src/types/engine.rs,py-commol/src/engine.rsandtests/building/test_engine.py; they run every entry of the list. - [ ] Say what a rate means for it in the guides, and add it to Model structure and Running simulations.
- [ ] Add the engine to Inside Rust: a box in the diagram and a row in the table.
Model kind¶
To add a kind of model, such as an agent network (see One interface for every model kind):
- [ ] Add the kind to
ModelKindincrates/commol-core/src/types/engine.rsand to itsALL: its name and the types of value calibration may set (value_types). - [ ] Add the structure of the kind to
crates/commol-core/src/types/and a variant toModelStructure(structure.rs). The compiler names everymatchto complete, such asModel::aggregate. The structure reads the process of the model; it does not hold bins or transitions. - [ ] Add its engines to the list, as in
Model type (engine), or add the new kind to the
kindsof an engine that runs it. Each engine implementsSimulationEngine:plan_valuesandapply_valuesresolve and write the values of the kind, andstate_outputsandoutput_sumsname what an expression reads. - [ ] Add a variant to
EngineValuePlaninpy-commol/src/engine.rs. - [ ] Write a
ModelStructuresubclass inpy-commol/commol/context/, withkindas aLiteraldefault. Implementcheck_modelandoutputs_by_source; overrideseries_label,uncalibrated_values,check_calibration_values,with_calibrated_values,report_sections,describeandcheck_unit_consistencywhen the kind has them. Reject incheck_modela feature of the process the kind cannot run, and name the form that carries over. Import the module incontext/__init__.py, soModel.from_jsonreads the kind. - [ ] Do not write a builder:
ModelBuilder.build(structure, engine)builds every kind. Do not changeSimulation,Calibrator, the reports or the figures: if one of them needs something of the kind, add a method toModelStructurewith a default. - [ ] For a variant of a structure, subclass it with
variantas aLiteraldefault, and add the variant and its checks to Rust (AggregateVariantforAggregate). - [ ] Give the description of the kind: override
ModelStructure.describewith the sections it fills (entities,dynamics,initialization,design_concepts), built from the blocks ofcontext/description.py; the renderers need no change. - [ ] Copy the tests of
tests/model_kinds/for the new kind, and run the Rust and Python suites. - [ ] Add the kind to Model structure, the Mental model and a guide page.
- [ ] Update Architecture if the kind adds a crate or changes the path of a simulation or a calibration.
PyO3 bindings¶
- [ ] Keep logic out of
py-commol/src/: convert types and call the crate. - [ ] Take a count as
i64(a seed asi128) and read it with its rule,crate::counts::count(&RULE, value)(commol_core::SEEDfor a seed), so a negative one raises the message of the rule; add the case totests/uncertainty/test_counts.py. - [ ] Update
_commol_rs.pyi. - [ ] Rebuild the extension with the test model kind
(Quick commands) before you run
pytest.
Reports¶
- [ ] Make a report a method of the object of results whose data it
describes; its block is a private function of
api/reporting.py. - [ ] Keep the key order of existing entries.
- [ ] Put a section that only one model kind has in its
ModelStructure.report_sections, not inapi/reporting.py. - [ ] Use
member_statistics(with the weights of aPredictive) for values across members, andParameterSample.summaryfor parameters. - [ ] Update
tests/reporting/.
Dependencies¶
- [ ] Let Dependabot propose the update; it groups updates monthly.
- [ ] For a major update that needs code changes, open your own pull request and close the Dependabot one with a link to it.
- [ ] The argmin dependency is a fork (
MUNQU/argmin, branchdevelopment): change the fork first, then updateCargo.lock. The fork keeps the numerical methods that know no model: the solvers, the sine transform of box bounds (SineTransform,BoundedProblem), the root finder (NewtonRoot) and the finite differences (finitediff). commol keeps what knows the model: the bounds, the start points, the bracket of a quantile.
Releases¶
- [ ] Follow Release process.
Review checklist¶
- [ ] The change lives in the owner that the Repository map names.
- [ ] No engine rule is re-implemented in Python.
- [ ] The change meets the documentation definition of done: the affected docs are updated, in the agreed format, and the doc checks pass.
- [ ] Every new claim in the docs names a file, function or test.
- [ ] The diagrams of Architecture still describe the code.
- [ ] The tests that guard the touched design decisions still pass.
- [ ] Each open issue that
.github/scripts/affected.pylists is updated, or does not depend on what changed.
Navigation¶
Previous: Coverage and mutation testing | Back to: Codebase Guide