Civic-Digital-Twins Modeling Framework
This repository contains a Python package implementing a Civic-Digital-Twins modeling framework. The framework is designed to support defining digital twins models and evaluating them in simulated environments with varying contextual conditions. We develop this package at @fbk-most, a research unit at Fondazione Bruno Kessler.
Note: this package is currently in an early development stage.
Conceptual Overview
The framework is organised in three layers.
Engine layer
The engine (civic_digital_twins.dt_model.engine) is an embedded DSL
compiler. The programmer builds a computation graph (DAG) by composing
typed nodes — constants, placeholders, and operations — using ordinary Python
expressions. The graph is then linearised by topological sorting and
evaluated by a NumPy-based interpreter that maps each node to the
corresponding numpy operation.
import numpy as np
from civic_digital_twins.dt_model.engine.frontend import graph, linearize
from civic_digital_twins.dt_model.engine.numpybackend import executor
a = graph.placeholder("a")
b = graph.placeholder("b")
c = a * 2 + b
state = executor.State(values={a: np.asarray(3.0), b: np.asarray(1.0)})
executor.evaluate_nodes(state, *linearize.forest(c))
print(state.get_node_value(c)) # 7.0
See docs/design/dd-cdt-engine.md for a full description of the engine.
Model layer
The model layer (civic_digital_twins.dt_model.model) provides typed
building blocks for defining a digital-twin model on top of the engine:
Index/TimeseriesIndex— named wrappers around graph nodes. An index can be a constant, a distribution (sampled at evaluation time), or a formula.Model— a typed computation unit. Use the@definedecorator to declare aModelsubclass via acompute()method;@inputs,@outputs, and@exposedecorators mark the contractual interface. Sub-models are wired via constructor arguments incompute(), producing a composable pipeline.ModelVariant— selects among pre-constructedModelimplementations sharing the same I/O contract. The active variant is resolved by a string key (static) or aCategoricalIndex/graph node (runtime dispatch).
from scipy import stats
from civic_digital_twins.dt_model import DistributionIndex, Index, Model, define, inputs, outputs
@define("example")
class ExampleModel(Model):
@inputs
class Inputs:
x: DistributionIndex
y: DistributionIndex
@outputs
class Outputs:
result: Index
def compute(self, inputs: Inputs) -> Outputs:
result = Index("result", inputs.x + inputs.y)
return ExampleModel.Outputs(result=result)
model = ExampleModel(inputs=ExampleModel.Inputs(
x=DistributionIndex("x", stats.uniform, {"loc": 0.0, "scale": 1.0}),
y=DistributionIndex("y", stats.uniform, {"loc": 0.0, "scale": 1.0}),
))
See docs/design/dd-cdt-model.md for the full
reference: index types, Model API, ModelVariant, and the domain modeling
pattern; see docs/design/dd-cdt-modularity.md
for multi-model composition and decomposition patterns.
Simulation layer
The simulation layer (civic_digital_twins.dt_model.simulation) runs a
model against one or more scenarios:
Scenario— wraps a model with optional value overrides and parameter axes; the canonical first argument toEvaluationand all ensemble classes.Evaluation— evaluates a model over a sequence of weighted scenarios, each of which maps every abstract index to a concrete value, and returns anEvaluationResult.Ensemble/WeightedScenario— a protocol and type alias that define the scenario contract consumed byEvaluation; concrete implementations (DistributionEnsemble,CrossProductEnsemble, …) draw or enumerate scenarios.
See docs/design/dd-cdt-simulation.md for
the full reference: Scenario, ensembles, Evaluation, EvaluationResult,
EvaluationHandle, and ModelEvaluator — the higher-level runner used by
the worked examples (see Usage patterns below).
Besides the three layer subpackages, civic_digital_twins.dt_model hosts two
top-level modules (axes, graph) with narrowly-scoped, deliberate roles —
see civic_digital_twins/dt_model/README.md
for the package-layout policy.
Usage patterns
The examples/ directory contains two worked examples, both driven through
a domain-specific ModelEvaluator subclass
(civic_digital_twins.dt_model.simulation.runner) rather than calling
Evaluation directly: evaluator.evaluate(Scenario(model, ...), EvaluationConfig(...)) runs the engine internally and returns a
domain-specific ModelOutput — a JSON-serialisable, optionally resumable
summary — rather than a raw EvaluationResult. Both use the
@define/compute() API (@inputs, @outputs, @expose, ModelVariant) —
see docs/design/dd-cdt-modularity.md.
They differ in whether the model has context variables: categorical
scenario factors outside the modeller's control (e.g. season, weather),
as opposed to DistributionIndex parameters, which represent uncertainty
the modeller chooses to sample directly.
Direct pattern (examples/mobility_bologna/) — no context variables,
only DistributionIndex parameters. DistributionEnsemble draws S
Monte-Carlo samples to produce weighted scenarios.
Context-variable pattern (examples/overtourism_molveno/) — the model
has categorical context variables (season, weather, …), expressed as
CategoricalIndex, and quantities whose distribution depends on that
context, expressed as ConditionalDistributionIndex. Internally,
CrossProductEnsemble enumerates the context combinations into weighted
scenarios, and presence quantities are swept over a multi-dimensional grid
via Evaluation.evaluate(parameters={pv: array, …}).
Installation
The package name is civic-digital-twins on PyPi. Install
using pip:
pip install civic-digital-twins
or, using uv:
uv add civic-digital-twins
The main package name is civic_digital_twins:
import civic_digital_twins
or
from civic_digital_twins import dt_model
Minimum Python Version
Python 3.12. Tested against Python 3.12, 3.13, and 3.14.
API Stability Guarantees
The package is currently in an early development stage. We do not anticipate breaking APIs without a good reason to do so, yet, breaking changes may occur from time to time. We generally expect subpackages within the top-level package to change more frequently.
Development Setup
We use uv for managing the development environment.
To get started, run:
git clone https://github.com/fbk-most/civic-digital-twins
cd civic-digital-twins
uv venv
source .venv/bin/activate
uv sync --dev
We use pytest for testing. To run tests use this command (from inside the virtual environment):
pytest
Pull requests are automatically tested using GitHub Actions. PRs targeting
dev run the fast CI (dev) workflow
(format, lint, type-check, tests on Python 3.12). PRs targeting main run
the full CI (release) workflow (all
Python versions, doc examples, domain examples, SPDX check, dependency
audit, and build smoke test).
Updating Dependencies
uv self update
uv sync --upgrade
Development model
This project follows a simplified GitHub Flow with an explicit dev branch:
feature/* ──PR─▶ dev ──PR─▶ main ──tag─▶ PyPI
(CI dev) (CI release) (publish)
- Feature work happens on short-lived branches cut from
dev. devis the integration branch. It always carries a+devversion marker (e.g.0.11.0+dev).maincontains only released commits. Mergingdevintomainis always immediately followed by a version tag and a PyPI release.
Releasing
Step 1 — Merging a feature PR into dev
Before opening the PR, verify locally:
- Tests pass:
uv run pytest - Format, lint, and type-check pass:
uv run ruff format --check .,uv run ruff check .,uv run pyright CHANGELOG.md[Unreleased]section updated (Added / Changed / Removed / Fixed; breaking changes flagged).- Design docs (
docs/design/) updated if public API or architecture changed. - SPDX licence header present on any new
.pyor.mdfile.
Open the PR targeting dev. The CI (dev) workflow runs automatically; merge
once it is green.
Step 2 — Preparing a release (promoting dev to main)
Perform the following steps on the dev branch before opening the
dev → main PR:
-
Set the final version in
pyproject.toml(remove the+devsuffix):version = "<version>"
-
Regenerate the lockfile:
uv lock -
Update
CHANGELOG.md: promote[Unreleased]to[<version>] - <date>and add the corresponding comparison link at the bottom. -
Check that documentation
Last-Updateddates are in sync with actual commit dates:git log -1 --format="%ai" -- docs/design/dd-cdt-engine.md git log -1 --format="%ai" -- docs/design/dd-cdt-model.md git log -1 --format="%ai" -- docs/design/dd-cdt-modularity.md git log -1 --format="%ai" -- docs/design/dd-cdt-simulation.md git log -1 --format="%ai" -- docs/getting-started.md
Update any
Last-Updatedfields that are out of date. -
Verify that the runnable doc scripts are in sync with the documentation and execute without errors (also enforced by
CI (release)):uv run python examples/doc/doc_engine.py uv run python examples/doc/doc_model.py uv run python examples/doc/doc_modularity.py uv run python examples/doc/doc_simulation.py uv run python examples/doc/doc_getting_started.py uv run python examples/doc/doc_overtourism_getting_started.py uv run python examples/doc/doc_readme.py
-
Verify that the full domain examples run end-to-end without errors (also enforced by
CI (release); output images are written toexamples/*/output/):uv run python examples/mobility_bologna/mobility_bologna.py uv run python examples/overtourism_molveno/overtourism_molveno.py
-
Verify that every tracked Python and Markdown file carries an SPDX header (also enforced by
CI (release)):# Python files — should print nothing git ls-files '*.py' | xargs grep -rL "SPDX-License-Identifier" # Markdown files — should print nothing git ls-files '*.md' | xargs grep -rL "SPDX-License-Identifier"
Add
# SPDX-License-Identifier: Apache-2.0(Python) or<!-- SPDX-License-Identifier: Apache-2.0 -->(Markdown) to any file that is missing the header. -
Commit the release preparation:
git add pyproject.toml uv.lock CHANGELOG.md docs/ git commit -m "chore: prepare v<version> release" git push origin dev
Open the PR from dev to main. The CI (release) workflow runs the full
verification suite automatically (all Python versions, doc examples, domain
examples, SPDX headers, dependency audit, build smoke test). Merge once it
is green.
Step 3 — Tagging and publishing
After the dev → main PR is merged:
git checkout main && git pull
git tag v<version> && git push origin main v<version>
Go to the repository's Releases page, review the auto-created draft, write
release notes, and click Publish release. This triggers the
publish.yml workflow, which builds the
sdist + wheel, runs twine check, and publishes to PyPI via OIDC — no manual
build or upload step is needed.
Step 4 — Post-release: bump dev back to development
After the release is published, switch back to dev and prepare it for the
next development cycle:
git checkout dev && git pull
Edit pyproject.toml to bump to the next planned version with the +dev
marker:
version = "<next-version>+dev"
Then:
uv lock
Add a fresh [Unreleased] section at the top of CHANGELOG.md:
## [Unreleased]
Commit and push:
git add pyproject.toml uv.lock CHANGELOG.md
git commit -m "chore: start v<next-version> development"
git push origin dev
Hotfix and backport releases
Two situations don't fit the normal dev → main flow above:
- Hotfix — a bug needs fixing in the version
maincurrently sits at, whiledevhas independently moved on to unfinished next-version work that isn't ready to ship. Branch directly offmain's current tip. - Backport — a bug needs fixing in an older, already-superseded
release line (
mainhas moved past it by one or more versions). Branch off the old release tag instead.
Both use a dedicated hotfix/vX.Y.Z or backport/vX.Y.Z branch as a
short-lived staging branch: individual pieces of work are PR'd and merged
into it (never committed to directly), then it is either merged onward
(hotfix) or tagged and released directly (backport).
Hotfix procedure
git checkout -b hotfix/v<version> origin/main, push it:git push -u origin hotfix/v<version>.- For each piece of work (bug fixes, release prep), branch off
hotfix/v<version>, PR into it, merge.CI (release)runs automatically on these PRs (see below). - Once
hotfix/v<version>contains everything for the release, open a PR from it intomain— this is the actual release PR (version bump, changelog, etc. is typically already in place from step 2) and triggersCI (release)normally, exactly like adev → mainPR. - Tag and publish from
main's new tip, exactly as in Step 3 above. - Forward-port the same fix into
devas its own small PR, so the next regular release doesn't reintroduce it. Don't touchdev's in-progress work otherwise.
Backport procedure
git checkout -b backport/v<version> v<old-version>, push it.- For each piece of work, branch off
backport/v<version>, PR into it, merge — same as the hotfix flow. - Tag and publish directly from
backport/v<version>'s tip. This branch never merges intomainordev— their code has diverged too far for that to be meaningful. On the Releases page, review the draft and explicitly set "Set as the latest release" to No before publishing, so the repository's "latest" designation stays on the current release. - Separately, port the same fix forward into whichever line is actually
current (typically as a hotfix per above, or folded into normal
devwork).
CI on maintenance branches
CI (release) (.github/workflows/ci-release.yml) triggers on
push/pull_request against main, hotfix/**, and backport/**, plus
workflow_dispatch for manual runs. A brand-new hotfix/*/backport/*
branch created straight from a historical tag may carry an older snapshot
of the CI workflow that lacks this pattern — add it (and a
workflow_dispatch: {} fallback) as part of setting up the branch.
The pattern trigger only fires once a branch's own committed copy of the workflow already includes it, so the very first PR into a brand-new branch (before that trigger exists there) won't auto-fire. Verify it manually instead, dispatching against the PR's source branch directly (this works even if the target branch doesn't have the trigger yet):
gh workflow run <workflow-file> --ref <branch-name>
One-time setup
PyPI Trusted Publisher: must be configured before the first release. See the PyPI Trusted Publishers documentation.
Branch protection: configure GitHub Rulesets (Settings → Rules → Rulesets) to require
CI (dev)to pass before merging intodev, and allCI (release)jobs to pass before merging intomain. Direct pushes tomainshould be blocked; maintainers should be allowed to bypassdevprotection for post-release bump commits.
Documentation
| Document | Description |
|---|---|
| Getting Started | Step-by-step guide: define a model with @define/compute(), sample with DistributionEnsemble, evaluate with Evaluation. |
| dd-cdt-engine.md | DSL compiler engine — graph nodes, topological sorting, NumPy executor. |
| dd-cdt-model.md | Model layer reference — index types, @define/compute(), Model, Evaluation, EvaluationResult, and the domain modeling pattern. |
| dd-cdt-modularity.md | Model modularity concept guide — @define/compute(), ModelVariant, decomposition patterns, and Bologna worked example. |
| dd-cdt-simulation.md | Simulation guide — Scenario, CrossProductEnsemble, EvaluationHandle, incremental evaluation, ModelEvaluator. |
License
SPDX-License-Identifier: Apache-2.0
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file civic_digital_twins-0.11.1.tar.gz.
File metadata
- Download URL: civic_digital_twins-0.11.1.tar.gz
- Upload date:
- Size: 426.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8e36e3b565577b1680634f29bf63b99aa60c544a587394c8a8367e87c10fa6b8
|
|
| MD5 |
2318c7aebb37c447a589d31efea0dc84
|
|
| BLAKE2b-256 |
32972f023deefa73cfb8be7f61acd68aa05e5bdb5df1a8fcb05c6192cb39c08a
|
Provenance
The following attestation bundles were made for civic_digital_twins-0.11.1.tar.gz:
Publisher:
publish.yml on fbk-most/civic-digital-twins
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
civic_digital_twins-0.11.1.tar.gz -
Subject digest:
8e36e3b565577b1680634f29bf63b99aa60c544a587394c8a8367e87c10fa6b8 - Sigstore transparency entry: 2533918713
- Sigstore integration time:
-
Permalink:
fbk-most/civic-digital-twins@8f43bc0116ac6f5ebef2b44576d5b7cba597593f -
Branch / Tag:
refs/tags/v0.11.1 - Owner: https://github.com/fbk-most
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@8f43bc0116ac6f5ebef2b44576d5b7cba597593f -
Trigger Event:
release
-
Statement type:
File details
Details for the file civic_digital_twins-0.11.1-py3-none-any.whl.
File metadata
- Download URL: civic_digital_twins-0.11.1-py3-none-any.whl
- Upload date:
- Size: 143.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0106c778f0e5d98cc2af398be49ecce9d5bceaec170800a14a08ed797c1a32be
|
|
| MD5 |
d3bcd48042dc145a29a25c5a6336df21
|
|
| BLAKE2b-256 |
0facb657fe08aa0fad9cf1f44372a42e28920966ed1f59294a30a1c7d7393782
|
Provenance
The following attestation bundles were made for civic_digital_twins-0.11.1-py3-none-any.whl:
Publisher:
publish.yml on fbk-most/civic-digital-twins
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
civic_digital_twins-0.11.1-py3-none-any.whl -
Subject digest:
0106c778f0e5d98cc2af398be49ecce9d5bceaec170800a14a08ed797c1a32be - Sigstore transparency entry: 2533918836
- Sigstore integration time:
-
Permalink:
fbk-most/civic-digital-twins@8f43bc0116ac6f5ebef2b44576d5b7cba597593f -
Branch / Tag:
refs/tags/v0.11.1 - Owner: https://github.com/fbk-most
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@8f43bc0116ac6f5ebef2b44576d5b7cba597593f -
Trigger Event:
release
-
Statement type: