Changelog¶
All notable changes to Commol will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]¶
[0.10.2] - 2026-10-24¶
This release makes the engine the one source of every rule: the range of
each setting, the values of a model, the windows, the checks of a model and
a calibration, and the language of formulas, which commol now reads with a
parser of its own. A value that was valid stays valid, and an invalid one
still raises ValueError when its object is built, but the messages are
the engine's. Read the changes before you upgrade: seeded results change
value, as every draw comes from one seed scheme in which neighbouring seeds
share no replicate; a chain of powers groups from the right, so
2 ** 3 ** 2 is 512; BootstrapCoverageEta gives a slightly different η;
a stratification fraction outside 0 to 1 is rejected; and two results are
equal when their values, a NaN included, and their positions, seed and
model are equal, so a run is no longer equal to a plain dict. See
Building models and
Uncertainty.
Changed¶
- The engine holds the range rule of every setting: a count, a share, a
length, a scale, a tolerance, a bound of an optimizer, an observation, an
observation model, an axis pattern, a distribution, a quantity of interest
and the setting of an uncertainty, identifiability or sensitivity method.
A value outside its range still raises
ValueErrorwhen the object is built, and the same values are valid; the message is the engine's, and names the setting and its range:replicates is a whole number of at least 1, got 0,sd is a finite number greater than 0, got 0. A count gives one message for every value outside its range, a negative one included, also when a binding is called with no Python object in between, and forSimulation.replicates(count is a whole number of at least 1, got 0) andCalibrator.run(starts is a whole number of at least 1, got 0). A seed outside its range givesseed is a whole number from 0 to 18446744073709551615, got -1in every function that takes one:Simulation.run,Simulation.run_at,Simulation.replicates,CalibrationProblem,SensitivityAnalysisand each binding. - The engine holds the rule of every value of a model and of every window.
An initial amount, an impulse and the points of a series fail when their
object (
Aggregate,Transition,SeriesValue) is built or an amount is updated, as before, and the same values are valid; the message is the one the model compiler gives, which names the bin, the compartment or the transition:Bin 'B': initial amount is a finite number of at least 0, got -1,Transition 't': impulse period is a finite number greater than 0, got 0,a series has one point per position, got two at 2. The windows ofwindowed_totals, of reports, of figures and ofAxisUnits.window_start,window_end,window_indexandwindow_endscome from the engine, with its messages (window is a finite number greater than 0, got 0,index is a whole number of at least 0, got -1), and an observation or a quantity whose window starts before 0 givesthe window 4 that ends at 3 starts before 0; an end is at least its window. Atick_everyof a figure that is not a finite number greater than 0 raisesValueError. - A calibration, a sensitivity analysis or any other method that sets a
model parameter raises
ValueErrorwhen it is built if the parameter is a formula or a series parameter. The engine computes such a parameter at each step, so a value set on it was lost and the method fitted nothing. The message is the engine's, for this case and for a parameter the model does not have:Parameter 'k9' is not in the model; the parameters that can be set are k1, k2. Python keeps no copy of this check. - The four engines of the aggregate kind name themselves in their messages
by the name of the engine: a position an engine does not report at gives
The DifferenceEquations engine reports only at integer steps; 2.5 is not one, and an impulse or a series point between two steps namesthe DifferenceEquations engineorthe StochasticSteps engine. Predictive.at_observationsand every method that reads a predictive at other observations raiseValueErrorfor a position the engine rejects, with the message of the engine, as a calibration does. The bands, the automatic block lengths and their estimates keep their values: one Rust function of each computes them for every method.- The engine decides the practical verdicts of
CollinearityandSampleIdentifiabilityand the two notes of a profile on its refits, and results read a position with the rule of an observation: the verdicts, reasons, notes and values stay.windowed_totalsreads each window as a calibration reads a windowed observation, and it gives floats. A figure asked for a series that is neither an output nor a bin or accumulator raisesKeyErrorwith the message ofSimulation.outputs_for, and a sum of outputs that the results do not hold gives one message,Missing output series in results, in a simulation, a report and a figure. - The engine checks a model, a calibration and the engine settings, and
Python repeats none of its checks, so each mistake gets the message of the
engine. A parameter named after an engine variable, a bin, a compartment
or a subgroup total names each conflict and asks to rename the parameter.
An observation of an output the model does not have names the output, the
observation and the outputs:
Invalid observed output name 'X' for observation 'series1' (available: A, B, C). A factor with a bound of 0 or below fails withFactor parameter 'f' must have positive bounds (got min=0, max=2).Collinearitychecksmax_subset_sizewhen it runs, withmax_subset_size must be from 2 to the number of parameters (3), got 9. The needs of a stochastic engine (replicates,seedandreplicate_summaryof a calibration;replicatesandseedof a sensitivity analysis) keep their messages, which the engine now gives. ODESolverandStochasticEventsSettingstake their defaults from the engine and check themselves with the engine when they are built:StochasticEventsSettings(epsilon=1.5)givesepsilon is strictly between 0 and 1, got 1.5. Every other setting strictly between 0 and 1 (target_cess,stay_probability,min_acceptance, thequantileofABCRejection,correlation_threshold) gives<name> is strictly between 0 and 1, got <value>,Quantile(1.5)givesA quantile level is between 0 and 1, got 1.5, and a Sobol analysis of the Rust API with fewer than 2 draws saysdraws, the name of the setting.- A negative count that reaches the engine, such as
Simulation.run(-1), thereplicatesof aSensitivityAnalysisor themax_subset_sizeofCollinearity, raisesValueErrorthat names the setting,num_steps is a whole number from 0, got -1, in place of anOverflowErrorwith no name. Every count of the bindings reads this way. - The engine draws a seed that its user does not set, in one place, and the
result gives the seed it ran with, as before:
results.seed,SimulationReplicates.seed,Calibrator.seedandSensitivityAnalysis.seed. A calibrator with no seed logs the seed it drew when it is built, not when it first runs. - Ruff checks that every signature of the package has annotations and that
none is
typing.Any; the raw input of a validator is aJsonValue. A keyword thatQuantile,Groups,LevelorIncrementsdoes not take raisesTypeErrorthat names it, such asQuantile(0.5, unknown=1), in place of theValidationErrorExtra inputs are not permitted. AParameterSamplewhosenames,boundsorvaluesare not valid gets the pydantic message of that field before the engine checks the log weights. - A calibration and every fit of a replicate of a bootstrap, the jackknife, a profile threshold or a conformal method run their starts with one rule: the engine chooses the best start, the first of lowest loss, and the seed gives the Latin hypercube and the seed of each start from two streams of its own. So a calibration with a seed, its structural rank and its fits of replicates give other values than in 0.10.1; a run stays reproducible from its seed on one version. When every start fails, the message lists the reason of each start, also in the message of a replicate that fails.
- A particle swarm draws its numbers with the generator of commol from the
seed of its start, which a calibration always gives: a verbose calibration
with a swarm is the calibration without
verbose, andCalibrator.run_with_historyis start 0 ofCalibrator.run. - The summaries of
PropagationResult, of the scenarios and ofmember_statistics, the effective size of a propagation and the densities of observation models and priors keep their values: one Rust function computes each, and Python calls the summary ascommol_rs.inference.weighted_summary. The error of aLaplacefit on its bound points toProfileLikelihoodand cites no issue. BootstrapCoverageEtatakesηwith the one quantile of commol, the one ofweighted_quantiles, on the logs of the thresholds, soηchanges once. It needs at least1/(2(1 − c))replicates times parameters, where it needed1/(1 − c) − 1.
Fixed¶
- A fraction of a stratification outside 0 to 1 raises
ValueErrorwhen itsAggregateis built, with the message of the engine:Stratification 'g', category 'g1': fraction is a number from 0 to 1, got 1.5. Before, only the sum was checked, so fractions such as{"g1": 1.5, "g2": -0.5}built a model whose compartment started at a negative amount. The sum of the fractions of a stratification, 1 within 1e-6, is checked at the same time (Stratification 'g': the fractions of its categories sum to 1 within 1e-6, got 0.9), where it was checked bybuild, andupdate_stratification_fractionsand the model compiler give the same messages. - Two results of commol with the same values are equal, also when they hold
a NaN: a NaN equals a NaN, whatever the objects that hold it. Before,
==compared the identity of the items of a list first, so two runs with one seed gave results that were not equal when one held a NaN, such as a PRCC coefficient of an input that the other inputs explain entirely. A frozen result has a hash that agrees with==, also when it holds a list, a dict or a NaN.ParameterSamplecompares as every other result. A result no longer compares its private attributes (Uncertainty). ==ofSimulationResultsandSimulationReplicatesnow also compares their positions, seed and model, and a NaN equals a NaN. Before, it read only the values, so two runs at other positions or with other seeds and the same values were equal, and two runs with one seed that held a NaN were not. A plaindictorlistwith the same values is no longer equal to a result: comparedict(results)to read the values only (Running Simulations).-
A profile reads a parameter with one finite bound, such as
[0, +∞), on its bound within1e-8ofmax(1, |bound|), the rule ofLaplace. Before, every value of such a parameter was on its bound, so a profile never saw the ridge leave the box through it, and the structure of a flat profile gaveundetermined="on_bound"with that parameter in place of a reading. -
A window whose start rounds to just below 0, such as a window of
1000 + 1e-7that ends at 1000, starts at 0 in an observation, a calibration and a quantity, aswindowed_totalsreads it. Before, a calibration rejected it while the same window that ends at 2000 was accepted. One rule of the engine gives the start of a window. - Two neighbouring seeds share no replicate: before, replicate
i + 1ofsimulation.replicates(n, seed=42)was replicateiofseed=43, and so were the starts and the swarms of a calibration with neighbouring seeds. Two methods that take one seed no longer draw from one sequence either: every random method draws from a stream of its own of the seed, and with one seed, replicatei + 1of a bootstrap no longer has the generator of particleiof an SMC sampler, nor the draws ofLaplacethe design of a sensitivity analysis. Every method draws with the samplers of the one generator of commol. So with one seed, the draws of the random methods and the replicates of a stochastic engine differ from 0.10.1; a run stays reproducible from its seed on one version. - One check of the engine defines the language of formulas, the one
Mathematical expressions
documents, for rates, formula parameters, calibration constraints and the
unit check. A formula outside it fails when it is added, with a message
that names the formula and its first part outside the language. Before,
k1 * (step > 30)was accepted as a formula parameter, andstep > 1as a rate became a parameter namedstep > 1that failed only when the model ran. A formula that calls a function the guide does not list, such aserf,fmaortrunc, fails as a rate always did; a formula parameter accepted it before. - A rate or a formula parameter that reads a variable the model does not define fails when the model is built. Before, only a transition rate was checked, and a formula parameter failed when the model ran. A category id is not a variable: before, a rate that read one passed the build and failed when the model ran.
- An error of a rate when the model runs names its transition, not the indices of its compartments.
SecurityConfig.max_complexity_scorelimits the number of nodes of the tree of a formula (numbers, variables, operators and calls), as the guide documents. Before, it limited nothing.- The unit check reads
^as a power, as the engine does, andcbrttakes the cube root of a unit. - A formula with a long chain of terms, such as a sum of 20 000 terms, no longer stops the Python process with a segmentation fault. The engine reads formulas with a parser of its own: a sum or a product of any length is one level, and a formula that nests deeper than 100 levels of parentheses, calls, signs and powers fails with a message that names the limit. Before, the parse used one step of the stack for each term, and the process crashed near 20 000 terms, or near 2 000 on a thread of the parallel work.
- A chain of powers groups from the right, as in Python:
2 ** 3 ** 2is -
Before, it was 64,
(2 ** 3) ** 2. -
ParticleSwarmConfig.inertia("constant")and.acceleration("constant")take the weight and the coefficients of canonical particle swarm,1/(2 ln 2)and0.5 + ln 2, which a swarm with no inertia or acceleration strategy uses. Before, their defaults were the rounded0.721and1.193, so a swarm that named the constant strategy moved a little differently from one that did not. - Three messages of a calibration with observation models no longer hold a run of spaces in the middle of a sentence.
[0.10.1] - 2026-10-23¶
This release ships the fixes of the audit at 0.9.0 that change no API. In the
StochasticEvents engine, a parameter or a seed changed between two runs acts
from the position where the first run stopped. The documentation reads as one
account of the library and names no application field, and commol no longer
installs cffi. CI runs with read rights and pinned tools, and the test suite
checks its own layout. Nothing breaks: code that runs on 0.10.0 runs on
0.10.1. See Stochastic engines and the
Quickstart.
Removed¶
cffiis no longer a dependency of commol: no code of commol uses it, sopip install commolno longer installs it. Code that importscffiinstalls it itself.
Fixed¶
-
In the
StochasticEventsengine, a parameter or a seed changed between two runs acts from the position where the first run stopped. The pending wait is drawn again from there, and a pending leap keeps only its events up to there, by binomial thinning. Before, the new rate acted back from the last event, and copies of one state with new seeds shared their next event. In Python this changes onlysimulation.engine.set_seedbetween two calls ofsimulation.engine.step();Simulation.run, replicates and calibration reset the engine after the seed, so their results do not change. -
The documentation and the docstrings name no application field, as Conventions asks: the dispersion of
NegativeBinomialObservationsis "the dispersionk(sizein R)", and bins of different units are "a count and a concentration". A test checks the words of the user pages and of the source ofcommol.
Documentation¶
- The documentation reads as one account of the library: the home page, the
quickstart and the examples show one process run by several engines, then
calibration, uncertainty, the comparison of runs and parallel work, and the
guide follows that order. Each rule lives in one page and the others link
to it; a test checks that every page is in the navigation and that no text
repeats across pages. The API reference names every name of
commol.__all__and the module of each name that the root does not export. The maintainer pages name the paths, symbols and tests of the code, and the design note of per-compartment initial conditions is part of Initial conditions.
[0.10.0] - 2026-10-23¶
This release makes equivalent things the same across commol, after an audit of the code, the repository and the documentation at 0.9.0. A setting that means the same thing has one name, every method takes its seed from its source and fits from its starts, equivalent results hold their failed fits in one place, equivalent methods raise one error type, a replicate summary takes the one quantile of commol, and commol adds no log handler. Before you upgrade, read each Breaking entry: most code changes only the names of settings and the type of an exception. See Uncertainty and Log messages.
Added¶
Laplace,Sandwich,InformationMatchingEtaandBootstrapCoverageEtatakestarts, at least 1 with the default 1: their fit is the best ofstartsstarts,Calibrator.run(starts), as in every bootstrap, jackknife, conformal method and profile. Code that does not pass it runs as before. See Local Approximations and Whatηmeans.
Changed¶
- Breaking: every uncertainty and identifiability method takes its random
numbers from the seed of its source,
Calibrator.seed(problem.seed) orSensitivityAnalysis.seed, and has no seed of its own. Removeseed=fromStructuralRankand fromscores.pit, and setproblem.seed(or the seed of the analysis) to choose the random numbers. The PIT randomizes ties from a stream of the seed of its draws, so its values at tied draws change for the same seed. See Scores. -
LaplaceMixture.startsdefaults to 1. -
Breaking: every uncertainty, identifiability and sensitivity method raises one exception for one kind of error:
ValueErrorwhen a setting, an input, the data or the model does not fit the method, andRuntimeErrorwhen a computation that could start fails (an engine run, an optimizer, every replicate, a sampler that stops before its target). An input error of an SMC method, a bootstrap, the jackknife or a profile that raisedRuntimeError, such as a loss of 0 at the fit or a fit outside the support forBootstrapCoverageEta, now raisesValueError. CatchValueErrorfor these errors where your code caughtRuntimeError. See Design decisions. -
Breaking: a setting that means the same thing has one name in every method. The level of an interval is
confidence_level, a count is a plain plural noun, and the rule that combines the replicates of a ratio isreplicate_rule. An old name raises aValidationErrorthat names it as not permitted. Rename each old name in your code:
| Where | Before | Now |
|---|---|---|
SplitConformal, RollingConformal, JackknifePlus, EnbPI, AdaptiveConformal |
level= |
confidence_level= |
ConformalResult.intervals (PredictionIntervals), and its report block |
.level, "level" |
.confidence_level, "confidence_level" |
Threshold, Threshold.chi_square, Threshold.beale, Threshold.bootstrap_calibrated |
level |
confidence_level |
ProfileSection (IdentifiabilityReport.profile) |
.level |
.confidence_level |
ParticleSwarmConfig |
num_particles= |
particles= |
Sobol |
base_draws=, bootstrap= |
draws=, replicates= |
Ratio |
replicates= |
replicate_rule= |
Simulation.replicates, ReplicateRuns |
n |
count |
See A setting has one name and one check.
-
One check, in Rust, rejects a confidence level that is not strictly between 0 and 1, with one message in every method:
confidence_level is strictly between 0 and 1, got 1.5; use 0.95 for a 95% interval. Every other setting strictly between 0 and 1 (target_cess,stay_probability,min_acceptance, thequantileofABCRejection,correlation_threshold) says<name> is strictly between 0 and 1, got <value>. -
Breaking: a calibration with
replicate_summary=Quantile(q)compares the observations with the one quantile of commol, the one ofweighted_quantiles,SimulationReplicates.quantilesand every interval (NumPy'smethod="hazen"with equal weights), instead of NumPy's defaultlinearquantile.Quantile(0.25)of the replicates 1, 2, 3, 4, 5 is now 1.75, not 2.0. So forqother than 0.5 the loss and the fitted values change slightly;"median"andQuantile(0.5)do not change. Your code does not change; run again a calibration withQuantile(q)whose result you stored. See One confidence-interval convention. -
Breaking: results that hold the same thing hold it in the same place, under the same name. Every result of a method that fits several times lists each fit that failed in
failed,"<Start|Replicate|Block|Origin> <index> failed: <reason>", logged as a warning, and counts the fits kept although their optimizer did not converge innot_converged. Change your code:
| Before | Now |
|---|---|
CalibrationResult.failed_runs |
CalibrationResult.failed |
BootstrapResult.diagnostics.failed, .diagnostics.not_converged |
BootstrapResult.failed, .not_converged |
JackknifeResult.diagnostics.failed, .diagnostics.not_converged |
JackknifeResult.failed, .not_converged; an entry names the block by its index from 0, "Block 2 failed: …", not by its span |
ProfileResult.failed_replicates |
ProfileResult.failed, and the new ProfileResult.not_converged |
The failed refits of RollingConformal, JackknifePlus and EnbPI, only logged as "Refit i failed: …" |
ConformalResult.failed and ConformalResult.not_converged |
The failed replicates of BootstrapCoverageEta in GibbsResult.diagnostics.notes |
GibbsResult.failed |
ProfileResult.report() |
ProfileResult.identifiability(); its report() gives the data |
sample.effective_size() |
sample.effective_size, a property, as on every other object |
When a method has fewer successful fits than it needs, its RuntimeError
says how many of how many fitted, how many it needs, and the first
reasons, and names the unit: "Only 1 of 5 blocks fitted, and the method
needs 2; …". It says "Every origin failed: …" (or replicate, block) only
when none fitted. The jackknife and jackknife+ no longer say that every
replicate failed when one fit succeeded. See
Resampling.
Removed¶
- Breaking:
commol.add_stderr_loggeris removed: commol configures no logging, and adds only aNullHandlerto the loggercommol. Calllogging.basicConfig(level=logging.INFO)instead, or add a handler of your own tologging.getLogger("commol"). See Log messages.
Fixed¶
IdentifiabilityReport.report()gives theprofilesection, the evidence of each profile likelihood; before, the report data had no such section.
[0.9.0] - 2026-10-22¶
This release gives control over the work of a calibration and over what a
result records. commol.threads(n, levels) sets how many threads the parallel
methods use and which levels run in parallel, a particle swarm can stop when
it has converged, a polynomial chaos takes a truncated basis for many inputs,
and Ratio gives a sensitivity analysis the ratio of two quantities. A
calibration result records its seed, and commol.build_info() names the
version and the commit of the package. Before you upgrade, read the
Breaking entry: CalibrationResult requires seed. See
Parallelism and
Sensitivity Analysis.
Added¶
-
PolynomialChaos(q_norm=..., max_interaction=...): a truncated basis for many inputs.max_interactionkeeps the terms of at most that many inputs, andq_norm, in(0, 1], the terms whose degrees haveΣ α_i^q ≤ degree^q; the defaults keep the total-degree basis. 20 inputs at degree 4 withmax_interaction=2have 1,221 terms instead of 10,626, and the number is known before any run.ChaosResultrecords both. See Bases for many inputs. -
ParticleSwarmConfig.improvement_stop(window=..., tol_abs=..., tol_rel=...)andParticleSwarmConfig.radius_stop(tol): a particle swarm stops when its best loss improved by at mosttol_abs + tol_rel·|best loss|over the lastwindowiterations, or when every particle is withintolof the best position, as a fraction of the size of the search box. Both are off by default and have no default values. A run that stops by a rule reportsconverged=True, so a profile that holds a prediction with a swarm also converges. See Stop rules. -
commol.set_threads(n), the context managercommol.threads(n, levels),commol.get_threads()andcommol.get_levels()choose the number of threads of every parallel method and the levels that run in parallel. The setting holds per Python thread (contextvars); without it, commol uses one thread per core. A particle swarm scores the particles of each iteration in parallel, in the order of the particles, withlevels="all", the default;levels="outer"keeps them in order. A seed gives the same result, history included, with any number of threads and either level. See Threads and Parallelism. -
A calibration result records the seed it used,
CalibrationResult.seed(Calibrator.seed:problem.seed, or the seed the calibrator drew), and the sample of every uncertainty method and ofCalibrator.point_samplenames it inmethod.settings["seed"]. Set asproblem.seed, it repeats the run, also from a result read back from JSON. See Reproducibility. -
commol.__version__gives the installed version, andcommol.build_info()gives aBuildInfowith the version and the full commit the extension was built from (Nonewhen the source has no git metadata, such as a source distribution). A build that is not a release, such as an install from a git commit, names its commit as a PEP 440 local version:0.1.0+g<first 7 characters of the commit>. See Check the installation. -
Ratio(numerator, denominator, replicates)is a quantity of interest: oneValueAt,GainorPeakover another, in each run, so PRCC, Morris, Sobol and a polynomial chaos read a ratio whose denominator depends on the inputs.replicatesis required: with a stochastic engine, a point of a design holds the mean of the ratios ("within"), the mean of the ratios of the replicates with a positive denominator ("within_positive") or the ratio of the means ("pooled"). A denominator of 0 makes the ratio of a run NaN, and every method follows the NaN rule. See A ratio of two quantities.
Changed¶
-
The tolerance of a profile under a particle swarm with the improvement stop rule is the improvement the rule allows at the fit,
tol_abs + tol_rel·|objective at the fit|; without that rule it stays1e-6·max(1, |objective at the fit|). See Verdicts. -
The parallel methods run in a thread pool of commol, not in the global pool of
rayon: the environment variableRAYON_NUM_THREADSno longer limits them; usecommol.set_threads. A particle swarm, which scored its particles one after the other, now scores them in parallel by default. -
Breaking:
CalibrationResultrequiresseed; a result saved by an earlier version, or built by hand, needs one to load.
Fixed¶
-
A particle swarm without
problem.seedtakes the seed the calibrator draws and logs, so that seed repeats it; before, it drew one of its own that no result or log kept. -
A profile that holds a prediction with a particle swarm scores its particles in parallel only when the levels of
commol.threadsare"all", and stops with the error of the first failing particle, as every other swarm does; before, it scored them in parallel at any level and reported the error of any failing particle.
[0.8.0] - 2026-10-22¶
This release compares runs. Scenarios runs every draw of a sample under
several sets of parameter values and compares them in pairs, a quantity of
interest reads a series and reduces it (the peak of the gains over windows,
for example), and results.quantities reads the quantities of one run.
Figures are methods of the results they draw: plot.series,
plot.observations and plot_compared, which draws several runs or samples
on the same panels. Before you upgrade, read the Breaking entries:
SimulationPlotter and seaborn are removed and matplotlib is the optional
extra commol[plot], the quantities take a series, and the runs of the
draws of a stochastic sample follow one tree of seeds, so their numbers change.
See Sensitivity Analysis and
Simulations.
Added¶
-
Scenarios(source, sample, scenarios, positions)runs every draw of a sample under each scenario, a set of parameter values that override those of the model, and is a mapping from the name of a scenario to itsPredictive. Every scenario runs with the seed of the source, so runkof each scenario is the same draw with the same seed (common random numbers).compare(name, baseline, quantities, confidence_level)takes the difference of each quantity of interest within each run and reduces the differences with the weights of the runs;summaryreduces one scenario. Both give aScenarioSummaryper label, with no interval when there is one run.Predictive.quantities(quantities)gives the quantities of every draw,Calibrator.point_sample(result)the point estimate of a fit as a sample of one draw, andCalibrator.with_modelandSensitivityAnalysis.with_modelthe same source on another model with the same seed. See Scenarios. -
plot.observations(observed_data, …)ofSimulationResults,SimulationReplicatesandPredictivedraws one panel per observed series (onelabel), with the model read through the observation operator of each observation: the sum of itsoutputs, itswindowand its factor. A series that sums some outputs, such asB_g2 + B_g3, gets its own panel. A value with a window is a step over its window, and the band of replicates or of a predictive (at_observations(…, NoNoise())) a rectangle over it; its legend says that it is a band of the model, not a prediction interval. See Plotting. -
SimulationResults.quantities(quantities)reduces one run to its quantities of interest, asdict[str, float], andSimulationReplicates.quantities(quantities)gives one such dict per replicate, with the Rust code ofPropagation.quantity_schedulegives the positions to run at; a run that lacks one raisesValueErrorthat lists each missing position with its quantity. The guide shows how to take a ratio of two quantities row by row. See The quantities of one run. -
plot_compared({label: result}, …)draws several runs, replicates or predictives of the same model on the same panels: one line per result, labelled with its key and in the same color in every panel, the band of each set of replicates and of each predictive, and the observations once.window=draws the gain of each series over each window. A result without the outputs of the first raisesValueErrorthat names its key. See Plotting. -
Every figure takes
axes=, theFigureof an earlier figure of commol, and draws each series on the panel of that series: each panel has the id of its series as the label of itsAxes. A series with no panel raisesValueErrorthat names it. See Plotting. -
A quantity of interest reads a series and reduces it. The series is a name or a list of names, their
Level, orIncrements(outputs, window): the gain of their sum over each window that ends at a position. SoPeak(series=Increments("acc", window=7), positions=ends)is the largest gain ofaccover a window of 7, andPeakPositionwith the same arguments the end of that window, with no class of their own.commolexportsLevelandIncrements. See Quantities of interest. -
PropagationResult.where(mask)keeps the runs wheremaskis true and renormalises their weights to sum to 1, andPropagationResult.effective_sizegives Kish's effective size of the weights. A filter conditions the result on the condition of the mask. -
plot_compared(…, panels="observations")draws one panel per observed series (onelabelofobserved_data), with each result read through the observation operator of each observation, asplot.observationsreads one result: the steps, markers and band of each result in the color of its key, and the observations once. The default,panels="series", draws one panel per series. See Plotting.
Changed¶
- Breaking: each figure is a method of the results it draws, and
SimulationPlotteris removed. Each figure returns the matplotlibFigure. A predictive gives its own figures, so noSimulationis built only to draw it. See Plotting.
| Before | Now |
|---|---|
SimulationPlotter(simulation, results).plot_series(…), .plot_cumulative(…) |
results.plot.series(…), results.plot.cumulative(…) |
SimulationPlotter(simulation, replicates, confidence_level=c).plot_series(…) |
replicates.plot.series(…, confidence_level=c) |
SimulationPlotter(simulation, run, confidence_level=c).plot_series(calibration_result=predictive, …), and plot_cumulative |
predictive.plot.series(…, confidence_level=c), predictive.plot.cumulative(…) |
SimulationPlotter.plot_prcc(result), plot_morris, plot_sobol, plot_profiles |
result.plot() of PRCCResult, MorrisResult, SobolResult, ChaosResult and ProfileResult |
-
Breaking: matplotlib draws the figures, and is the optional extra
commol[plot](pip install 'commol[plot]'); seaborn is no longer a dependency, andimport commolloads no plotting library. Without matplotlib, a figure raisesImportErrorwith the install line. The other keyword arguments of a figure go toAxes.plot, not toseaborn.lineplot. -
Breaking:
PlotConfig.styletakes a matplotlib style or a list of styles, andPlotConfighas nopaletteand nocontext. The style sheets of matplotlib give the former values:style="whitegrid", context="talk", palette="colorblind"isstyle=["seaborn-v0_8-whitegrid", "seaborn-v0_8-talk", "seaborn-v0_8-colorblind"]. The presets (presentation,publication,notebook) use these sheets. -
Breaking:
ValueAt,Peak,PeakPositionandTrajectorytakeseriesin place ofoutputs:ValueAt(outputs="B", at=10)isValueAt(series="B", at=10).Gain(outputs, start, end)keepsoutputs, and the outputs of a quantity read back as a list. The quantities live incommol.context.inference.quantities, and cross to Rust as the JSON of their classes. -
Every reduction across runs raises
ValueErroron a quantity that is NaN:PropagationResult.summary(),member_statistics,weighted_quantiles, and Morris, Sobol and the polynomial chaos before they estimate, as PRCC did. The message of a method names the quantity, the points where it is NaN, and the point, inputs, replicate and seed of the first such run.PropagationResult.valueskeeps the NaN of each run. -
Breaking: the runs of the draws of a sample follow one tree of seeds. Draw
jof a stochasticPredictive,Propagation,at_observationsor score takes a seed of its own, and its replicateitakes the seed that replicateiofSimulation.replicates(n, seed=…)takes from it. Before, the seed of a replicate depended on the number of replicates. So the numbers of these methods with a stochastic engine change from 0.7.0. The runs of a draw no longer change with the number of replicates or of draws, andPredictive.draw_seedsandPropagationResult.draw_seedsgive the seed of each draw:Simulation.replicates(n, seed=draw_seeds[j])of the model at the values of drawjgives its runs. See The predictive.
Fixed¶
-
A NaN in the values a
Peakor aPeakPositionreads makes it NaN. The peak skipped the NaN, and the position of the peak was the first position when the value there was NaN. -
A weighted quantile of values with a NaN raises
ValueError, where it sorted the NaN by its sign bit and gave a wrong number.
[0.7.0] - 2026-10-22¶
This release reads a run through the classes of commol. The axis of a model
gives the window grid (AxisUnits.window_end and the other methods) and, from
a calendar origin, the date of each position. Simulation.sample,
total_series and windowed_totals read bins, accumulators and output names,
and each report is a method of the object it describes. Before you upgrade,
read the two Breaking entries: the module functions of the window grid
and the report functions are removed, and the table under Changed gives the
method that replaces each one. See Reports and
Simulations.
Added¶
-
commol.sample(series, positions, at)reads a series at given positions, andSimulation.sample(results, ids, at)reads bins and accumulators by id at given positions, next toSimulation.windowed_totals: for example, the amount in a bin at the end of each window. An id stands for all its outputs, summed; an output name stands for itself. -
An axis of time can start at a calendar date:
ModelBuilder(origin=...)takes adateor adatetimewith no time zone, andModel.axis_units()givesposition_of,moment_of,day_endsandweek_ends(weeks that start on a weekday of the caller).SimulationPlotter.plot_serieslabels the ticks of such a model with dates when it has noposition_label. An origin needs a step unit of time of a whole number of days or a whole fraction of a day:monthandyearraiseValueError. -
Simulation.total_seriesandSimulation.windowed_totalstake output names as well as bin and accumulator ids. An output name stands for itself, sosimulation.windowed_totals(results, ["moved_g2", "moved_g3"], 2)sums two strata of the accumulatormoved; a bin or accumulator id still stands for all its outputs. An id that is neither raisesKeyErrorwith the bins, accumulators and outputs of the model. -
The window grid on the axis:
AxisUnits.window_start(index, span, unit),window_end(index, span, unit),window_index(at, span, unit)andwindow_ends(span, unit, end), for a window of a span in a unit, such as one week. The width must be a whole number of steps. -
SimulationResults.modelandSimulationReplicates.model: the results ofSimulation.run,run_atandreplicateshold a reference to theirModel, and the runs ofPredictive.runs()the model of its source. A report reads the outputs of each bin and accumulator from it; results with their model still pickle. See Reports.
Changed¶
-
Breaking: the module functions
window_start,window_end,window_indexandwindow_endsare removed fromcommol. Use the methods ofAxisUnits(Model.axis_units()), with the width as a span and a unit:window_end(2, 168)on an axis of hours isunits.window_end(2, 1, "week"), and a width of 7 steps is(7, "day"), or(7, "step")when the model declares no step unit. -
Breaking: each report is a method of the object whose data it describes, and
commolexports no report function. The private functions ofapi/reporting.pybuild the blocks, with the same keys in the same order. See Reports.
| Before | Now |
|---|---|
simulation_report(simulation, results, at, window, num_windows, …) |
results.report(positions, window, windows, …) |
simulation_report(…, ensemble_runs=replicates, confidence_level=c) |
replicates.report(c, positions, window, windows, …) |
simulation_report(…, ensemble_runs=predictive, confidence_level=c) |
predictive.report(c, positions, window, windows, …), with no run of a point estimate |
applied_parameters_report(model, result, problem, …) |
model.applied_parameters(result, problem, …) |
identifiability_report(report) |
report.report() |
commol.api.reporting.observed_report, series_report, accumulators_report |
The blocks observed, series and accumulators of a report |
[0.6.1] - 2026-10-22¶
This release fixes bugs of 0.6.0 and changes no API. A series point or an
impulse that the engine cannot run now fails at ModelBuilder.build and
Model.update_parameters, not at Simulation. Errors of a Predictive draw
say what was not feasible. A PRCC analysis with a NaN value raises a
ValueError and no longer runs forever. The quantities of a sensitivity
analysis read a gain at its start and give different labels to close
positions. A windowed series is plotted at the position of each observation,
and a figure no longer changes the global style of matplotlib.
Fixed¶
- The default label of a quantity of interest wrote each position with 6
significant digits, so two positions close together gave one label and the
analysis raised
ValueError:Trajectory(outputs="A", positions=[1000000, 1000001])gave"A at 1e+06"twice. The label now writes each position in full ("A at 1000000","A at 1000001"). ModelBuilder.buildandModel.update_parametersaccepted a series point or an impulse position that the engine cannot run, such as a point or animpulse_atof 1.5 underDifferenceEquationsorStochasticSteps, or a"pulse"point at 1.5 under any engine; the error came only atSimulation(model), asRuntimeError. Both now raiseValueErrorwith the message of the engine, and a rejected update leaves the model unchanged.- The
InfeasibleStateErrorof a draw of aPredictivesaid onlyInfeasibleRun. It now gives the message of the run after the index of the parameter set: the compartment, the step and the flows, asSimulation.runreports them. - The
InfeasibleStateErrorof a draw of aPredictivewhose initial amounts leave the remainder below zero said onlyInfeasibleStart { excess: ... }. It now gives the message a calibration gives for the same values after the index of the parameter set: what the amounts exceed, the remainder and by how much. - A
Gain(start, end)quantity read its start atend - (end - start), which can round away fromstart:Gain(start=0.1, end=0.4)read it at0.09999999999999998, so on theDifferentialEquationsengine the gain did not start whereValueAt(0.1)reads. A gain now reads its start at exactlystart. SimulationPlotter.plot_seriesread the model of a windowed series with the first window it found, at the window ends of a grid from 0, so with observations at 7.5 and 17.5 no model value sat on an observation and the windows of the other observations were ignored. When the observations of a series have a window, or differ in outputs or factor, the model line, the band of the replicates and the band of aPredictivenow read each observation through its own operator at its own position, as the loss reads it. Observations with several factors no longer leave the line unscaled or raiseValueErrorwith aPredictive.- A figure of
SimulationPlotterwith aPlotConfigstyle,paletteorcontextchanged the global style of matplotlib, so every figure drawn with matplotlib after it had the style of commol. The style now applies to the figure of that plot only, andmatplotlib.rcParamsis as it was before the call. PRCCnever returned when a quantity was NaN at a draw. It now raisesValueErrorthat gives the index of the quantity and the draws where it is NaN, before it ranks any value.
[0.6.0] - 2026-10-22¶
This release adds the first uncertainty methods with a stated model of the
data: observation models and their likelihood, the Bayesian posterior and the
Gibbs posterior by sequential Monte Carlo, approximate Bayesian computation,
priors, local approximations (Laplace, the sandwich and the Laplace mixture),
conformal intervals, the profile likelihood and sensitivity analysis with no
data. Before you upgrade, read the Breaking entries: an Aggregate states
the initial amount of each bin in the unit of that bin, a
CalibrationProblem needs a loss_function, a Predictive takes a
Calibrator, and public class names write every letter of an acronym in
capitals. See Uncertainty.
Added¶
-
Local approximations through
Calibrator.uncertainty, on the unbounded scale of the parameters, with a fit on a bound refused:Laplace(draws), the normalN(θ̂, H⁻¹)of the observation models, andCalibrator.delta_band(result, positions, level), the band of the outputs by the delta method;Sandwich(block_length, draws), the covarianceH⁻¹ B H⁻¹with the scores summed over blocks (CR1) and a Student t withG − 1degrees of freedom, which needs no noise model and refusesloss_function="mae"and too few blocks;LaplaceMixture(starts, draws, target), a Laplace approximation at each optimum of a multi-start fit as the proposal of Pareto-smoothed importance sampling of aLikelihoodTarget(priors)orGibbsTarget(priors, eta), which reportsk_hatand marks a sample withk_hat > 0.7as unreliable.curvatureis"gauss_newton"(the expected Fisher information of each observation model, by default) or"hessian". See Local Approximations. -
GibbsPosterior: the Gibbs posteriorprior(θ) · exp(−η · loss(θ))of the problem's loss, sampled by sequential Monte Carlo from the priors to the posterior; it returns aGibbsResultwith theηit ran with.etais required: a number above 0,InformationMatchingEta(block_length)(the curvature of the sum of squares or of observation models against the variance of the scores of its terms summed over blocks, with a deterministic engine) orBootstrapCoverageEta(replicates, block_length, confidence_level)(any loss), joined in the unionEta. See Sequential Monte Carlo. ABCSMCandABCRejection: approximate Bayesian computation on a required distance,LossDistance()orSeriesDistance(metric). With a stochastic engine each distance reads one fresh replicate of its particle, so the problem'sreplicatesandreplicate_summarydo not apply, and the result says so. See ABC.SMCResult: the sample, the level of each step, the log evidence, the value each particle keeps and the diagnostics of each step (SMCDiagnostics: effective size, resampling, acceptance rate and move count).- Priors:
Uniform(),LogUniform(),Beta(a, b),Normal(mean, sd),LogNormal(mu, sigma)andGamma(shape, rate), each truncated to the bounds of its parameter, andPriors(a mapping of parameter to distribution, orPriors.uniform_on_bounds()). Every sampling method takespriorswith no default, and a prior whose bounds do not fit raises aValueErrorthat names the distributions that fit them. Their draws are the same on every platform. See Priors. SampleIdentifiability(reference="prior", priors=...): the contraction of each parameter against its prior. See Identifiability.- Conformal prediction intervals through
Calibrator.uncertainty: intervals for observations with a coverage guarantee that needs no noise model, no prior and no correct model.SplitConformalscores held-out observations;RollingConformalgives intervals ahead of the data, one quantile per horizon (thek-th observation after an origin);JackknifePlusrefits once per block left out, with the CV+ bound for blocks of more than one observation;EnbPIdraws its own replicates of blocks for series with dependent errors;AdaptiveConformalwalks a stream of new observations with a working level that moves with each miss. Each takes a requiredscore("absolute","normalized"or"spread"),by("series"or"all") andlevel, and returns aConformalResult(PredictionIntervals, the scores and the guarantee). See Conformal Prediction Intervals. SimulationPlotter.plot_series(prediction_intervals=...)draws the band of conformal intervals, andsimulation_report(..., prediction_intervals=...)reports them.- Observation models:
CalibrationProblem(observation_models={series: model})takes one model per series label,NormalObservations(sd),LogNormalObservations(sigma, center)(center="mean"or"median", no default),StudentTObservations(df, scale),PoissonObservations(),NegativeBinomialObservations(overdispersion)(varianceμ + φμ²) orBinomialObservations(trials)(or the newObservedDataPoint.trialsof each observation), and the problem fits by maximum likelihood. A noise argument is a number or a calibration parameter of the new type"noise", which never reaches the engine. Observed values outside a model's support raise aValueErrorthat names the observation. A stochastic engine andnormalize_observationsneed a loss. See Observation Models. BayesianPosterior: the posterior of the observation models and its log evidence, on the sequential Monte Carlo sampler.GibbsPosterioron observation models is the fractional posteriorprior · L^η.DistributionNoise(): prediction intervals that draw from the observation models;commol.scores.log_score, the log density of held-out observations under the predictive, which needsDistributionNoise().ParametricBootstrap(replicates): fits to data sets drawn from the observation models at the fit.ResamplingDiagnostics.block_lengthisNonefor it.-
A model can hold bins of different units, such as a count and a concentration: each bin starts at an amount in its own unit, a model whose bins all start at
0builds and runs, and theCalibratorrejects an initial-condition remainder whose unit is not the unit of a calibrated amount. See Bins of different units. -
Sensitivity analysis with no data:
SensitivityAnalysis(simulation, inputs, replicates=None, seed=None).run(method), the third entry point of the registry, with methods that subclassSensitivityMethod[R]. What varies is anInputSpaceof calibration parameters (model parameters or initial conditions): independent distributions (InputSpace.independent,InputSpace.uniform_on_bounds) or a weighted sample (InputSpace.from_sample). Each run is reduced in Rust to quantities of interest:ValueAt,Gain(as an observation with a window),Peak,PeakPositionandTrajectory. With a stochastic enginereplicatesandseedare required; a point of a design holds the mean of its replicates on common seeds, and a propagation keeps every replicate. See Sensitivity Analysis. Propagation: the draws of the inputs and the quantities of each run (PropagationResult, withsummary(confidence_level)); its sample gives the bands of aPredictive, which runs the same replicates.PRCC: partial rank correlation coefficients and their p-values (PRCCResult), weighted for a sample, with Kish's effective size in the test.Morris: elementary effects,mu,mu_starandsigma(MorrisResult), on levels at the centres of strata of equal probability.Sobol: first-order and total indices on the design of Saltelli, with bootstrap intervals (SobolResult).PolynomialChaos: a Legendre expansion ofF(x), its leave-one-out error, its Sobol indices andpredictas a surrogate (ChaosResult); the number of terms is checked before any run.SimulationPlotter.plot_prcc,plot_morrisandplot_sobol.ProfileLikelihood(parameters, threshold, max_steps): the profile of each parameter, refitted with the problem's own optimizer at values that step away from the fit by the adaptive step of Raue et al. (2009), its pointwise interval and its verdict (identifiable,practically_non_identifiable,structurally_non_identifiable), in aProfileResult;ProfileResult.report()gives the verdicts as anIdentifiabilityReportwith aprofilesection. A profile is read by resetting the other parameters to their fitted values (ProfileResult.structure), against the floor of two evaluations (ProfileResult.evaluation_floor): it gives the structural verdictin_combinationornon_identifiable, and names the parameters that compensate it. A crossing made by the bound of another parameter counts as an open side (Profile.box_crossings), andProfile.refit_errormeasures the error of a refit at the fit. See Profile Likelihood.PredictionProfile(specs, threshold): the interval of each prediction (PredictionSpec), held at each value as an equality constraint by an augmented Lagrangian. See Prediction profiles.Threshold.chi_square(level)(observation models),Threshold.beale(level)(a sum of squares of independent Gaussian errors) andThreshold.bootstrap_calibrated(bootstrap, level)(any loss, calibrated by the replicates of a bootstrap).SimulationPlotter.plot_profiles.
Changed¶
- Nelder-Mead with an infinite bound fails with the message
Nelder-Mead needs finite bounds, instead of a rate that is not a number. The sine transform of the bounds and the root finder of the quantiles come from the argmin fork; calibrations, profiles, quantiles and draws give the same results. - Breaking:
InformationMatchingEta(block_length)takes a requiredblock_length, anintor"auto": the variance of the scores is summed over blocks of positions, as the meat ofSandwich, so it holds under dependence;block_length=1gives the independent version. It also takes a problem with observation models, where a correct model givesη ≈ 1. - Breaking:
Predictive(source, sample, positions)takes aCalibratoror aSensitivityAnalysis(anyPredictiveSource), and its attributecalibratoris nowsource.Predictive.at_observationsof an analysis takes onlyNoNoise(), andscores.log_scorerefuses its predictive. - With a stochastic engine, every replicate of every draw of a
Predictive, ofPredictive.at_observationsand ofscores.log_scorehas a seed of its own, derived from the calibrator's seed; before, every draw ran the same replicates of the problem, so a predictive with few replicates showed the same few noise paths for every draw. The bands and scores of a stochastic predictive change; a seed still repeats them. - Breaking:
ModelStructure.check_calibration_values(model, engine, parameters, remainder)takes the values and the remainder instead of a calibration problem; a model kind that implements it changes its signature. - A prior whose family does not fit its bounds is named "The distribution ... of" in its message, instead of "The prior ... of".
- The starts of
Calibrator.run(starts=n)come from the Latin hypercube ofcommol-random; a seed gives the same starts as before. - Breaking: an
Aggregatestates the initial amount of each bin in the unit of the bin, not fractions of one total. A structure that statestotal,bin_fractionsorcompartment_fractionsis rejected with the fields to use.
| Before | Now |
|---|---|
Aggregate(total=1000, bin_fractions={"S": 0.99, "I": 0.01}) |
Aggregate(bin_amounts={"S": 990, "I": 10}) |
compartment_fractions={"I_g1": 0.005} (of the total) |
compartment_amounts={"I_g1": 5} (in the unit of its bin) |
model.structure.total |
No declared total: sum(model.structure.bin_amounts.values()) for bins of one unit |
update_bin_fractions({...}), which kept the sum at 1 |
update_bin_amounts({...}); the other bins keep their amounts |
update_compartment_fractions(process, {...}) |
update_compartment_amounts(process, {...}) |
stratification_fractions |
Unchanged: the split of the amount of every bin a stratification applies to |
- Breaking: a calibrated initial condition of a bin, a bin prefix or a
compartment is an amount in the unit of its bin, not a fraction of the
total; its bounds are amounts too. A category value is still a split. The
remainder takes the sum of the amounts the model states less every other
compartment, so a bin that no value covers keeps its amount; with a bin left
None, the remainder holds the sum ({"S": 1000, "I": None}). Without a remainder no initial state is infeasible. A remainder below zero is measured as a share of the amounts the values set. The write-back updates the amount of a bin only when the values and the remainder set every compartment of it. - Breaking:
applied_parameters_reportgives an aggregate model the sectionsinitial_amounts(amount,unit,calibrated) andcategory_splits(group, a list of categories,fraction,calibrated) in place ofinitial_conditions, with nototalrow. A category split is reported as the fraction of its group, with no rescaling of its interval. - Breaking: a stochastic engine needs a whole amount in each bin, not a
whole total, and
largest_remainderandrandomround within each bin, so a unit never moves to another bin. - Breaking: the description of an aggregate model shows the bin amounts and their units, and the initial state with the unit of each compartment.
- Breaking (Rust):
InitialConditionshasbin_amountsandcompartment_amountsin place oftotal,bin_fractionsandcompartment_fractions;CompartmentalEngine::totalis the sum of the amounts the model states;check_model_valuesreturns the plan it resolves, andInitialConditionPlan::coveragegives the compartments each value sets. - Breaking:
CalibrationProblem.loss_functionhas no default: a problem needsloss_functionorobservation_models, exactly one, and raises aValueErrorwith both or neither. Writeloss_function="sse"where a problem relied on the default. - Breaking: public class names write every letter of an acronym in
capitals, as PEP 8 asks:
OdeSolverisODESolver.EnbPIkeeps the spelling of its authors. See Conventions.
Fixed¶
- A
DifferentialEquationsrun withmethod="tsit45"whose solution blows up returned NaN with no error. A step that leaves the state not finite now raisesRuntimeErrorthat names the last position the solver reached, with either method. - The finite-difference step of a parameter with an infinite bound was infinite, so its sensitivities, identifiability and curvature failed: the width of its bounds now counts as 1 in the step.
- The quantile of a distribution hundreds of orders of magnitude below 1,
such as
Beta(0.01, 1)at1e-3, was far from it, so a prior or a draw of such a shape was wrong: when its Newton and halving steps run out, the quantile now bisects the floating-point numbers between the two ends. - A particle swarm whose first particle had a NaN cost kept no best particle and returned its start point: a NaN cost now counts as the worst.
- A transition whose rate holds
$compartmentdroppedper_compartment,accumulatorsand theabsoluteof its stratified rates: each transition it expands to now keeps them, so withper_compartment=True$compartmentreads the compartment that flows, and its accumulators count the flow. - A
Calibratoron aSimulationthat had run shared a calibrated bin, bin prefix or remainder across its compartments in the proportions of the last state of the run, so its fit, its uncertainty and itsPredictivestarted from a wrong split: they now share it in the proportions of the initial state. Model.update_parameterswrote each value with no check, so a list of points for a series parameter left a model thatSimulationcould not read, with an error that named no parameter. It now takes a list of points for a series parameter and keeps its mode, or aSeriesValue(now incommol) with a new mode, and it runs the checks of a new model: a value it cannot take raisesValueErrorat the call, with the id of the parameter, and leaves the model unchanged.- A
Predictiveat positions out of order failed inside a draw with aRuntimeErrorthat named a parameter set: it now raisesValueErrorbefore any draw runs, with the first pair out of order, as it does for a position the engine rejects.
[0.5.0] - 2026-10-19¶
This release replaces the ensemble calibration with uncertainty methods that
have a stated meaning. Calibrator.uncertainty resamples the data (bootstraps
and the jackknife) and returns a ParameterSample; a Predictive runs its
draws, and commol.scores scores its bands. Calibrator.identifiability tells
which parameters the model and the data determine, and Calibrator.run(starts=n)
fits from several start points. Before you upgrade, read the Breaking
entries: the ensemble calibration is removed (use BayesianBootstrap in its
place), and every interval uses one quantile, so the intervals of
member_statistics and SimulationReplicates.quantiles move slightly. See
Uncertainty and
Resampling.
Added¶
Calibrator.run(starts=n)runs the optimizer fromnstart points in parallel and returns the best: the initial guess, then rows of a Latin hypercube over the bounds drawn from the problem'sseed.CalibrationResult.startslists every start (StartSummary: start, end, loss, converged); distinct ends with equal losses show that the data do not identify the parameters. See Several starts.CalibrationResult.failed_runs: why each failed start failed, also logged as a warning.ParameterSample: weighted draws of the parameters of a problem, with their invariants checked at construction and weighted statistics computed in Rust (mean,covariance,correlation,quantiles,interval,effective_size,draws,resample,summary). It saves to JSON and loads back equal.Predictive(calibrator, sample, positions): the run of every draw, in parallel, its band (band,series_band) and every draw's value of a set of observations through the observation operator of the loss, with an observation noise that has no default (at_observations(observed_data, noise):NoNoise()for a band of the model,ResidualNoise(...)for a prediction interval).SimulationPlotterandsimulation_reporttake a predictive.commol.scores: empirical coverage, interval score, weighted interval score, the CRPS of a weighted sample and PIT values of a predictive, per observation and per series. Each takes the observation noise, so a band of the model is never scored as a prediction interval by accident.Calibrator.uncertainty(method): one entry point for every uncertainty method. A method's configuration subclassesUncertaintyMethod[R]and registers its runner; the result type follows the configuration's type.Calibrator.identifiability(method): which parameters the model and the data determine, and which combinations they determine in their place.StructuralRank(the rank of the sensitivities at random points, on the design of the observations or a dense one),Collinearity(Brun's indices at a fit or over a sample),SampleIdentifiability(contraction, correlations and modes of a sample) andStartsIdentifiability(starts of equal loss that end apart). Each returns anIdentifiabilityReportwith a structural and a practical verdict per parameter;mergejoins reports andidentifiability_reportgives one as plain data. See Identifiability.InferenceMethod[R], the common base ofUncertaintyMethod[R]andIdentifiabilityMethod[R]; each entry point refuses the other kind with aTypeError.weighted_quantiles, and aweightsargument formember_statistics.CalibrationHistoryandCalibrationEvaluation, returned byCalibrator.run_with_history().- The crate
commol-inference: the parameter sample, the one quantile, the predictive and the scores. See Uncertainty. - Resampling methods with no noise model, through
Calibrator.uncertainty:BayesianBootstrap,ResidualBootstrapandWildBootstrapreturn aBootstrapResult(the fit, one draw per replicate, diagnostics, and the Monte Carlo error of each interval bound,interval_error);BlockJackknifereturns aJackknifeResult(estimate, bias, standard error, pseudo-values and the influence of each block). Blocks of consecutive positions span every series;block_lengthis an integer or"auto"(Politis and White). See Resampling. ResidualNoise: observation noise made of blocks of residuals of a fit, which gives a prediction interval with no noise model;NoNoisegives a band of the model.CalibrationProblem::with_data(Rust): a copy of a problem with other observed values and weights.
Changed¶
- The
latestversion of the documentation site is the documentation of the last release, published from its tag; thedevversion followsmain. - Breaking: every interval uses one quantile: the weighted inverse of the
empirical distribution between the midpoints of the cumulative weights,
NumPy's
method="hazen"with equal weights.member_statisticsandSimulationReplicates.quantilesused NumPy'slinear; their intervals move slightly. - Breaking:
CalibrationResult.best_parameterskeeps the order ofCalibrationProblem.parameters. - Breaking:
Calibrator.run_with_history()returns aCalibrationHistoryinstead of the extension's object. - Breaking:
applied_parameters_report(..., sample=, confidence_level=)reads the statistics of aParameterSample;parameter_statisticsis gone. - Breaking (Rust):
CalibrationProblemis split into layers (ForwardModel,ObservationOperator,Feasibility,Discrepancy,OptimizationObjective);optimizetakes the problem by reference, and the optimizer configurations have noverbosefield.CalibrationEvaluationhas nopredictions. - A verbose optimizer writes its table to Python's
sys.stdoutonly, and needs one start; the library never writes to the standard error. - A rate, a formula parameter or a constraint that the expression compiler
cannot compile, such as
typeof(k), is rejected when the model or the calibration problem is built, with the reason; it no longer fails when it is evaluated. - The functions
erf,erfc,tgamma,lgamma,j0,j1,y0,y1,fdimandremainderof a rate come from thelibmcrate, so they give the same values on every platform; the other functions still come from the system's math library. AxisPatternis abstract:AxisPattern()raisesTypeError. Build a pattern with one of its class methods.- Error messages that list ids (the stratifications of a transition, the
missing and extra keys of the fractions of an
Aggregate) list them in sorted order.
Fixed¶
- Several starts of Nelder-Mead began at the same point: each start now comes from the run seed.
- A parameter value could land on another parameter when results were read by name from an unordered mapping.
- A failed run of a multi-start optimization was written to the standard error and dropped; it is now part of the result.
- An error of the model at a vertex of the initial Nelder-Mead simplex made the optimizer panic; it now stops the calibration with its message (the argmin fork returns the error).
- A
tocondition on a transition without a source was ignored by its flow but applied by its accumulators;buildnow rejects it, and acategorycondition chooses the compartment the flow enters. Model.from_jsonread a file in the encoding of the system; it now reads UTF-8, the encoding of JSON.Model.check_unit_consistencychecked every flow of a category transition in the unit of the first bin of the model; each flow is now checked in the unit of its own bin. In a model of bins of different units, a rate that readstotal, a subgroup total, a category orneighborsfails the check: these add amounts of different units.- The LaTeX description wrote a part of a rate in parentheses, such as
(I / N)or the argument ofsin(t / T), as upright text, and wrote a unit inside the name of another variable (A_{g1(\text{item})}); only units are text now, each after its variable. The expanded form mergesk * pandk * (1 - p)also when a third term shares another factor, and a merged term keeps the place of its first term. ModelBuilderandSimulationlogged to the root logger of the application; they log to thecommollogger, as the rest of the library does, soadd_stderr_loggershows their messages.AxisPattern.combine(mode="max")raised "Unknown combine mode": a mode may be given by its value.- A quantile level outside
[0, 1], or NaN, gave the smallest or the largest value;ParameterSample.quantilesandweighted_quantilesnow raiseValueError. AxisPattern.from_formulaandAxisPattern.combinewith aconfigthat allows formulas longer than the default rejected them anyway; the configuration now applies.- A rate of
StochasticStepsthat is not a number raisedInfeasibleStateErrorand said that a rate cannot be negative; it now raisesRuntimeError, asStochasticEventsdoes, and says that the rate is not a number. - A rate that is not a number gave amounts that are not numbers in
DifferenceEquationsandDifferentialEquations, and a calibration only penalized the run; the run now stops withRuntimeErrorthat names the flow and the position, as in the stochastic engines. - A rate that read an id with digits, such as
e1,E2orc1star, was rejected as a malformed number; and the exponent of a number such as1e1was read as the undefined variablee1. An identifier now starts only after a character that is not a letter, a digit or_, so the digits of an id belong to its name and an exponent belongs to its number.
Removed¶
commol.api.model_loader.ModelLoader: a copy ofModel.from_jsonthat no page documented and nothing called.- Breaking: the ensemble calibration:
Calibrator.run_probabilistic,ProbabilisticCalibrator,CalibrationProblem.probabilistic_config,ProbabilisticCalibrationConfig,ProbabilisticCalibrationResult,EnsembleSolution,EnsembleSelectionSummary,ParameterSetStatisticsand their settings classes. Its intervals had no statistical meaning: its members were points the optimizer visited, and the selection maximized the coverage of the data it was fitted to. The closest replacement with no noise model isCalibrator.uncertainty(BayesianBootstrap(replicates=..., block_length=...)): the same parallel fits, each on randomly weighted data, with a stated meaning. - The dependency on
scikit-learn.
[0.4.0] - 2026-10-16¶
Added¶
Model.describe(layout="default")returns aModelDescription: the model as data, sections of tables, equations, state graphs and text, for every kind of model.render(format)gives text, Markdown (with the state graph as a Mermaid diagram), a LaTeX document, JSON or Graphviz DOT, andsave(path)writes it in the format of the suffix (.txt,.md,.tex,.json,.dot). In a notebook it shows as Markdown. See Describing a Model.layout="odd": the description follows the ODD protocol for describing individual- and agent-based models (Grimm et al. 2006, 2010, 2020), and marks what commol cannot fill "To be written by the modeller.".EngineType.dynamics_formandEngineType.schedule: how an engine advances (difference_equation,differential_equation,stochastic_events,rules), as a description shows it.- The process of a model apart from its structure:
Model.process(Process: bins, accumulators, stratifications, transitions) is written once, andModel.structurelays it out.Aggregateis the structure of amounts per compartment, the kindaggregate. neighbors_<bin>andneighborsin a transition rate: the amount of a bin among the neighbors of a unit and their total. A rate that reads them and no compartment is proportional.Aggregate(neighbors=Complete())makes every unit a neighbor of every other;Groups(stratification, weights)makes units neighbors by the categories of a stratification. A rate that reads them needsneighbors, with no default.- Variants of a structure: a subclass with the same kind, fields and engines,
saved with its
variantand loaded back as its own class. - Seeds and replicates for stochastic engines:
Simulation.run(…, seed=)andrun_at(…, seed=)(SimulationResults.seedkeeps the seed, drawn when none is given),Simulation.replicates(n, seed=)withrunandrun_atthat returnSimulationReplicates(runs in parallel, one seed each;mean,quantiles,member_statistics),Simulation.is_stochastic, andEngineType.is_stochastic.SimulationPlotterdraws the mean and a band of replicates, andsimulation_reporttakes them asensemble_runs. CalibrationProblem(replicates=…, replicate_summary=…)calibrates a stochastic engine with common random numbers:"mean","median",Quantile(q)or"crps"(the continuous ranked probability score). A stochastic engine needs them and aseed; there is no default summary.- The crate
commol-random: a portable generator (Xoshiro256PlusPlus), the derivation of replicate seeds, samplers and a sum tree of rates, for every stochastic engine. - Two stochastic engines of an aggregate model, in the crate
commol-stochastic, that run the process of any model as whole units moved at random, with no change to it:"StochasticEvents"(continuous time: Gillespie's direct method, exact also when a rate readst, ormethod="tau_leaping") and"StochasticSteps"(the chain binomial, with per-step probabilities). Their settings areStochasticEventsSettingsandStochasticStepsSettings. A total must be whole, and initial amounts that are not whole needrounding="largest_remainder"or"random", with no default. A per-step probability outside[0, 1], or probabilities out of one compartment that sum to more than 1, raiseInfeasibleStateError. See Stochastic Simulation. - The
dynamicssection of a stochastic model is an event table: each flow, the change of one event and its propensity or its draw; theschedulesection lists the settings of the engine.
Changed¶
- Breaking: a model is built from a process and a structure.
ModelBuilderwrites the process, andbuild(structure, engine, settings=None)joins it with a structure; the model is aModelof the class of the structure.
| Before | Now |
|---|---|
builder.set_initial_conditions(total=…, bin_fractions=[{"bin": "A", "fraction": 0.9}, …], stratification_fractions=[…], compartment_fractions=[…]) then builder.build(engine=…) |
builder.build(Aggregate(total=…, bin_fractions={"A": 0.9, …}, stratification_fractions={"group": {"g1": 0.5, …}}, compartment_fractions={"A_g1": 0.0}), engine=…) |
CompartmentalStructure, Model[CompartmentalStructure] |
Aggregate, Model[Aggregate] |
model.structure.bins, stratifications, transitions, accumulators |
model.process.bins, … |
model.structure.initial_conditions.total, bin_fractions, … |
model.structure.total, bin_fractions, … (mappings by id) |
model.structure.initial_state(), subgroup_total(…), get_conditioning_categories(…), update_compartment_fractions(…) |
The same methods, with the process first: model.structure.initial_state(model.process), … |
model.structure.update_initial_conditions(…) |
model.structure.update_bin_fractions(…) |
model.structure.outputs_by_source(), series_label(id) |
outputs_by_source(model), series_label(model, id) |
CompartmentFraction, BinFraction, StratificationFraction(s) |
Mappings in Aggregate |
CompartmentalEngineType |
EngineName |
- Breaking: the model file has
processandstructure, the structure haskind: "aggregate"andvariant, and the fractions are objects keyed by id. A file of 0.3 does not load; rebuild the model and save it again. - Breaking: an engine names a method, not a kind:
EngineType.kindsreplacesEngineType.kind, and the error of an engine that does not run a kind lists the engines of that kind. - Breaking:
neighborsand the names that start withneighbors_are reserved: no parameter, bin or accumulator may take them. EngineType.equation_lhscomes from the form of the dynamics of the engine (DynamicsFormin Rust), and returnsNonefor an engine without equations.- Python names follow PEP 8: ruff checks them (
Nrules).
Fixed¶
- The unit check reads the flows the engine plans, so a proportional rate is
checked as its flow, the rate times its source:
k2in1/dayfrom a bin initempasses asitem/dayinstead of failing as1/day.
Removed¶
- Breaking:
Model.print_equations,ModelStructure.print_equationsandPrintEquationsOutputFormat.model.describe().render("text")shows the same compact and expanded forms, andrender("latex")writes them in LaTeX; a description with some units missing shows none instead of raising.
[0.3.0] - 2026-10-15¶
Added¶
- An ODE engine: a model built with
engine="DifferentialEquations"runs in continuous time,dX/dt = Σ flows, integrated bydiffsol(commol-differential). It has the same flows, outputs, accumulators, calibration, ensembles, reports and plots as a difference model. On[n, n + 1)a rate seesstep = nand the continuoust. The engine restarts its integration at each integer when a rate readsstep, and wheretreaches a value a rate compares it with (t >= t_on), so the solver cannot step over a jump. OdeSolver(method"tsit45"or"bdf",rtol,atol), the settings of theDifferentialEquationsengine, stored in the model asModel.engine.settingsand set withbuild(engine="DifferentialEquations", settings=...).- One list of engines in the Rust engine (
EngineType): each entry names the model kind it runs, validates its settings, and says the form of its equations and what a rate means (per_steporcontinuous). Python asks it throughcommol_rs.engine.EngineTypeinstead of branching on engine names. Simulation.run_at(at): the outputs at the given sorted positions. A differential model takes any position, such as0.5.SimulationResults: whatSimulation.runandrun_atreturn. Adictfrom output name to values, as before, with the positions inpositionsand the rows inrows().- The window grid as module functions:
window_start,window_end,window_indexandwindow_ends, besidewindowed_totals. - A public Pydantic class rejects an unknown field and names it
(
extra="forbid"on every class ofcommol.__all__), so a misspelt or an old name is an error instead of being dropped. - Impulses:
add_transition(..., impulse_at=[...])orimpulse_every=(period, offset)moves the rate (a fraction or an amount) at once at those steps, in both engines, before the flows of the step.print_equationslists impulses apart. ModelBuilder(..., step_unit=...)(Model.step_unit): the unit of one step for the unit check, of any single dimension: the axis is the independent variable of the model, which need not be time (for example"meter"). Without it, the step unit isstep, the default increment of any axis.AxisUnits(step_unit, engine)andModel.axis_units():steps(value, unit)converts a value in any unit of the step dimension to steps onpint(3 daywith a step ofhouris72.0;2 kmwith a step ofmis2000.0), and raises, naming both units, for a unit of another dimension.rate_from_mean_span(span, unit)andrate_from_probability(probability, period, unit)ask the engine what a rate means: the per-step rate forDifferenceEquations(1 - exp(-1/n),1 - (1 - p)^(1/n)) and the continuous rate forDifferentialEquations(1/n,-ln(1 - p)/n).EngineType.rate_from_mean_spanandrate_from_probabilityin the engine's list (RateMeaningin Rust), which hold those formulas.- The unit check reads
if(condition, a, b): both branches share a unit. - One interface for every kind of model:
ModelStructure, an abstract base class that the structure of each kind inherits.Simulation,Calibrator,ProbabilisticCalibrator, the reports andSimulationPlotteruse only it, so another kind (for example an agent network) is a new subclass with its engines in the engine's list.CompartmentalStructureis the structure of a compartmental model. A subclass that misses a method cannot be made, and the error names the method. - An engine runs one model kind, and a model whose engine runs another kind is rejected when it is made, with an error that names the engine and both kinds.
- Each kind declares the values calibration may set besides
parameterandfactor(ModelStructure.calibration_value_types); a compartmental model acceptsinitial_condition. A type the kind does not accept raises an error that names the kind.
Changed¶
- Breaking: a
DifferenceEquationsstep that leaves a compartment below zero (by more than10⁻¹²of the total) raisesInfeasibleStateError, aValueError. The message names the compartment, the step, its amount before the step and the flows out of it. Before, the run went on with negative amounts. A calibration gives such a run a penalty and goes on; any other error of a run still stops it. See Amounts below zero. - Breaking (Rust):
SimulationEngine::run_at_positions_into_buffer,run_at,runandstepreturnSimulationError(InfeasibleorFailed) instead ofString. - A position on the axis is a
floateverywhere, and the engine alone decides which positions its model type accepts: a difference model integer steps, a differential model any non-negative position. Its error names the observation, constraint, impulse transition or series parameter that holds a rejected position. The engine trait runs only at positions (run_at_positions_into_buffer);run(n)returns the grid it ran on. - The engine of a model and its settings are one field,
Model.engine = {type, settings}(ModelEngine), which replacesModel.dynamics.typology. The builder takes the engine where it builds, so one builder builds the model for each engine with its own settings. Settings of one engine given to another raise an error from the engine that names both. A model file saved before hasdynamics.typologyand does not load: move it to"engine": {"type": …, "settings": null}.
| Before | Now |
|---|---|
builder.build(typology="DifferenceEquations") |
builder.build(engine="DifferenceEquations") |
model.dynamics.typology |
model.engine.type |
commol_rs.difference.DifferenceEquations(model), commol_rs.differential.DifferentialEquations(model) |
commol_rs.engine.Engine(model), the engine the model names |
- A model is a header, an engine and a structure: Model.structure, tagged |
|
in JSON by kind, replaces Model.population and Model.dynamics, and the |
|
transitions are held once. Model is generic in its structure class: |
|
ModelBuilder.build returns Model[CompartmentalStructure], and |
|
Model.from_json reads the class from the kind. The methods that only a |
|
| compartmental model has moved to its structure. A model file saved before | |
has population and dynamics and does not load: move the fields of |
|
population and dynamics.transitions into |
|
"structure": {"kind": "compartmental", …}. |
| Before | Now |
|---|---|
model.population.bins, .accumulators, .stratifications, .initial_conditions |
model.structure.bins, … |
model.dynamics.transitions (and model.population.transitions) |
model.structure.transitions |
model.initial_state() |
model.structure.initial_state() |
model.update_initial_conditions(…), update_stratification_fractions(…), update_compartment_fractions(…) |
model.structure.update_initial_conditions(…), … |
model.subgroup_population(…), model.get_conditioning_categories(…) |
model.structure.subgroup_total(…), model.structure.get_conditioning_categories(…) |
model.get_outputs_by_source() |
model.structure.outputs_by_source() |
model.get_uncalibrated_initial_conditions() |
model.structure.uncalibrated_values() |
commol.context.dynamics (Transition, StratifiedRate) |
commol.context.transition |
commol.context.engine.EngineType |
CompartmentalEngineType; ModelEngine.type is a str that the engine's list checks |
commol_rs.core.Model(population=…, dynamics=…) |
commol_rs.core.Model(structure=commol_rs.core.CompartmentalStructure(…)) |
commol_rs.model.initial_state(model), expanded_outputs, transition_flows |
they take a commol_rs.core.CompartmentalStructure |
commol_rs.calibration.check_initial_conditions |
check_model_values |
commol_rs.calibration.calibrate(…, initial_population_size, …) and the other calibration functions |
no initial_population_size: the engine knows it |
applied_parameters_reportreturnsparametersand the sections of the kind of the model; a compartmental model addsinitial_conditions, with the same keys as before.- The Rust engine trait (
SimulationEngine) is the same for every model kind:compartments,state(waspopulation),set_initial_conditionandoutput_labelsmoved toCompartmentalEngine(commol-model), and the engine resolves and writes the values of its kind (plan_values,apply_values). The initial-condition plan moved fromcommol-calibrationtocommol-model. The crates are not published. - Names of positions, lengths and counts, and names with more than one meaning. No old name is kept; an old keyword raises an error that names it.
| Before | Now |
|---|---|
ObservedDataPoint(step=…) |
ObservedDataPoint(at=…) |
ObservedDataPoint(window_steps=…) |
ObservedDataPoint(window=…) |
ObservedDataPoint(compartment="B") |
ObservedDataPoint(output="B") |
ObservedDataPoint(compartment="label", compartments=[…]) |
ObservedDataPoint(label="label", outputs=[…]) |
ObservedDataPoint(scale_id=…) |
ObservedDataPoint(factor_id=…) |
CalibrationParameter(parameter_type="scale"), CalibrationParameterType.SCALE |
"factor", CalibrationParameterType.FACTOR |
CalibrationConstraint(time_steps=…) |
CalibrationConstraint(at=…) |
NelderMeadConfig(initial_step=…) |
NelderMeadConfig(initial_simplex_size=…) |
Simulation.run_at(steps) |
Simulation.run_at(at) |
Simulation.windowed_totals(results, source_ids, window_steps, at_steps) |
Simulation.windowed_totals(results, ids, window, at) |
Simulation.total_series(results, source_ids), group_outputs(source_ids), outputs_for(source_id) |
ids, id |
windowed_totals(series, window_steps, at_steps) |
windowed_totals(series, positions, window, at) |
window_end_steps(window_steps, num_steps) |
window_ends(window, end) |
TimeScale.window_start, window_end, window_index |
commol.window_start, window_end, window_index |
simulation_report(…, steps, window_steps, …), series_report, accumulators_report, observed_report |
at, window |
SimulationPlotter.plot_series(step_to_label=…) |
position_label=…, which takes a float |
TimePattern (commol.api.time_patterns) |
AxisPattern (commol.api.patterns) |
TimePattern.window(start, end, amount) |
AxisPattern.interval(start, end, value) |
TimeSeriesValue, "time-series parameter" |
SeriesValue, "series parameter" |
EnsembleSolution.windowed_prediction_steps |
windowed_prediction_positions |
engine.current_step() |
engine.current_position() |
simulation.run(n, output_format="list_of_lists") |
simulation.run(n).rows() |
- Reports, windows, plots and ensemble statistics read a value at a position
of the results, not at a list index, so a differential model windows and
reports at non-integer positions once its results hold them. The report key
scale_idisfactor_id, andcompartmentsisoutputs. EnsembleSolutionhasprediction_positions; its prediction series hold the unit grid and every observation position and window start.- Series parameters take
(position, value)points withfloatpositions."linear"interpolates intand"step_function"holds from any position; a differential engine restarts its integration at their points, so a"linear"series is a ramp, not a staircase."pulse"keeps integer positions. A difference model gives the same results and rejects a point between integers. AxisPattern.intervalandlinear_rampreadtand takefloatbounds. A difference model gives the same results; in a differential model an interval opens at any position and a ramp is continuous.- A pattern parameter that the builder generates is named
_series_<n>(was_ts_<n>). - The model compiler moved from
commol-differenceto the new cratecommol-model, which both engines use;commol_rs.differenceno longer hasinitial_state,expanded_outputs,transition_flowsandcheck_names, which are incommol_rs.model. No public Python name changes. SimulationPlotter.plot_seriesandplot_cumulativetakeseriesinstead ofbins. An entry is an output name or a bin or accumulator id; an id plots the sum of its outputs, so a bin of a stratified model can be plotted. There is nobinsalias: rename the argument.- Every loss uses the observation weights the same way:
"sse"isΣ w·r²,"rmse"is√(Σ w·r² / Σ w)and"mae"isΣ w·|r| / Σ w. Before,"sse"squared the weight and"rmse"and"mae"ignored it. With every weight at 1 nothing changes. The ensemble central loss follows the same rule. Simulation.runstarts from the initial state at every call, so two calls give the same result. Before, a secondruncontinued the first one. To continue a run, callsimulation.engine.step().-
Model.check_unit_consistencyinfers one step unit from the parameter units (day,week,step, …;stepwhen there is none) and requires every flow to be exactly<bin unit>/<step unit>. A model in"1/step"now passes. Rates in two units of time, such as"1/day"and"1/week", are an error that names each parameter, because the engine converts nothing. Before, the check accepted any convertible unit and defaulted today. -
Nelder-Mead searches inside the bounds: it optimizes an unbounded coordinate and scores
x = lo + (hi − lo)·(sin u + 1)/2. Its first simplex is set byNelderMeadConfig.initial_simplex_size(a fraction of each bound width, default 0.1), which replacessimplex_perturbation; there is no alias. Every Nelder-Mead result can change. -
The library names what the compartments hold without assuming individuals: the state (the amount of every compartment), the amount (the value of one compartment), the total (their sum, which the initial fractions are taken of) and a subgroup total (the sum of the compartments with some categories). The total is a
float, finite and greater than 0, so a model of a mass or a concentration can be stated; before, it was anint. The rate variableNistotalandN_<cat>…istotal_<cat>…, soNis a free id (a bin may be namedN).AxisPatterntakes avalue. No old name is kept: a model file withpopulation_sizedoes not load, and the error names the field; rename it tototal, andNtototalin every rate, formula and constraint.
| Before | Now |
|---|---|
set_initial_conditions(population_size=…), InitialConditions.population_size: int, JSON key population_size |
set_initial_conditions(total=…), InitialConditions.total: float, JSON key total |
Rate, formula and constraint variables N, N_<cat>… (N_g1, N_g1_t1) |
total, total_<cat>… (total_g1, total_g1_t1) |
RESERVED_VARIABLE_NAMES = ("N", …) |
("total", "step", "t", "pi", "e") |
InitialConditions.subgroup_population(…), CompartmentalStructure.subgroup_population(…) |
subgroup_total(…) |
InitialCompartment.population |
InitialCompartment.amount |
applied_parameters_report: row "N", group "population" |
row "total", group "total" |
AxisPattern.pulse(at, amount), pulses(at, amount), periodic(period, amount, offset), interval(start, end, amount) |
value instead of amount |
get_predefined_variable_units(stratifications, bin_unit) |
get_predefined_variable_units(subgroup_totals, bin_unit), with the names the engine gives |
commol_rs.engine.Engine.population |
Engine.state |
commol_rs.core.InitialConditions(population_size=…) |
InitialConditions(total=…) |
commol_rs.calibration.calibrated_initial_population |
calibrated_initial_state |
| — | commol_rs.model.subgroup_total_names(structure) |
Rust: CompartmentalEngine::population, population_size; DifferenceEquations::population, DifferentialEquations::population |
state, total |
- The library and its documentation assume no application and no axis: text says "along the axis" or "position", not "over time", and examples use abstract names. A rate that names no compartment is proportional (was "per-capita"), because a source can hold a mass or a concentration. The errors of a constraint at positions say "Constraint '…' evaluation failed at position …" and "Position … not found …" (were "Time-dependent constraint" and "Time step").
| Before | Now |
|---|---|
AxisPattern.seasonal(amplitude, period, phase, baseline) |
AxisPattern.sinusoid(amplitude, period, phase, baseline) |
TransitionFlow.per_capita_of (commol.context._model.engine_state) |
TransitionFlow.proportional_to |
Guide anchor #absolute-and-per-capita-rates |
#absolute-and-proportional-rates |
Guide anchor #time-dependent-rates |
#rates-that-change-along-the-axis |
- The unit check no longer infers the step unit from the parameter units: it
is the declared
step_unitor elsestep. A model with rates in1/dayand nostep_unitfails the check, and the message says to declare it: addstep_unit="day"toModelBuilder. - A unit means what
pintdefines. The list of time aliases (decade,quarter,semester,mo,wk, …) is gone:pintreadsquarteras a mass anddecadeas a number, so1/quarterwas a mass without error. A name thatpintdoes not define is a unit of its own dimension. - In the unit check,
thas the step unit (it is a position on the axis), and a number written beside it in a sum, a difference or a comparison is read in that unit.stepstays dimensionless. A rate such assin(2 * pi * t / 365)now fails the check; writesin(2 * pi * t / period)withperiodin the step unit. - Guides and docstrings say what a rate means for each engine (What a rate means) instead of "every flow applies once per step".
Removed¶
TimeScale(commol.api.time_scale). Its rate conversions gave per-step rates only, which a differential model reads as too small.
| Before | Now |
|---|---|
TimeScale(step_seconds=3600), TimeScale.from_step_unit("hour") |
AxisUnits(step_unit="hour"), or model.axis_units() |
steps_from_seconds/minutes/hours/days/weeks(x) |
steps(x, "second"), steps(x, "minute"), … |
steps_per_hour, steps_per_day, steps_per_week |
steps(1, "hour"), steps(1, "day"), steps(1, "week") |
rate_from_mean_duration(seconds) |
rate_from_mean_span(span, unit), which asks the engine |
rate_from_probability(p, period_steps) |
rate_from_probability(p, period, unit), which asks the engine |
window_start, window_end, window_index |
commol.window_start, window_end, window_index |
steps returns a float and does not raise for a fraction of a step; the
engine rejects a position it does not accept where the position is used.
- Transition conditions:
Transition.condition, theconditionargument ofModelBuilder.add_transition,ModelBuilder.create_condition, theConditionandRuletypes, and theLogicOperatorsandVariablePrefixesenums, in Python and in Rust. The engine never evaluated them, so a condition that was never true still moved population. Write the condition in the rate ("if(step > 30, k, 0)"), or use anAxisPatternor a stratified rate. A model file with aconditionkey raises an error that names it. Simulation.run(..., output_format=...)andrun_at(..., output_format=...): both returnSimulationResults; userows()for one row per position.commol_core::integer_steps,SimulationEngine::run_into_bufferandrun_at_steps_into_buffer; the Python checks that repeated the engine's rules on positions (check_steps_for_typology, the impulse check ofDynamics, the order check ofSimulation.run_at).- The
"weighted_sse"loss andLossConfig.weighted_sse(). Use"sse", which uses the weights. commol.constants.ModelTypes. The names of the compartmental engines are theLiteralcommol.context.engine.CompartmentalEngineType.PopulationandDynamics, in Python (commol.context.population,commol.context.dynamics) and in Rust, and their PyO3 classes; the structure of a model isCompartmentalStructure.Model.get_uncalibrated_initial_conditions,Model.initial_state, and the other methods of a compartmental model onModel: they are onmodel.structure(see Changed).
Fixed¶
- Examples 6 to 9 of the guide (
examples.md) no longer go below zero. Example 6 writes the rate it describes,k2_max * B * max(0, 1 - C / capacity); Examples 7, 8 and 9 setper_compartment=Trueonflow_AB, so each compartment ofAloses an amount computed from its own amount. -
A differential model: on a piece of integration that ends where a rate jumps (a value
tis compared with, a series point), the right-hand side seestbelow the end. Before, the last solver step of the piece already saw the rate after the jump, so a rate of 0.5 that starts at 2.5 moved3·10⁻⁸of the state before 2.5. -
CalibrationResult.convergedisTrueonly when the optimizer met its convergence test ortarget_cost. Before, every finished run reportedTrue, also one that stopped atmax_iterations. - Nelder-Mead from a start value of 0 builds a real simplex, and no longer
stops at a bound where the clamped loss is flat. Before, the simplex was
degenerate at 0, and could collapse on a bound and report
converged=True. - The verbose Nelder-Mead run uses the configured simplex. Before, it always used a 10% perturbation.
Model.check_unit_consistencynames every bin without a unit. Before, it failed with "'None' is not defined in the unit registry".- A
Simulationfollows its model:runandenginerebuild the engine when the model changed after theSimulationwas created, for example withupdate_parametersorapply_calibration_parameters. Before, it used the old values, or failed with "Parameter index … not found" for a parameter that wasNone. piandework in every transition rate, such as"pi * k * A". Before, outside a function argument, the simulation failed with "Variable 'pi' not found".%(remainder) works as the only operator of a rate, as in"step % 7". Before, the rate was read as a parameter name.- A
total_…variable that names no subgroup total, such astotal_t1_g1whengroupis declared beforetype, is a build error that lists the valid names. Before, a formula parameter or the engine gave it the value 0. - A bin or an accumulator id cannot be
total,step,t,piore, and a bin cannot expand into a name that is also atotal_<categories>subgroup total. Before, the engine variable hid such a bin, and the two sums shared one value. - A parameter id cannot be
total,step,t,pi,e, an expanded compartment or a subgroup total. Before, only bin ids were rejected, and the parameter and the engine variable shared one value. ModelBuilder.resetremoves the accumulators, andModelBuilder.clonecopies them and thebin_unit. Before,resetkept the accumulators and a clone lost them.- An
initial_conditionid is resolved by the engine when theCalibratoror theProbabilisticCalibratoris created. A category of a non-binary stratification now gives "needs a binary stratification" instead of "not found", and bin prefixes ("B_g1") and qualified categories ("group:g1") are accepted, as documented. Before, both were rejected as "not found".
Documentation¶
- The guide is rewritten and split into pages with one job each (building models, transitions, expressions, time patterns, units, simulations, and six calibration pages). Every example runs in CI, and every claim was checked against the engine. Behaviors that were described wrongly are now documented as they are, among them: a violated constraint replaces the loss with a penalty.
- The API reference documents every public name, and
Model, plotting and reports have their own pages. - The site publishes
llms.txtandllms-full.txtfor AI agents, and marks the pages of thedevversion. README.mdpresents Commol as a library for declarative dynamical modelling: the idea behind it, what it can model and its limits, an example that derives, calibrates and simulates a model, and links to the guides. The citation title is "Commol: High-Performance Declarative Dynamical Modelling in Python".- The package metadata on PyPI has the same description, and keywords and classifiers (Python versions, audience, topic, development status).
[0.2.0] - 2026-10-12¶
Added¶
- Per-compartment initial conditions:
InitialConditions.compartment_fractions(Python and Rust) accepts overrides keyed by expanded compartment name, expressed as fractions of the total population and applied after the uniform expansion. IncludesModelBuilder.set_initial_conditions(compartment_fractions=...),Model.update_compartment_fractions, andModel.initial_state(), which returns each expanded compartment with its bin, categories and initial population as the engine builds it. The Rust engine is the single source of truth for the expansion and validates the overrides (unknown or repeated name, fraction outside0.0..=1.0, total abovepopulation_size). - Rust
commol_difference::initial_stateandcommol_calibration::calibrated_initial_population, exposed to Python. The calibration objective, the calibrated predictions and the model write-back share one implementation of how calibration values reach the engine.
Changed¶
- Breaking: calibrated initial conditions no longer auto-calculate the
last population-fraction value as the remainder of the population. The new
CalibrationProblem.initial_condition_remaindernames the compartment or bin that absorbs the difference; it is not calibrated and keeps its own split. Without it, values are applied as given and only an initial population abovepopulation_sizeis penalized. A single exact compartment, such as a composite state, can now be calibrated on its own. - Breaking: calibrated initial conditions are applied in fixed layers — bins, category splits, exact compartments, remainder — instead of in parameter order.
- Breaking: a stratification category may be named with a qualified id
(
sex:female). A bare name that more than one stratification declares is rejected as ambiguous, in calibration and inupdate_stratification_fractions, and the report qualifies its rows. Category pairs come from the engine's compartment labels instead of parsing compartment names. - Breaking:
get_category_fraction,subgroup_populationandget_conditioning_categoriesresolve category ids like calibration: a qualified id (sex:female) selects its stratification and a bare name that more than one stratification declares raises instead of silently using the first one.get_conditioning_categoriesreturns qualified ids for shared names. - Bin and bin-prefix initial-condition ids, and the remainder, resolve from the
engine's compartment labels instead of name prefixes, so a bin
Ano longer covers the compartments of a binA_x. - The Rust engine expands bins and accumulators in one function; the equation
printer and unit checker use it (
expanded_outputs) instead of their own Python expansion. The RustModelTypesgainsDifferentialEquations, so any model definition can be expanded; the difference engine rejects it. - Breaking:
update_bin_fractions/update_initial_conditionsapply the same rule as construction: the bin fractions must sum to 1.0 after the update. -
Breaking (Rust):
SimulationEnginerequirescompartment_labels().CalibrationProblem::fix_auto_calculated_parametersandget_parameter_fix_infoare removed;applied_parameter_valuesreturns the clamped values the objective scored, which is what results now report. -
Breaking: transitions are planned by the Rust engine from compartment labels (bin and categories) instead of compartment names, in one planner shared by the simulation and
print_equations. The printed equations are therefore the simulated ones: a per-capita rate is shown multiplied by its source (gamma * I), a transition without flows shows(no flows), and the compact form lists the flows of each transition. - Breaking: building a model rejects a stratified rate whose
categoryconditions match no source compartment (usetoto route into a stratification the source does not carry), a transition with several sources and a per-capita rate, a category transition without exactly one source and one target, and a flow that ends in categories no compartment has. These used to be skipped without notice. - Constraint aggregates (a bin or bin prefix in a constraint expression) are
built from output labels, so a bin
Ano longer sums the outputs of a binA_x.
Fixed¶
- A transition between bins no longer moves bins whose id starts with the
source bin id:
A -> BmovedAB -> BBas well. - Transitions between the categories of a stratification (
young -> old) are now simulated; the engine used to ignore them whileprint_equationsshowed them. -
A transition with several sources or targets (
A + B -> 2 C) now applies its flow to every source and target, as documented; the engine used to move only the first source to the first target. -
The applied-parameters report now shows the effective head count for bin, stratification-category, and subgroup rows — the uniform expansion plus the declared
compartment_fractionsoverrides — and reports eachfractionas that head count over the declaredpopulation_size, so every row shares one denominator withN. An aggregate row written back as overrides no longer reports the stale pre-calibration value ascalibrated, and a bin left uncalibrated keeps itsnullmarker even when overrides touch some of its compartments. - A conditional-stratification category is calibrated as a fraction of its
conditioning subgroup but reported as a fraction of the whole population, so
the report rescales its
intervalby the subgroup's share of the population; the interval now brackets the reportedfraction. apply_calibration_parameterswrites initial-condition values back so the model rebuilds the initial population the engine scored for them. Bin and category values update the fractions they declare, as the engine applied them; every compartment those fractions do not reproduce is pinned as a per-compartment override. The call computes the result before it changes the model, so a rejected value leaves the model unchanged.population_sizemust be greater than zero, reported as a domainValueErrorrather than a laterZeroDivisionError.update_bin_fractionsre-applies the sum-to-1/conservation rule.- Calibrating a bare category name that two stratifications declare no longer
builds an initial population larger than
population_size: the engine used to split the overlapping pairs of both stratifications from one snapshot. - The PyO3
InitialConditionsbinding exposespopulation_size,bin_fractions,stratification_fractionsandcompartment_fractionswith getters, and the_commol_rsstub declares only those real attributes (correcting thebin_fractionsfield name). - An initial-condition override that names an unknown compartment raises a
ValueErrorwhen the model is built or updated, and aRuntimeErrorfrom theDifferenceEquationsengine constructor. - The greedy ensemble search of probabilistic calibration takes its random starts from the shuffled part of the sample (argmin fork). The starts were biased towards low indices, so the same seed can now select a different ensemble.
Dependencies¶
- cranelift 0.126, rand 0.10 (with the argmin fork migrated to it), thiserror 2, evalexpr 13 and pyo3 0.29.
[0.1.0] to [0.1.3] - 2026¶
See the GitHub Releases page for the details of these versions.
Added¶
- GitHub Actions CI/CD pipelines for code quality, building, and PyPI releases
- MkDocs Material documentation site
- Comprehensive development workflow documentation
- Contributing guidelines and release process documentation
[0.0.1] - 2025¶
Added¶
- Initial release of Commol
- ModelBuilder fluent API for constructing compartment models
- Support for difference equations models
- Compartment states (bins) and stratifications
- Mathematical expression support in transition rates
- Rust-powered simulation engine
- Python API with Pydantic validation
- Basic compartment model support (difference equations)
- Initial conditions and parameter management
- Simulation runner with multiple output formats
Security¶
- Expression validation to prevent code injection
- Input sanitization for mathematical formulas
Version History¶
- Unreleased - Current development version
- 0.10.2 - The engine as the one source of rules: settings, model values, windows and formulas; one seed scheme; results equal by value
- 0.10.1 - Fixes of the audit: stochastic changes from the position, one account in the documentation, read-only CI and pinned tools
- 0.10.0 - One way for equivalent things: setting names, seeds and starts, result shapes, error types, the quantile and logging
- 0.9.0 - Control over the work and the record: threads and levels, swarm stop rules, truncated chaos bases, ratios, seeds and build info
- 0.8.0 - Comparing runs: scenarios, quantities of a series, figures as methods and runs on the same panels
- 0.7.0 - Reading a run through objects: the window grid and dates on the axis, series by output name and reports as methods
- 0.6.1 - Fixes of 0.6.0: checks at build, messages of infeasible draws, PRCC with NaN, quantity labels and starts, plots
- 0.6.0 - Uncertainty methods with a model of the data: likelihood, posteriors, local approximations, conformal intervals, profiles and sensitivity analysis
- 0.5.0 - Uncertainty by resampling the data, identifiability of parameters and the removal of the ensemble calibration
- 0.4.0 - One process under several structures and engines, stochastic engines and model descriptions
- 0.3.0 - ODE engine, positions on the axis and one interface for every model kind
- 0.2.0 - Per-compartment initial conditions and engine-planned transitions
- 0.1.0 to 0.1.3 - First 0.1 versions
- 0.0.1 - Initial release with core functionality
For detailed information about each release, see the GitHub Releases page.