Skip to main content

macrocosim (Python)

Python integration-testing client for the macrocosim microgrid simulator.

Build a topology, launch the simulator, drive its environment, inject faults, and assert on the resulting grid state — from inside pytest, talking to it the same way a downstream control app would (the gRPC Microgrid API + the HTTP /api control plane).

from datetime import timedelta
from frequenz.quantities import Energy, Percentage, Power
import macrocosim as mc

async def test_limiter_holds_import_cap():
    bat = mc.battery(id=4, capacity=Energy.from_kilowatt_hours(92),
                     initial_soc=Percentage.from_percent(50))
    inv = mc.battery_inverter(id=3, successors=[bat],
        rated=(Power.from_kilowatts(-5), Power.from_kilowatts(5)))
    mg  = mc.Microgrid(id=1,
        topology=mc.grid(id=1, successors=[mc.meter(id=2, successors=[inv])]))

    with mc.launch(mg) as site:
        site.component(inv).status(health=mc.Health.ERROR)            # fault injection
        await site.component(inv).expect.active_power(
            approx=Power.from_watts(0), tol=Power.from_watts(100))
        await site.component(bat).expect.soc(
            within=(Percentage.from_percent(45), Percentage.from_percent(55)))

Install

Released as a platform wheel that bundles the macrocosim + macroctl binaries (built from the Rust crate via maturin), so a downstream install just works — no separate binary to fetch:

pip install 'macrocosim[grpc]'      # or: uv add 'macrocosim[grpc]'

launch() finds the bundled binaries automatically (in the interpreter's scripts directory, even off PATH). Override with MACROCOSIM_BIN / MACROCTL_BIN, or bin= on launch, to point at a local build.

Development

This package is uv-native; the build backend is maturin, so uv sync compiles the binaries from the repo's Rust crate (needs the Rust toolchain + git submodule update --init).

uv sync                       # venv from uv.lock; builds the binaries
uv run pytest                 # tests; ruff / ty also via `uv run`

Point tests at a fast local cargo build instead of the wheel's binary:

MACROCOSIM_BIN=../target/debug/macrocosim uv run pytest

The surface

Build a topology — the spec is the graph (nested successors), rendering 1:1 to macrocosim's (make-*) Lisp:

from frequenz.quantities import Energy, Percentage, Power

P = Power.from_kilowatts
mg = mc.Microgrid(id=1, topology=mc.grid(id=1, successors=[
    mc.meter(id=2, successors=[
        mc.battery_inverter(id=3, rated=(P(-5), P(5)), successors=[
            mc.battery(id=4, capacity=Energy.from_kilowatt_hours(100),
                       initial_soc=Percentage.from_percent(50))]),
        mc.solar_inverter(id=5, sunlight=Percentage.from_percent(80)),
        mc.meter(id=6, power=Power.from_watts(1000))])]))

Constructors: grid, meter, battery_inverter, solar_inverter, battery, ev_charger, chp, steam_boiler. Kwargs mirror the plist keys (snake_case:kebab-case): rated=(lo, hi) Power bounds, capacity an Energy, initial_soc / sunlight a Percentage; mc.raw("(lambda () …)") splices Lisp.

Typed throughout — no bare numbers or unit strings. Knobs take enums (mc.Health, mc.CommandMode, mc.TelemetryMode, scenario mc.Metric, mc.Schedule); quantities are frequenz-quantities (Power, Energy, Percentage, Frequency), imported from it directly; times are datetime (timedelta, or a datetime.time for an absolute scenario clock). mc.to_lisp_atom(v) shows the Lisp literal any of them emits. Or use an existing config: mc.launch("topology.lisp").

Launch and get a Site (a context manager; tears the process down):

with mc.launch(mg) as site:
    site.grpc          # first microgrid's gRPC address ("host:port")
    site.grpc_url      # ... as a "grpc://host:port" URL a client connects with
    site.eval("(...)") # raw Lisp escape hatch

Read — component telemetry over gRPC (what the app sees), aggregates over the graph-derived formulas:

site.active_power(3)      # Power | None       (component, gRPC)
site.soc(4)               # Percentage | None  (battery SoC, gRPC)
site.grid_power()         # Power | None       (/ pv_power() / consumer_power() / …)

Energy — the simulator integrates each power aggregate into a cumulative energy stream (server-side), so you can judge what an app did over a run:

site.grid_energy()        # Energy | None  (cumulative, import positive)
site.battery_energy()     # Energy | None  (/ consumer_energy() / pv_energy())

Assert on them through expect (a one-shot check — energy accumulates, so it isn't a settling value to poll):

await site.expect.grid_energy(max=Energy.from_kilowatt_hours(15))   # held import down
await site.expect.battery_energy(approx=Energy.from_kilowatt_hours(-8),
                                 tol=Energy.from_kilowatt_hours(1))  # discharge total

Per-component energy is a first-class metric too, assertable from Lisp and the scenario framework: (check "15m" :component 2 :metric 'energy :max 15000.0) or metric=mc.Metric.ENERGY on a Python Scenario.check.

Mutate — reach a component with site[id] (or site.component(id)), then act by intent:

inv = site[3]
inv.command(active_power=Power.from_kilowatts(2),      # app command (gRPC gateway)
            lifetime=timedelta(seconds=30))
inv.command(bounds=(Power.from_kilowatts(-1),
                    Power.from_kilowatts(1)))           # narrow the envelope
inv.status(health=mc.Health.ERROR)                       # inject a fault
site[6].drive(power=Power.from_megawatts(2))            # drive the environment
site[5].drive(sunlight=Percentage.from_percent(30))

command goes through the real gRPC gateway, so an out-of-envelope value raises mc.SetpointRejected (the production behaviour under test); status / drive are test-side stimuli.

Assert — settle-aware expect, on a component (site[id].expect) or a microgrid aggregate (site.expect). The settle-aware assertions are async (they await between polls, so an app under test on the same event loop keeps running); the cumulative-energy ones are one-shot but async too, for a uniform surface:

await site[3].expect.active_power(
    approx=Power.from_kilowatts(2), tol=Power.from_watts(300),
    timeout=timedelta(seconds=15))
await site.expect.grid_power(
    max=Power.from_megawatts(1), for_=timedelta(seconds=30))
await site[4].expect.soc(
    within=(Percentage.from_percent(45), Percentage.from_percent(55)))

await expect.<metric>(…) polls until the matcher holds; pass for_= to require it on every sample across a duration instead. Matchers: approx+tol, within, max, min.

Async core (v2): signalsmacrocosim.aio is the async-native core: every read, write, and wait is a coroutine on your event loop (no background threads). Its unit is the signal: once aio.launch binds the topology, the builder objects are the live handles, and every observable quantity is an object with up to three verbs — read (returns the plain quantity; raises NoSample), expect (takes one typed matcher), and set where the simulator allows it. Capability lives in the type:

load = mc.meter(id=5, power=Power.zero())
bat = mc.battery(id=4, capacity=Energy.from_kilowatt_hours(100),
                 initial_soc=Percentage.from_percent(60))

async with mc.aio.launch(mg) as site:
    await load.power.set(Power.from_kilowatts(20))       # drive the world
    await site.grid_power.expect(mc.at_most(Power.from_kilowatts(13)))
    await bat.soc.set(Percentage.from_percent(11))       # teleport state
    await bat.soc.expect(mc.between(Percentage.from_percent(10),
                                    Percentage.from_percent(12)))
    stored = await bat.stored_energy.read()              # state (SoC×capacity)
    await site.battery_energy.expect(                    # flow (∫ battery_power)
        mc.at_most(Energy.from_watt_hours(-1)))
    await inv.health.set(mc.Health.ERROR)                # fault injection

Matchers: near(x, tol=…), between(lo, hi), at_most(x), at_least(x) — one per expect, typed by the signal's quantity. An inverter's power has no .set (commanding it is site[inv].command() through the real gateway), and a cumulative signal's expect has no hold_for — the distinctions are in the types, not runtime errors. Every *_energy site aggregate is the integral of its *_power; energy stored in a battery is bat.stored_energy. Stimuli go over typed JSON control endpoints; rejections raise ControlRejected. See ../docs/python-api-redesign.org for the design.

Scenarios — author in Python, or run a registered Lisp scenario:

scn = mc.Scenario("cloud-fade", length=timedelta(minutes=4))
scn.at(timedelta(seconds=30), pv.sunlight, Percentage.from_percent(20))
scn.check(timedelta(seconds=110), inv.power,
          mc.near(Power.from_megawatts(1.5), tol=Power.from_kilowatts(300)))
site.define_scenario(scn).run(wait=True).assert_passed()

# deterministic, serverless gate (no app under test):
mc.run_scenario_stepped([mg, scn], "cloud-fade")

pytest — the plugin auto-loads; provide a macrocosim_config fixture:

@pytest.fixture
def macrocosim_config():
    return mg

async def test_grid_holds(macrocosim):
    await macrocosim.expect.grid_power(
        approx=Power.from_kilowatts(7), tol=Power.from_watts(500))

@pytest.mark.macrocosim_scenario("cloud-fade")   # runs + gates after the test
def test_scenario(macrocosim): ...

The expect assertions are async, so awaiting them needs pytest-asyncio (installed with the grpc extra) and asyncio_mode = "auto" in your pytest config — otherwise an async def test is collected but never awaited, and the assertion silently never runs.

Status

Early but functional end to end — see todo.org §Y in the macrocosim repo for the design and roadmap. Building, launching, reading and mutating are synchronous; the settle-aware expect assertions are async (so they compose with an app under test on the same event loop). Runnable examples/ cover each piece; examples/pytest_demo/ is a live suite.

License

The Python package is MIT. The wheel also bundles the macrocosim and macroctl binaries, which are GPL-3.0-only; their source is the macrocosim repository at the release's git tag: v0.1.0 for wheel version 0.1.0, and for a pre-release the semver form of the PEP 440 version, so v0.1.0-alpha.1 for 0.1.0a1.

Download files

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

Source Distribution

macrocosim-0.1.0.tar.gz (2.5 MB view details)

Uploaded Source

Built Distributions

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

macrocosim-0.1.0-py3-none-win_amd64.whl (9.7 MB view details)

Uploaded Python 3Windows x86-64

macrocosim-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (10.9 MB view details)

Uploaded Python 3manylinux: glibc 2.17+ x86-64

macrocosim-0.1.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (11.0 MB view details)

Uploaded Python 3manylinux: glibc 2.17+ ARM64

macrocosim-0.1.0-py3-none-macosx_11_0_arm64.whl (10.2 MB view details)

Uploaded Python 3macOS 11.0+ ARM64

macrocosim-0.1.0-py3-none-macosx_10_12_x86_64.whl (10.6 MB view details)

Uploaded Python 3macOS 10.12+ x86-64

File details

Details for the file macrocosim-0.1.0.tar.gz.

File metadata

  • Download URL: macrocosim-0.1.0.tar.gz
  • Upload date:
  • Size: 2.5 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for macrocosim-0.1.0.tar.gz
Algorithm Hash digest
SHA256 1fa059174e18a1a57fd6f4fafe5c61717255a29184081afea2a63e6dc0cf681f
MD5 d436e0f086481d71f0df7ae5f222e3c1
BLAKE2b-256 aa163d093ea3abb7378763e311b406a86c629a2b1adad24636c3c99e151eb59a

See more details on using hashes here.

Provenance

The following attestation bundles were made for macrocosim-0.1.0.tar.gz:

Publisher: release.yml on shsms/macrocosim

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file macrocosim-0.1.0-py3-none-win_amd64.whl.

File metadata

  • Download URL: macrocosim-0.1.0-py3-none-win_amd64.whl
  • Upload date:
  • Size: 9.7 MB
  • Tags: Python 3, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for macrocosim-0.1.0-py3-none-win_amd64.whl
Algorithm Hash digest
SHA256 c987e11687737e8b220321948a13f7509498937dd689bfdfa1307f51527a848b
MD5 b2fdec20d4b22f5d99623898958c326e
BLAKE2b-256 2a6fce0ed657fbc9d13f2797e9508d0a01fdd559a5def53bac6ddbb3899caf25

See more details on using hashes here.

Provenance

The following attestation bundles were made for macrocosim-0.1.0-py3-none-win_amd64.whl:

Publisher: release.yml on shsms/macrocosim

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file macrocosim-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for macrocosim-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 5ff38ca63bac9a40d95bbef938b769dc275ade52e784c5757772a4bb3001e5c6
MD5 130b64f7fdc4dd0e194bfe4dcf940ff7
BLAKE2b-256 25c4c3e14c112da2b8776506226eca619e6167a3e91dccf49eced9c05321407c

See more details on using hashes here.

Provenance

The following attestation bundles were made for macrocosim-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: release.yml on shsms/macrocosim

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file macrocosim-0.1.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for macrocosim-0.1.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 b0f42af0271b611b9498a3baf5c8aeb4e12f28a46d0dd51fe6c6e5b99a44d3fc
MD5 34e35cdc286367c13438e8ace0ef322a
BLAKE2b-256 c33da0e97d0a1cb3c917e8cdec88526c88af5551a31ca87071d608a1cdf1107c

See more details on using hashes here.

Provenance

The following attestation bundles were made for macrocosim-0.1.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:

Publisher: release.yml on shsms/macrocosim

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file macrocosim-0.1.0-py3-none-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for macrocosim-0.1.0-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 39ea521c541831e977bfec040520906038aa060a3d8bf21b06b2f4f5c69d6c70
MD5 29b74ef6f515c8d6c7718af8eb962cd2
BLAKE2b-256 a5c7c9b49df57bea742d8f6f3da74f4a73cd73d78861f0db8865411ef61b511e

See more details on using hashes here.

Provenance

The following attestation bundles were made for macrocosim-0.1.0-py3-none-macosx_11_0_arm64.whl:

Publisher: release.yml on shsms/macrocosim

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file macrocosim-0.1.0-py3-none-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for macrocosim-0.1.0-py3-none-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 eed0f92ebb10dba2f9f1f8d5cb4ee518183781022d8b09301f08f178e3830310
MD5 7305a0cf225e5018c55802cbf1b366db
BLAKE2b-256 41fce083fb79391073d56b5421f7bdd37e723abd5926a6d596e953671dafe376

See more details on using hashes here.

Provenance

The following attestation bundles were made for macrocosim-0.1.0-py3-none-macosx_10_12_x86_64.whl:

Publisher: release.yml on shsms/macrocosim

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.1.0 This release

6 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page