Repository navigation
Modelly 1.4.0
The scale-out release: split runs across processes, a GPU backend, differentiable models,
live data and assimilation, fields and ODEs coupled to agents, emulators and history matching,
synthetic populations, a static export that runs a model in the browser, and Studio II. No
playbook is slower than 1.3.3 and every output is identical; the event-heavy wolf-sheep example
runs at 0.95, event scheduling at 0.70 and column data collection at 0.47 of 1.3.3's time
(docs/performance.md). Hosted CI now runs only the Release and docs workflows; every other
check runs locally (scripts/release-checks.sh), on Linux x86-64.
Added
-
Per-agent random streams (provisional): with
agent_streams = Trueon a model class,
agent.randomandagent.rngare generators of the agent's own, seeded from the model's
seed andAgent.stream_key()(unique_idby default). An agent draws the same numbers
whatever the activation order or the population, and checkpoints continue every stream.
Off by default; models without the flag draw exactly as before. -
modelly.distributed.partition(provisional):grid_blocks(rows, cols).layout(grid)cuts a
2-D grid into balanced blocks and gives each cell's owner and each block's halo, on plain
grids and tori. -
Split runs (
modelly.distributed, provisional): a model that declares
partitionable = Partitionable(space=..., phases=...)and uses per-agent streams is built
split across workers withCluster(backend=...).build(Model, scenario, partition), with ghost
agents exchanged at partition borders before a phase that looks across them and agents
migrating with their streams after a phase that moves them. Backendslocal,mpi
(mpi4py, newdistributedextra),daskandray(imported only when used). Results equal
one process for the same seed, byte for byte; a model that breaks the contract (model-level
draws in a phase, Column agents, own events, moves into cells with a capacity, agent
references in attributes, births keyed onunique_id) is refused with the reason, and
owns()lets a model build only its partition's agents. -
Graph and k-d partitioners (provisional):
partition.graph(parts)splits aNetworkby
breadth-first banding and greedy boundary refinement,partition.kd(parts)a
ContinuousSpaceby recursive bisection at the population quantile; both deterministic,
pure numpy, with halos by hops or by distance. -
Distributed collectors and checkpoints (provisional):
Sum,MinandMaxreporters reduced
across partitions (collect()), agentsnapshot()s in stream-key order, and
checkpoint(directory)writing one file per partition plus anindex.jsonwith digests,
whichCluster.restorecontinues draw for draw. -
Partition-independence check (provisional):
check_independence(Model, scenario, partitions, steps)runs one process and each split and raises at the first differing reporter or agent
state; the test suite runs it on a grid, a network and a continuous model at 1, 2, 4 and 16
partitions, and on MPI with 2 and 4 ranks.benchmarks/distributed/weak_scaling.pymeasures
weak scaling, with the guide "From laptop to cluster" (docs/distributed.md). -
modelly.stream(provisional):ReplaySource,CSVSource(a growing file) and
HTTPSourcehand out observations once each, in time order;ObservationOperatorreads the
same quantities from a model, with residuals and a Gaussian log-likelihood. -
modelly.stream.Ensemble(provisional):nreplicates of one scenario with spawned seeds,
advanced in lock step in this process, with starting values drawn from aprior;bandand
forecastgive the (weighted) mean and quantiles of an operator now and ahead as a
long-format DataFrame (time,quantity,mean,q0.05, ...), for plots and live views. -
modelly.stream.ParticleFilter(provisional): the bootstrap particle filter (Gordon et al.
1993) over an ensemble, with the effective sample size tracked and systematic resampling
(Douc & Cappé 2005) below a threshold; duplicates are checkpoint copies reseeded from derived
keys, so a run is bit-identical for a seed with the compiled or the pure-Python core. The new
nowcasting tutorial (docs/assimilation.md) tracks a synthetic truth with it. -
modelly.stream.EnsembleKalmanFilter(provisional): the stochastic ensemble Kalman filter
with perturbed observations (Evensen 1994, 2003) on named model attributes
(state/parameter augmentation), with optional bounds; deterministic for a seed. -
modelly.stream.MQTTSourceandKafkaSource(provisional): sources reading JSON records from
an MQTT or a Kafka topic, through the new optional extrasmqtt(paho-mqtt) andkafka
(confluent-kafka), or through a client passed in. -
population:
modelly.population, synthetic populations from aggregate tables:ipfand
trs(truncate, replicate, sample) rounding insynthesize_population,marginal_errors
to check the fit,anneal(combinatorial optimisation of whole records by simulated
annealing),assign_householdsfrom a household-size table,locate_householdsand
assign_places(nearest school or workplace with room, within a distance limit), and
contact_network(household, school and work layers of aMultilayerNetwork). No bundled
personal data;modelly.space.geo.ipfandsynthesize_populationkeep working. -
zoo:
age_seir.model.scenario_from_populationstarts the age-structured SEIR model from a
synthetic population (group sizes and contact matrix); the default scenario is unchanged. -
geo:
modelly.space.geo.samples, small real samples of Burlington, Vermont, bundled for
offline use with their licences inNOTICE.md(OpenStreetMap streets, ODbL; ESA WorldCover
land cover, CC BY 4.0; census tracts, public domain), andscripts/geo_data.pyto download
the full sources, verify their SHA-256 and rebuild the samples. -
zoo:
road_traffic, traffic assignment on the OpenStreetMap streets, validated against the
exact edge betweenness. -
zoo:
habitat_selection, Metropolis dispersal over the WorldCover raster, validated against
its stationary law. -
zoo:
schelling_regions, Schelling segregation on the census tracts by contiguity. -
modelly.couple: multi-scale coupling.Fieldmakes a grid property layer diffuse, drift and
decay on its own sub-steps (finite volumes, explicit with a stability check or implicit;
closed, absorbing or periodic boundaries; mass conserved to round-off, with a budget of what
was added, left and decayed);ODEandStockFlowintegrate continuous components with
scipy'ssolve_ivp;CouplingandPhaserun them with the agents' step phases, checked at
build time for reads with no writer, two writers and cycles inside a phase. Coupled models
checkpoint and resume bit for bit. Guide:docs/coupling.md. -
Two coupled zoo models:
nutrient_foragers(foragers on a diffusing nutrient field,
validated against the carrying capacity of the energy balance) andhospital_epidemic(SIR
agents coupled to a hospital-capacity stock-flow ODE, validated against the final-size
relation and the hospital's exact linear solution), both with mass-balance tests. -
modelly.emulateemulators (provisional):GaussianEmulator(one Gaussian process per
output, fitted with restarts, with covariances and conditioning) andNeuralEmulator(a
numpy ensemble of small networks), fitted to unit-cube points or to an experiment's runs
(from_results), and usable in place of the model inmodelly.sensitivity;Simulation
runs a model through the experiment executors (or a function) for several outputs, each
reduced by a statistic, optionally with common random numbers.GaussianProcess.fittakes
restarts=Trueand the process gainscovariance,conditionandnoise_variance; the
default fit is unchanged, bit for bit. -
modelly.emulate.ActiveLearning(provisional): designs that run next where the emulator is
least certain (integrated variance reduction, or the largest variance), chosen in batches by
conditioning on each pick, seeded and resumable. Against Latin hypercubes with the same
emulator (benchmarks/emulate/active_vs_lhs.py), it saves a factor of 1.6 to 2.7
of the runs on the zoo's Bass diffusion (2 or more at 20, 40 and 50+ runs, 1.3 at 30), but
only 1.1 to 1.4 on Branin below 80 runs and 0.8 to 1.4 on Ishigami: the roadmap's factor of 2 holds on
one of the two kinds of benchmark (table indocs/emulators.md). -
modelly.emulate.HistoryMatch(provisional): history matching in waves, with the
implausibility|z - E[f(x)]| / sqrt(Var_emulator + Var_obs + Var_discrepancy), a cutoff of
3, NROY tracked on a fixed sample and emulators refitted inside it every wave. On the zoo's
age-structured SEIR (truth at the default parameters, three outputs), four waves of 20 runs
leave 17 %, 4.4 %, 3.1 % and 2.5 % of the box, the truth inside every time. Guide:
docs/emulators.md. -
gpu:
modelly.gpuruns column phases (DeviceTable), cohort tables (DeviceCohortTable,
binomial thinning included) and cell-space operators (DeviceCellIndex: counts, neighbour
sums, random-neighbour moves) on numpy, CuPy or JAX, chosen byget_backendor
MODELLY_BACKENDwith a numpy fallback;PhiloxStream, a Philox4x32-10 stream keyed by
(seed, stream), draws the same numbers on every backend (Random123 known-answer vectors);
plan_memory/run_batchedrun populations larger than the GPU in batches, bit for bit;
modelly-gpuvariants of the grid epidemic and insect cohorts playbooks. On a laptop
RTX 5050 the grid epidemic at 10 million agents keeps the CPU run's integer state bit for bit
and runs its 60 steps 34x faster than the CPU column variant (docs/performance.md).
Extrasgpu(CuPy) anddiff(JAX);import modellyloads neither. -
diff:
modelly.diffsimulates column phases traced by JAX (DiffModel,@phasewith a
differentiableflag) and differentiates their outputs with respect to parameters:
pathwise normal and uniform draws, Bernoulli choices through straight-through, Gumbel-softmax
or score-function estimators (each warnsGradientApproximationWarning),check_gradients
against finite differences, andGradientCalibration(Adam onmodelly.calibratepriors and
targets, deterministic for a seed). Differentiable twins of the Bass diffusion and
Bouchaud-Mezard zoo models recover known parameters;docs/differentiable.mdsays when the
gradients mean something. -
quality: partition independence in the determinism suite. The three reference models of split
runs (grid_epidemic,network_sir,continuous_sir) are golden examples, and
tests/determinism/test_determinism_partition.pysplits each from the golden seed (2, 4 and
16 partitions) and checks every output equals the single-process run, so a split run on any
platform gives the golden digest. A newmpiworkflow runs the distributed tests under Open
MPI on Ubuntu and macOS and under MS-MPI on Windows (2 ranks only);test_mpi.pyuses
mpiexecon Windows and reads the rank counts fromMODELLY_MPI_RANKS. -
provenance: the language-model response cache travels with the study.
modelly runexports
the replies of the model'smodelly.llm.Clients tollm-responses.jsonl.gzand records it in
the manifest (llm_cache, an optional field of manifest format 1);modelly bundlepacks it
and refuses a run whose replies changed;modelly verifyreruns inside the new
modelly.llm.replay.replaying(path)(provisional), where every client answers from the
recorded replies only, so a bundle verifies with no network and no API key. Runs without a
client write the same manifest and bundle as before. -
plugins: Modelly now reads three more entry-point groups.
modelly.samplersadds design kinds
([design] kind = "name"in an experiment file,designs.design_factoryanddesign_kinds,
provisional);modelly.lint_rulesadds rules tomodelly lint(an object with aRuleand a
check(tree),modelly.lint.plugins, provisional);modelly.studio_viewsadds Studio tabs.
A rule or view that fails to load or reuses a built-in code is skipped with a warning. A
complete third-party package with one entry point in every group is in
examples/plugins/modelly-example-plugin, tested intests/test_example_plugin.py. -
modelly.calibrate.ExternalBayesOpt(provisional): Bayesian optimisation by an outside
library over the same unit box and common random numbers asBayesOpt, through any optimiser
withask()andtell(point, value); the built-in backend is optuna (TPE seeded from the
calibration's seed, or any optuna sampler), from the new optional extracalibrateand
imported only when the search starts. Alsomethod = "external_bayes_opt"in calibration
files. -
calibrate: a zoo model calibrated end to end. ABC-SMC recovers the Bass diffusion model's
innovation and imitation inside their 95 % intervals on three seeds, from priors ten times
wider (tests/calibrate/test_zoo_end_to_end.py, "A zoo model end to end" in
docs/calibration.md). -
sensitivity: Morris stability across seeds on zoo models. The whole ranking of the Bass
diffusion model's three parameters, and the top parameter of the age-structured SEIR model,
are the same on five seeds (tests/sensitivity/test_morris_stability.py); the guide says
which parameters swap places and why. -
docs: design reviews of calibration (E1), sensitivity analysis (E2) and language-model agents
(E7), each what was decided, what else was considered and why (docs/design/). -
quality: WebAssembly in the release matrix. The release workflow's new
pyodidejob installs
the pure wheel with micropip in Pyodide under node, importsmodelly, checks the pure-Python
twins are in use and runs the Boltzmann wealth example (scripts/pyodide-smoke.mjs); the
PyPI upload waits for it. In Pyodideimport modellyneeds Pyodide'ssqlite3package, and
importing any bundled example needsnetworkx. -
quality: SLURM and PBS job scripts run for real.
test_cluster_scripts.pyputs stand-in
sbatchandqsubexecutables onPATHthat run the generated job script inbashonce per
array element, so every element runs in a fresh shell and interpreter and must give the serial
numbers; the newplatformsworkflow'sslurmjob submits the same array to a single-node
SLURM cluster on the runner (MODELLY_REAL_SCHEDULER=slurm). -
quality: a bundle made on Linux verified on macOS and Windows. The
platformsworkflow runs
and bundles three studies on Ubuntu (Boltzmann wealth, Schelling, and the language-model
opinions example, whose replies travel in the bundle), and macOS and Windows jobs unpack each
and runmodelly verify. -
release:
.zenodo.jsonat the repository root, so the GitHub-Zenodo integration archives each
GitHub release with the same title, licence, keywords and creators asCITATION.cff(no
contact data; the version comes from the tag), checked bytests/test_zenodo.py. -
lint: MDL011 (a scipy.stats.qmc engine, a
.rvs()draw or a SALib sampler without a seed),
MDL013 (a mutable default in aScenario, shared by every scenario of the class) and MDL014
(a clock, timer or OS entropy read in a calibrationTarget);--fixseeds a sampler in a
model or agent method fromself.rng. 21 rules, five with a fix. -
modelly lint --lsp: the linter as a language server over stdio (standard library only),
publishing findings as diagnostics on open, change and save, with the safe fix and a
# noqacomment as quick fixes; settings for Neovim, Helix, VS Code and PyCharm in
docs/lint.md. -
.pre-commit-hooks.yaml:modelly-lintandmodelly-lint-fixhooks, so a project's
pre-commit configuration runsmodelly lint(or its safe fixes) on the staged files. -
modelly new --template geo(residents of a town's districts in aGeoSpace) and
--template llm(citizens deciding throughmodelly.llm, offline by default); both pass
their determinism test andmodelly lintas generated. -
DataCollector.to_xarray(andmodelly.data.xarray_export.to_xarray): model reporters as an
xarray.Datasetovertime, arrays such as a grid stacked into(time, ...), agent
reporters as a(time, AgentID)panel; xarray is the new optionalxarrayextra. -
docs: From R and Julia, reading Modelly's Parquet, DuckDB and netCDF results with arrow,
duckdb, Parquet2.jl and DuckDB.jl, and running Modelly from R (reticulate) and Julia
(PythonCall); the Python side of every snippet runs in the test suite. -
modelly netlogo(python -m modelly.netlogo): translate a NetLogo model (.nlogo) into a
runnable, seeded, lint-clean Modelly model for a documented subset (turtles, patches, breeds,
ask,set,if/ifelse,repeat,create-/hatch/sprout, movement,neighbors,
one-of,random, globals; sliders and switches become Scenario fields). Lines outside the
subset stay as# NETLOGOcomments and are listed with their line numbers. Ships Fire and
Wolf Sheep Predation sources written from the published descriptions; the translated Fire
model shows the percolation threshold near 59% density. Seedocs/netlogo.md. -
Strict type checking:
[tool.mypy]in pyproject.toml runs mypy in strict mode on batch,
bench, calibrate, dev, experiments, lint, llm, netlogo, new, profile, provenance, scenarios,
sensitivity, timetravel, the data exports and the top-level modules (156 files); a newtypes
stage ofscripts/ci-local.shandpython -m modelly.dev gateruns it with installed mypy,
else the pinned mypy throughuvx, and is skipped with a message when neither is available. -
web:
modelly export-web model.py:Model --out site/writes a static site that runs the
model in the browser with Pyodide (pinned 314.0.7, from its CDN): Studio's parameter panel,
run controls, a space view and a reporter plot, the model in a Web Worker, Modelly as a
trimmed pure-Python wheel and only the packages the model imports; every example and zoo
model runs from its export (tests/web/test_browser.py, CI jobbrowser), and the
gallery embeds three live models. A first visit transfers 14 MB (Pyodide, numpy, pandas)
and is ready in 8 s on a 50 Mbit/s line, 21 s on DevTools "Fast 4G"
(tools/web/measure_load.py). Guide:docs/browser.md. -
native: the C modules build for WebAssembly with Emscripten (
pyodide build,
scripts/build-wasm-wheel.sh);modelly export-web --wheelships that wheel. -
Memory-mapped columns (
modelly.core.storage):model.columns.storage = MemoryMapped(dir)
orCohortTable(rng, storage=MemoryMapped(dir), ...)keeps a table's columns in
numpy.memmapfiles, grown in place and deleted byclose(), awithblock or garbage
collection; checkpoints carry the rows and load them into a new folder.
benchmarks/large_columns.pyruns a cohort population of any size on either storage. -
modelly bench --against VERSION: every Modelly playbook variant in this checkout and in a
release built from its tag (same compiler and dependencies), alternating, results checked
identical, with the median and interquartile range of the per-round CPU ratios; a variant the release cannot run (one added since it) is
left out and listed, so the comparison covers what both sides have. -
benchmarks/pgo.py: the compiled modules built with GCC profile-guided optimisation
(trained on the playbooks,benchmarks/pgo_training.py) and timed against -O3 on every
playbook. It measured no faster, so the build does not use it (docs/performance.md). -
Studio Ensemble tab (provisional): replicates of the parameter panel's values, seeds spawned
from the panel's seed, and the mean, median and 5 to 95 % band of every numeric model
reporter across them step by step, asmodelly.streamband frames (CSV download). -
Studio Assimilation tab (provisional):
modelly studio --observations observed.csvruns a
particle filter or an ensemble Kalman filter over the observations and shows the filtered
bands with the data, the effective sample size and every replicate's weight over time, or
the adjusted parameters' mean and spread over time. -
Studio Experiments tab (provisional):
--results DIRbrowses an experiment's results store:
one row per run, filters by parameter (ranges or values), an output against a parameter and
the selected runs step by step;modelly.studio.browser.ExperimentBrowserfrom Python. -
Studio Analysis tab (provisional):
--analysis file.py:function(orsession.analyses)
shows calibration posteriors (marginals, pairs, fits, HDI summary) and sensitivity results
(the method's plot, the ranking). -
Studio Map tab (provisional): the space drawn by WebGL in the browser (agents, network nodes
and edges,GeoSpaceregion outlines), with pan and zoom, falling back to Canvas 2D without
WebGL; the renderer ships inside Modelly. 100 000 agents and a 100 000-edge network draw in
headless Chromium, Firefox and WebKit on Linux; above 10 000 elements the Model tab leaves the
space to it. -
Studio session script (provisional): Export → Session script downloads a Python script that
rebuilds the live run (new runs, steps, replays with changed values and seeds), checked to
give the same model state on six bundled examples. -
Studio bundle export (provisional): Export → Bundle reruns the live run with
modelly run's
recorder and packs it as amodelly bundlezip thatmodelly verifychecks. -
python -m modelly.studio.assets [--fetch](provisional): lists and downloads the front-end
files Solara's page needs into its cache, so Studio runs offline;modelly studiowarns when
some are missing. The browser checks, run locally before a release one browser at a time
(MODELLY_BROWSER=chromium|firefox|webkit), serve the page with the CDN unreachable.
Changed
- The event list's heap is compiled (
modelly/_native/fastevents.c): the same ordering (time,
then priority, then insertion order), the same heap array, a pure-Python twin, and
checkpoints that load in either; time advance no longer pops and re-pushes the first future
event, andschedule_eventbuilds one weak reference instead of two. - The
DataCollector's attribute reporters,AgentSet.getandaggreadColumn
attributes from their table in C instead of calling the Python getter per agent; the rows
are identical. - docs: the build fails on warnings.
scripts/build-docs-version.shpasses-W --keep-goingby
default (SPHINXOPTS=builds an old tag that warns) and the docs workflow sets it. The build
was made warning-free first: a docstring formula read as a substitution, ambiguousGridand
Observationtype references from imports only made for type checking, and the "no lexer"
warnings of the text-notebook tutorials, which appear only when they are not executed and are
filtered inconf.py.
Deprecated
modelly.space.geo.ipfandmodelly.space.geo.synthesize_population(also through
modelly.space.geo.population, with its constants) warn with aModellyDeprecationWarning
and go in 2.0: import them frommodelly.population. They are the same functions, so results
do not change.docs/deprecations.mdgains "Announced for 2.0", every change 2.0 makes with
what to use today and when it starts to warn.
Fixed
- Modelly imports and runs under Pyodide: the profiler falls back to
process_timewhere
thread_timeis missing, the LLM client sends calls one at a time where threads cannot
start,synthesize_populationpassesintpcounts tonp.repeat(32-bit numpy), and
modelly.examplesimports each example on first use instead of all of them.