Skip to main content

A universal orchestrator for dynamic modular simulations.

Project description

๐ŸŒ€ Moirai

A universal orchestrator for dynamic, modular simulations.

Python 3.12+ License: MIT PyPI version

Moirai is a lightweight, extensible Python framework for orchestrating heterogeneous simulations. A central Simulation runtime manages pluggable Module components โ€” each responsible for its own domain logic.

Use it for: agent-based models, ECS systems, weather simulation, economic models, epidemiology, social dynamics, city simulators, power grids, supply chains, and any other dynamic system you can imagine.


โœจ Philosophy

Simulation  โ†’  universal runtime
Module      โ†’  pluggable domain logic
EventBus    โ†’  loose coupling between modules
Metrics     โ†’  observable simulation state
Resources   โ†’  shared data between modules
Clock       โ†’  real-world time mapping
Scheduler   โ†’  deferred & recurring callbacks
Snapshot    โ†’  serialize state to JSON

๐Ÿš€ Quick Start

Installation

pip install moirai-sim

Or from source:

git clone https://github.com/yourname/moirai
cd moirai
pip install -e ".[dev]"

Basic Usage

from moirai import Simulation
from moirai.clock import Clock
from moirai.examples.weather import WeatherModule
from moirai.examples.population import PopulationModule

sim = Simulation(seed=42, clock=Clock(time_step=1.0, unit="day"))

sim.attach(WeatherModule(initial_temp=15.0))
sim.attach(PopulationModule(initial_mood=100.0))

sim.run(365)

print(sim.summary())

๐Ÿงฉ Writing a Module

from moirai.module import Module

class EconomyModule(Module):
    phase    = "economy"   # execution phase (lexicographic order)
    priority = 0           # within-phase ordering (lower = earlier)
    enabled  = True        # set False to skip update() each tick

    def setup(self, sim) -> None:
        sim.resources.set("gdp", 1_000_000.0)

    def update(self, sim) -> None:
        growth = sim.rng.uniform(0.98, 1.03)
        gdp    = sim.resources.get("gdp")
        sim.resources.set("gdp", gdp * growth)
        sim.metrics.track("gdp", sim.resources.get("gdp"))
        sim.eventbus.emit("gdp_updated", value=sim.resources.get("gdp"))

    def teardown(self, sim) -> None:
        pass

๐Ÿ”Œ Event Bus

# Standard subscription
sim.eventbus.subscribe("gdp_updated", lambda value, **_: print(f"GDP: {value:,.0f}"))

# One-shot (auto-unsubscribes after first call)
sim.eventbus.once("run_start", lambda **_: print("Simulation started!"))

# Wildcard โ€” receives every event
sim.eventbus.subscribe("*", lambda **kw: log.debug("event received"))

# Decorator shorthand on the simulation
@sim.on("temperature_changed")
def log_temp(value, **_):
    print(f"[{sim.clock.format_time(sim.ticks)}] Temp: {value:.1f} ยฐC")

โฑ Clock

from datetime import datetime
from moirai.clock import Clock

# 1 tick = 1 day, anchored to a calendar date
clock = Clock(time_step=1.0, unit="day", start_date=datetime(2025, 1, 1))

sim = Simulation(seed=42, clock=clock)
sim.run(90)

print(sim.clock.format_time(sim.ticks))   # "2025-04-01"
print(sim.time)                           # 90.0
print(clock.ticks_for(365))              # 365

๐Ÿ—“ Scheduler

# One-shot callback at tick 10
sim.scheduler.schedule_at(10, lambda: print("Tick 10!"))

# Recurring every 7 ticks (weekly event)
handle = sim.scheduler.schedule_every(7, lambda: print("Weekly checkpoint"))

# Cancel
sim.scheduler.cancel(handle)

๐Ÿ“Š Metrics

# Track values
sim.metrics.track("population", 5_000)

# Query
print(sim.metrics.history("population"))  # full history
print(sim.metrics.latest("population"))   # most recent value

# Statistical summary
s = sim.metrics.summary("population")
print(s.mean, s.std, s.min, s.max, s.count)

# Export to JSON
sim.metrics.export_json("results/metrics.json")

# pandas-compatible dict
import pandas as pd
df = pd.DataFrame(sim.metrics.to_frame())

๐Ÿชฃ Resources

# Set, get, update
sim.resources.set("gdp", 1_000_000.0)
sim.resources.update({"inflation": 0.02, "unemployment": 0.05})

# Type-safe get โ€” raises ResourceTypeError on mismatch
gdp = sim.resources.get_typed("gdp", float)

# set-if-absent
sim.resources.setdefault("mortality_rate", 0.01)

โธ Pause & Resume

class CheckpointModule(Module):
    phase = "control"

    def setup(self, sim): pass
    def teardown(self, sim): pass

    def update(self, sim):
        if sim.ticks == 100:
            sim.pause()   # stops the loop after this tick

sim.attach(CheckpointModule())
ticks_run = sim.run(365)   # returns early at tick 100
print(sim.state)           # SimulationState.PAUSED
sim.run(265)               # resume for remaining ticks

๐Ÿ“ธ Snapshots

sim.run(100)

# Save
snap = sim.snapshot()
snap.save("checkpoint.json")

# Load & inspect
from moirai.snapshot import Snapshot
loaded = Snapshot.load("checkpoint.json")
print(loaded.tick)         # 100
print(loaded.resources)    # {"temperature": ...}
print(loaded.summary())

๐Ÿ”ฌ Built-in Examples

Module Domain Key resources
WeatherModule Climate temperature
PopulationModule Social population_mood
EpidemicModule Epidemiology (SIR) sir_S, sir_I, sir_R
EconomyModule Macroeconomics gdp, inflation, unemployment
from moirai.examples.epidemic import EpidemicModule

sim = Simulation(seed=0)
sim.eventbus.subscribe("epidemic_ended", lambda tick, **_: print(f"Ended at tick {tick}"))
sim.attach(EpidemicModule(population=100_000, initial_infected=10, beta=0.3, gamma=0.1))
sim.run(500)

๐Ÿ“ Project Structure

moirai/
โ”œโ”€โ”€ pyproject.toml
โ”œโ”€โ”€ README.md
โ”œโ”€โ”€ CHANGELOG.md
โ”œโ”€โ”€ src/
โ”‚   โ””โ”€โ”€ moirai/
โ”‚       โ”œโ”€โ”€ __init__.py          # Public API
โ”‚       โ”œโ”€โ”€ simulation.py        # Orchestrator + SimulationState
โ”‚       โ”œโ”€โ”€ module.py            # Abstract Module base
โ”‚       โ”œโ”€โ”€ eventbus.py          # Pub/sub (once, wildcard)
โ”‚       โ”œโ”€โ”€ metrics.py           # Time-series + MetricSummary
โ”‚       โ”œโ”€โ”€ resources.py         # Typed resource container
โ”‚       โ”œโ”€โ”€ clock.py             # Time-unit mapping
โ”‚       โ”œโ”€โ”€ scheduler.py         # Deferred/recurring callbacks
โ”‚       โ”œโ”€โ”€ snapshot.py          # JSON checkpoint
โ”‚       โ”œโ”€โ”€ exceptions.py        # Exception hierarchy
โ”‚       โ”œโ”€โ”€ py.typed             # PEP 561
โ”‚       โ””โ”€โ”€ examples/
โ”‚           โ”œโ”€โ”€ weather.py
โ”‚           โ”œโ”€โ”€ population.py
โ”‚           โ”œโ”€โ”€ epidemic.py      # SIR model
โ”‚           โ””โ”€โ”€ economy.py       # GDP / inflation / unemployment
โ””โ”€โ”€ tests/
    โ”œโ”€โ”€ conftest.py
    โ”œโ”€โ”€ test_simulation.py       # 68 tests
    โ”œโ”€โ”€ test_modules.py          # 85 tests
    โ”œโ”€โ”€ test_clock.py            # 17 tests
    โ”œโ”€โ”€ test_scheduler.py        # 20 tests
    โ””โ”€โ”€ test_snapshot.py         # 15 tests

๐Ÿงช Running Tests

pytest
pytest --cov=moirai --cov-report=term-missing

๐Ÿ“„ License

MIT โ€” see LICENSE for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

moirai_sim-0.3.0.tar.gz (41.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

moirai_sim-0.3.0-py3-none-any.whl (39.9 kB view details)

Uploaded Python 3

File details

Details for the file moirai_sim-0.3.0.tar.gz.

File metadata

  • Download URL: moirai_sim-0.3.0.tar.gz
  • Upload date:
  • Size: 41.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.10

File hashes

Hashes for moirai_sim-0.3.0.tar.gz
Algorithm Hash digest
SHA256 6eab16b182293a11d4d34873531d42d728e1c36c554bc2787d2b7c94e1993962
MD5 95c8094a1709927902b5828a5f40dfe0
BLAKE2b-256 b5330928f0577cb249a66cda60cfe7a3dd1da54e75345b92d1aa59a246c2e2b8

See more details on using hashes here.

File details

Details for the file moirai_sim-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: moirai_sim-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 39.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.10

File hashes

Hashes for moirai_sim-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c181d3b0f2bfe6841557939f2c2b02f4321c6bcc59671f5551ae0d29ee502012
MD5 cfbeb6ed577a8b9c152023f4ad71901f
BLAKE2b-256 dbc8c6a07aeeeeb4cb173cd5c847ab00d5427db5e02d0fb6df7da23d41c45fa5

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page