diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 0234e23b..33e3064d 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -30,6 +30,10 @@ jobs:
run: npm run build
working-directory: viewer
+ - name: Check viewer
+ run: npm run lint && npm test
+ working-directory: viewer
+
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
@@ -38,6 +42,29 @@ jobs:
- name: Build package
run: uv build
+ - name: Install built package for browser validation
+ run: |
+ uv venv .venv
+ uv pip install dist/*.whl
+ .venv/bin/python viewer/tests/prepare.py
+
+ - name: Generate the published examples from scratch
+ run: |
+ uv pip install -r examples/requirements.txt
+ .venv/bin/python examples/build_gallery.py --output results/gallery
+ .venv/bin/python viewer/export_docs.py --source results/gallery --output results/viewer-release/gallery
+
+ - name: Install browser
+ run: npx playwright install --with-deps chromium
+ working-directory: viewer
+
+ - name: Verify local viewer and portable report
+ run: npm run test:browser
+ working-directory: viewer
+ env:
+ PLAYWRIGHT_CHANNEL: chromium
+ VIEWER_GALLERY_URL: http://127.0.0.1:8722/gallery/index.html
+
- name: Upload dist
uses: actions/upload-artifact@v4
with:
diff --git a/.github/workflows/lint-and-unit-test.yml b/.github/workflows/lint-and-unit-test.yml
index fd8b5541..6216d3e5 100644
--- a/.github/workflows/lint-and-unit-test.yml
+++ b/.github/workflows/lint-and-unit-test.yml
@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- python-version: [3.9, 3.13]
+ python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.15"]
steps:
- uses: actions/checkout@v4
@@ -29,6 +29,10 @@ jobs:
run: npm run build
working-directory: viewer
+ - name: Test viewer data format
+ run: npm test
+ working-directory: viewer
+
# Python setup
- name: Install uv and set python version to ${{ matrix.python-version }}
uses: astral-sh/setup-uv@v5
@@ -40,7 +44,7 @@ jobs:
run: uv pip install hatch-vcs
- name: Install dependencies
- run: uv pip install -e ".[dev,sil]"
+ run: uv pip install -e ".[dev,live,mosaik]"
# type checking
- name: Run type checker
@@ -53,3 +57,10 @@ jobs:
# unit-testing
- name: Run tests
run: make test
+
+ - name: Check reproducible examples with pinned dependencies
+ if: matrix.python-version == '3.13'
+ run: >-
+ uv run --extra dev --extra live --extra mosaik
+ --with-requirements examples/requirements.txt
+ pytest tests/test_examples.py tests/test_infrastructure_study.py tests/test_microgrid_comparison.py
diff --git a/.gitignore b/.gitignore
index 5e1aac01..3d291608 100644
--- a/.gitignore
+++ b/.gitignore
@@ -90,3 +90,8 @@ crash.log
# Sphinx
_build
results.csv
+
+# Browser validation outputs
+viewer/test-results/
+viewer/playwright-report/
+/test-results/
diff --git a/.readthedocs.yml b/.readthedocs.yml
index 97b96c60..82814b1f 100644
--- a/.readthedocs.yml
+++ b/.readthedocs.yml
@@ -8,10 +8,8 @@ build:
jobs:
post_create_environment:
- pip install uv
- pre_install:
- - cd viewer && npm ci && npx vite build
post_install:
- - uv pip install -e ".[docs,sil]"
+ - uv pip install -e ".[docs,live]"
pre_build:
- make docs-viewer
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d49ad9d5..e47b17df 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -5,7 +5,7 @@
Install dependencies (requires [uv](https://docs.astral.sh/uv/)):
```bash
-uv pip install -e ".[dev,sil,docs]"
+uv pip install -e ".[dev,live,docs]"
```
## Viewer
@@ -61,4 +61,4 @@ make docs-viewer
## Release
1. Create a GitHub release (tag triggers the `publish.yml` workflow)
-2. The workflow builds the viewer, bundles it into the Python package, and publishes to PyPI
\ No newline at end of file
+2. The workflow builds the viewer, bundles it into the Python package, and publishes to PyPI
diff --git a/Makefile b/Makefile
index 98ed1c16..8fbe58e6 100644
--- a/Makefile
+++ b/Makefile
@@ -1,28 +1,25 @@
.PHONY: docs docs-viewer serve-docs typecheck lint test check
+CHECK_RUN = uv run --extra dev --extra live --extra mosaik
+
check: typecheck lint test
typecheck:
- uv run mypy vessim
+ $(CHECK_RUN) mypy vessim
lint:
- uv run ruff check vessim
+ $(CHECK_RUN) ruff check vessim
test:
- uv run pytest
+ $(CHECK_RUN) pytest
docs: docs-viewer
- uv run mkdocs build
+ uv run --extra docs mkdocs build
serve-docs: docs-viewer
- uv run mkdocs serve --watch docs
+ uv run --extra docs mkdocs serve --watch docs
docs-viewer:
- cd viewer && [ -d node_modules ] || npm ci
- cd viewer && VITE_STATIC_BASE="./" npx vite build --outDir dist
- cp -r viewer/dist/. docs/viewer/
- uv run python examples/basic_example.py
- mkdir -p docs/viewer/results
- cp results/basic_example/metadata.yaml docs/viewer/results/
- cp results/basic_example/timeseries.csv docs/viewer/results/
- echo '{"mode":"single","experiments":[{"name":"","status":"completed"}]}' > docs/viewer/experiments.json
+ npm --prefix viewer ci
+ npm --prefix viewer run build
+ uv run --with-requirements examples/requirements.txt python viewer/export_docs.py
diff --git a/README.md b/README.md
index f5f11be4..75889656 100644
--- a/README.md
+++ b/README.md
@@ -2,80 +2,96 @@
-Vessim is a **co-simulation testbed for microgrids**\*
+Vessim is a Python library for simulating datacenter energy systems.
-Vessim lets you model how loads, (renewable) generation, energy storage, and the public grid interact, and test control and dispatch strategies against them, in pure simulation or with **real software and hardware** in the loop.
+Vessim models the interaction between datacenter demand and energy infrastructure, helping you understand how design and control decisions affect cost, carbon emissions, and resilience.
-*\*By "microgrid" we mean any local energy system that combines loads, generation, and storage behind a grid connection. Grids can scale from small battery-constrained systems to a GW-scale datacenter campus.*
+[Documentation](https://vessim.readthedocs.io/en/latest/) · [Getting started](https://vessim.readthedocs.io/en/latest/getting_started/) · [Explore example results](https://vessim.readthedocs.io/en/latest/vessim_viewer/)
-Check out the [website and documentation](https://vessim.readthedocs.io/en/latest/)!
+- **Simulation studies:** Compare infrastructure and control strategies across demand, weather, and operating scenarios.
+ Explore design choices through parameter sweeps, optimization, or probabilistic simulations.
+- **Live modeling:** Connect real applications to a simulated energy system through REST APIs.
+ Test how a scheduler or controller responds to changing power availability.
+- **Digital twins:** Model an energy system alongside the real installation using live measurements.
+ Explore possible futures, such as prolonged low renewable generation or a grid outage.
-## What can I do with Vessim?
+## Getting started
-Vessim helps you understand and optimize how loads, energy sources, and storage interact.
-It was originally designed for energy-aware and carbon-aware data centers, but has also been used for other use cases such as battery price arbitrage and demand response for cooling warehouses.
-
-- **Control and dispatch strategies**: Test battery charging/discharging, load-shifting, and price- or carbon-driven dispatch.
-- **Demand response and power outages**: Simulate demand response signals or power outages to understand your system's flexibility and test mitigation strategies.
-- **Microgrid composition**: Experiment with adding solar panels, wind turbines, or batteries to see how they would affect your energy costs and carbon emissions.
-- **Energy- and carbon-aware computing**: Develop applications that adapt their energy consumption to the carbon intensity and price of electricity.
-
-Vessim can simulate multiple distributed microgrids in parallel and easily integrates historical datasets and new simulators.
-Vessim’s software-in-the-loop capabilities let you run real systems against simulated microgrids. Connect live data sources like Prometheus and interact through REST APIs.
+Install Vessim with Python 3.9 to 3.15:
+```console
+python -m pip install vessim
+```
-## Simple Example
+This example models three days of datacenter demand, solar generation, and battery operation.
+Surplus solar power charges the battery, which supplies demand when solar generation falls.
+The grid supplies any remaining deficit and receives surplus power once the battery is full.
-The scenario below simulates a microgrid with a computing system drawing 700W, a solar panel, and a 1.5 kWh battery.
+Download [solar_example.csv](https://raw.githubusercontent.com/dos-group/vessim/main/datasets/solar_example.csv) into your working directory, then run:
```python
import vessim as vs
-environment = vs.Environment(sim_start="2022-06-09", step_size=300)
+# Scale the solar profile to a 2.4 kW installation.
+solar = vs.Trace.from_csv("solar_example.csv", column="Berlin", scale=2400)
-environment.add_microgrid(
- name="datacenter",
+env = vs.Environment(sim_start="2026-01-01", step_size=5 * 60)
+env.add_microgrid(
+ name="site",
actors=[
- vs.Actor(name="server", signal=vs.StaticSignal(value=700), consumer=True),
- vs.Actor(name="solar_panel", signal=vs.Trace.from_csv(
- "datasets/solar_example.csv", column="Berlin", scale=5000
- )),
- ],
- dispatchables=[
- vs.SimpleBattery(name="battery", capacity=1500, initial_soc=0.8, min_soc=0.3)
+ vs.Consumer("load", signal=700),
+ vs.Producer("solar", signal=solar),
],
+ dispatchables=[vs.SimpleBattery("battery", capacity=1000), vs.GridConnection()],
)
+recorder = env.add_controller(vs.Recorder("results/basic_example"))
+env.run(until=3 * 24 * 3600)
+
+metrics = recorder.metrics["microgrids"]["site"]
+print(f"Grid import: {metrics['grid_import'] / 1000:.2f} kWh")
+print(f"Grid export: {metrics['grid_export'] / 1000:.2f} kWh")
+```
-environment.add_controller(vs.CsvLogger("results/my_experiment"))
-environment.run(until=24 * 3600)
+```text
+Grid import: 26.71 kWh
+Grid export: 11.08 kWh
```
-Check out the [Getting Started walkthrough](https://vessim.readthedocs.io/en/latest/getting_started/) and [`examples/`](examples/) for software-in-the-loop simulations.
+[Explore this example in your browser](https://vessim.readthedocs.io/en/latest/viewer/index.html?study=getting-started#results), or open your saved results locally:
+```console
+python -m vessim view results/basic_example
+```
-## Installation
+The [Getting Started walkthrough](https://vessim.readthedocs.io/en/latest/getting_started/) explains how the components work together.
+To use your own data, see [Actors and Signals](https://vessim.readthedocs.io/en/latest/concepts/signals/).
-You can install the [latest release](https://pypi.org/project/vessim/) of Vessim
-via [pip](https://pip.pypa.io/en/stable/quickstart/):
+## Explore further
-```
-pip install vessim
-```
+| Example | What you can explore |
+| --- | --- |
+| [Exploring microgrid configurations](examples/infrastructure_study.py) | Compare wind, solar, and battery configurations over a year. [View results](https://vessim.readthedocs.io/en/latest/viewer/index.html?study=infrastructure-year#results) |
+| [Optimization](https://vessim.readthedocs.io/en/latest/concepts/optimization/) | Search for designs with different cost and emissions trade-offs. [View results](https://vessim.readthedocs.io/en/latest/viewer/index.html?study=optuna-search#results) |
+| [Dispatch strategies](examples/strategy_comparison.py) | Compare control policies across demand scenarios. [View results](https://vessim.readthedocs.io/en/latest/viewer/index.html?study=dispatch-comparison#results) |
+| [Demand response](examples/demand_response.py) | Adjust workload concurrency under a flexible grid connection agreement |
+| [Custom metrics](examples/custom_metrics.py) | Calculate electricity costs, emissions, and your own study objectives |
-If you require software-in-the-loop capabilities, install the `sil` extension:
+[Run the examples locally](examples/README.md) with a shared setup and reproducible commands.
-```
-pip install vessim[sil]
-```
+For [live measurements and REST control](https://vessim.readthedocs.io/en/latest/concepts/live/), install `pip install 'vessim[live]'` and use `vs.Environment.live()`.
+To [connect external simulators](https://vessim.readthedocs.io/en/latest/concepts/coupling/) through Mosaik, install `pip install 'vessim[mosaik]'`.
## Publications
-If you use Vessim in your research, please cite our paper:
+If you use Vessim in your research, please cite:
+
+Philipp Wiesner, Ilja Behnke, Paul Kilian, Marvin Steinke, and Odej Kao.
+[**Vessim: A Testbed for Carbon-Aware Applications and Systems.**](https://dl.acm.org/doi/pdf/10.1145/3727200.3727210)
+*ACM SIGENERGY Energy Informatics Review 4 (5)*, 2024.
-- Philipp Wiesner, Ilja Behnke, Paul Kilian, Marvin Steinke, and Odej Kao. "[Vessim: A Testbed for Carbon-Aware Applications and Systems.](https://dl.acm.org/doi/pdf/10.1145/3727200.3727210)" _ACM SIGENERGY Energy Informatics Review 4 (5)_. 2024.
+More papers are listed in the [documentation](https://vessim.readthedocs.io/en/latest/publications/)!
-For details in Vessim's software-in-the-loop simulation methodology, refer to:
-- Philipp Wiesner, Marvin Steinke, Henrik Nickel, Yazan Kitana, and Odej Kao. "[Software-in-the-Loop Simulation for Developing and Testing Carbon-Aware Applications.](https://doi.org/10.1002/spe.3275)" _Software: Practice and Experience, 53 (12)_. 2023.
+## About
-For more related papers and concrete use cases, please refer to the [documentation](https://vessim.readthedocs.io/en/latest/publications).
+Vessim was originally developed at the research group for [Distributed and Operating Systems](https://distributedsystems.berlin/) at TU Berlin. It is now maintained at the [Machine Learning Systems Lab](https://mlsys.cst.cam.ac.uk/) at the University of Cambridge.
diff --git a/benchmarks/README.md b/benchmarks/README.md
new file mode 100644
index 00000000..8cc7c737
--- /dev/null
+++ b/benchmarks/README.md
@@ -0,0 +1,82 @@
+# Benchmarks
+
+These scripts measure simulation speed, recorder overhead, and viewer indexing.
+Run them from a checkout installed with `pip install -e .`.
+Install `pip install -e '.[mosaik]'` only when comparing backends.
+Write reports under `results/` to keep generated measurements out of the repository.
+
+## Backend workloads
+
+```console
+python benchmarks/run.py --backend native --output results/benchmarks/native.json
+python benchmarks/run.py --backend mosaik --output results/benchmarks/mosaik.json
+```
+
+Six workloads cover static inputs, traces, CSV recording, multiple microgrids, mixed sampling intervals, and controller feedback.
+Reports contain median setup and execution times, individual timing samples, peak traced Python memory, and trajectory checksums.
+Each workload retains its trajectory in memory with metrics disabled, and the CSV case additionally writes it to disk.
+Repeated runs must produce the same checksum.
+Imports and trace loading are outside the timed workloads, except for lazy backend initialization on first use.
+Memory measurements run separately from timing measurements.
+
+Use `--steps` and `--repeats` to change the workload size.
+To profile all six workloads separately:
+
+```console
+python benchmarks/run.py --output results/benchmarks/native.json --profile results/benchmarks/profiles
+python -m pstats results/benchmarks/profiles/traces.prof
+```
+
+The fixed `input.csv` is also used to compare native and Mosaik trajectories in `tests/test_backends.py`.
+The tests compare every timestamp and recorded field, allowing floating-point roundoff in numeric values.
+Separate behavioral tests check power balance, battery state, sampling, and controller ordering against expected results.
+Historical timing reports remain available in Git history.
+
+## Study throughput and memory
+
+```console
+python benchmarks/studies.py --runs 1000 --output results/benchmarks/studies-native.json
+```
+
+This measures sequential scenarios with a shared trace and metrics-only recording, including model construction and cleanup.
+It also compares Python allocation peaks for no recorder, metrics only, and full in-memory trajectories at 1,000 and 10,000 steps.
+Use `--backend mosaik` for a backend comparison, or `--runs`, `--steps`, and `--memory-steps` for shorter checks.
+Traced allocations are not total process memory.
+
+## Annual workloads and parallel execution
+
+```console
+python benchmarks/native_study.py --quiet --output results/benchmarks/annual
+```
+
+This uses the infrastructure example's synthetic inputs with three reference configurations.
+It measures the cost of observations, built-in metrics, custom metrics, and recording to memory or disk over a simulated year.
+It also includes five-minute simulations, profiles, and throughput at different worker counts.
+Reports separate input preparation, model setup, and simulation time.
+Parallel measurements include process startup and writing experiment records, and check that results agree across worker counts.
+
+For a short throughput check:
+
+```console
+python benchmarks/native_study.py --quiet --throughput-only --jobs 6 --workers 1 2 --output results/benchmarks/parallel
+```
+
+## Viewer
+
+```console
+node viewer/benchmark.mjs 10000
+```
+
+This measures indexing synthetic configurations in JavaScript, excluding browser rendering and network requests.
+For browser checks with 10,000 recorded summaries, use the existing viewer test fixtures:
+
+```console
+npm --prefix viewer ci
+npm --prefix viewer run build
+python viewer/tests/prepare.py
+python -m vessim view results/viewer-release/input/large
+```
+
+Both viewer datasets are synthetic fixtures, not simulation results.
+Compare timings on the same machine with the same dependencies and workload settings.
+Profiling adds overhead, so profile timings should not be compared with ordinary execution times.
diff --git a/benchmarks/input.csv b/benchmarks/input.csv
new file mode 100644
index 00000000..d7536f8b
--- /dev/null
+++ b/benchmarks/input.csv
@@ -0,0 +1,1002 @@
+,solar,load,carbon
+0,0.0,700.0,300.0
+60,5.235971141695885,699.9444495882868,301.6665895072445
+120,10.471842598048722,699.7778600701122,303.33271608367534
+180,15.707514685613328,699.5004165278026,304.9979169270678
+240,20.942887724740213,699.112427203418,306.6617294923393
+300,26.177862041473343,698.6143231562925,308.323691620031
+360,31.41233796944778,698.0066577841242,309.9833416646828
+420,36.64621585178719,697.2901062081451,311.6402186230649
+480,41.879396043001165,696.4654645230564,313.2938622622314
+540,47.11177891088233,695.5336489125606,314.9438132473599
+600,52.3432648384032,694.4956946314737,316.5896132693415
+660,57.57375422561273,693.3527548555489,318.23080517208587
+720,62.80314749153259,692.1060994002885,319.8669330795061
+780,68.031345076053,690.7571133101685,321.4975425221483
+840,73.25824744182825,689.307295319843,323.1221805634299
+900,78.48375507617168,687.7582561890373,324.7403959254523
+960,83.70776849295036,686.111716912981,326.3517391143535
+1020,88.93018823447906,684.369506810368,327.95576254516413
+1080,94.15091487341394,682.5335614909678,329.55202066613396
+1140,99.36984901464521,680.6059207051483,331.14007008249325
+1200,104.5868912971898,678.5887260776948,332.7194696796152
+1260,109.80194239608286,676.4842187284488,334.28978074554516
+1320,115.01490302426878,674.2947367824045,335.8505670928617
+1380,120.22567393449152,672.0227127720323,337.40139517983766
+1440,125.43415592118414,669.6706709347166,338.9418342308651
+1500,130.64024982235748,667.2412244083057,340.47145635611247
+1560,135.84385652148805,664.7370723278952,341.98983667038056
+1620,141.04487694940516,662.1609968270665,343.49655341112305
+1680,146.24321208617698,659.5158599469128,344.9911880556
+1740,151.4387629629957,656.8046004562889,346.47332543713173
+1800,156.6314306640619,654.030230586814,347.9425538604203
+1860,161.82111632846758,651.195832686258,349.39846521590624
+1920,167.00772115207852,648.3045557940277,350.8406550931301
+1980,172.19114638941514,645.3596121425577,352.2687228930659
+2040,177.3712933555327,642.3642735884945,353.6822719393961
+2100,182.54806342790002,639.3218679776357,355.08090958869695
+2160,187.72135804827704,636.2357754476674,356.4642473395035
+2220,192.89107872459132,633.1094246728017,357.8319009402243
+2280,198.05712703281318,629.9462890544928,359.1834904958753
+2340,203.21940461882954,626.7498828624588,360.51864057360393
+2400,208.3778132003164,623.523757330299,361.8369803069737
+2460,213.5322545686101,620.2714967100444,363.13814349897996
+2520,218.68263059057696,616.9967142900241,364.4217687237691
+2580,223.82884321048155,613.7030483804722,365.68749942703244
+2640,228.97079445185378,610.3941582713351,366.9349840250466
+2700,234.1083864193539,607.0737201667703,368.1638760023334
+2760,239.2415213006366,603.7454231008534,369.373834007912
+2820,244.370101368213,600.4129648390306,370.5645219501165
+2880,249.49402898131117,597.0800477698712,371.7356090899523
+2940,254.61320658773548,593.750374791683,372.88677013296615
+3000,259.72753672572344,590.4276451985625,374.01768531960374
+3060,264.8369220258013,587.1155505704476,375.1280405140293
+3120,269.941265212638,583.8177706717428,376.217527291384
+3180,275.040469106897,580.5379693630705,377.28584302345723
+3240,280.1344366270865,577.2797905306913,378.33269096274836
+3300,285.22307079140774,574.0468540381166,379.3577803248942
+3360,290.3062747196013,570.8427517044107,380.3608263694411
+3420,295.38395163479163,567.6710433136496,381.3415504789374
+3480,300.4560048653297,564.5352526599722,382.2996802363253
+3540,305.5223378466331,561.4388636326156,383.2349495006107
+3600,310.5828541230249,558.3853163452858,384.1470984807896
+3660,315.63745734956973,555.3780033141625,385.0358738080104
+3720,320.68605129390824,552.4202656887865,385.90102860595255
+3780,325.7285398380891,549.5153895400142,386.7423225594017
+3840,330.764826980399,546.6666022091666,387.55952198100255
+3900,335.7948168371905,543.8770687224259,388.35239987617035
+3960,340.8184136447071,541.1498882744654,389.12073600614355
+4020,345.83552176090717,538.4880907852194,389.86431694915973
+4080,350.84604566728405,535.8946335336162,390.5829361597381
+4140,355.8498899706852,533.3723978720176,391.2763940260521
+4200,360.84695940512773,530.9241860250123,391.9444979253755
+4260,365.83715883361293,528.5527179761218,392.58706227758773
+4320,370.8203932499369,526.2606284458755,393.2039085967226
+4380,375.79656778049946,524.0504639646151,393.79486554054677
+4440,380.76558768611056,521.9246800432785,394.3597689581533
+4500,385.7273583637939,519.8856384453067,394.8984619355586
+4560,390.68178534858805,517.9356045627047,395.4107948392883
+4620,395.6287743153447,516.0767448991734,395.8966253579412
+4680,400.56823108052515,514.3111246631053,396.3558185417193
+4740,405.5000616039923,512.6407054731212,396.7882468399131
+4800,410.42417199080245,511.06734317869586,397.19379013633124
+4860,415.34046849299153,509.5927857982939,397.5723357826659
+4920,420.24885751136094,508.2186715773068,397.92377862978253
+4980,425.149245597259,506.9465271679489,398.24802105692794
+5040,430.0415394543603,505.7777659331342,398.544972998846
+5100,434.9256459404419,504.7136863762184,398.81455197079566
+5160,439.8014720691568,503.75547069835125,399.05668309146256
+5220,444.6689250118035,502.90418348504096,399.27129910375885
+5280,449.5279120990944,502.1607705233913,399.45834039350564
+5340,454.37834082291954,501.52605775132406,399.61775500599174
+5400,459.22011883810774,501.00075033995546,399.74949866040544
+5460,464.05315396418473,500.585431910145,399.85353476213425
+5520,468.87735418712845,500.2805638840881,399.92983441292995
+5580,473.6926276611207,500.08648497267205,399.9783764189357
+5640,478.49888271029545,500.0034107991655,399.99914729657314
+5700,483.29602783048466,500.03143365965934,399.9921412762875
+5760,488.0839716909602,500.17052242052466,399.9573603041505
+5820,492.86262313617266,500.420522553003,399.8948140413197
+5880,497.63189118748676,500.7811563048886,399.80451986135483
+5940,502.39168504491374,501.2520230091135,399.6865028453919
+6000,507.1419140888393,501.8325995288921,399.5407957751765
+6060,511.88248788174997,502.52224083893026,399.3674391239579
+6120,516.6133161699541,503.3201807420539,399.1664810452469
+6180,521.3343088853004,504.22553272046014,398.9379773594396
+6240,526.0453761468929,505.23729092064616,398.68199153831307
+6300,530.7464282628016,506.3543312709204,398.3985946873937
+6360,535.4373757317705,507.57541273025566,398.08786552620677
+6420,540.118129244922,508.899178667096,397.7498903664101
+6480,544.7885996874562,510.32415836658527,397.3847630878195
+6540,549.448698140348,511.8487686645438,396.9925851123312
+6600,554.0983358820407,513.4713157063763,396.57346537574995
+6660,558.7374243901337,515.1899968289592,396.12752029753
+6720,563.365875343069,517.0029025634128,395.65487374843667
+6780,567.983600621812,518.9080187565385,395.15565701613895
+6840,572.5905123115301,520.9032288085583,394.63000876874145
+6900,577.1865227032652,522.9863160246755,394.07807501626627
+6960,581.7715442956045,525.1549660778401,393.50000907009576
+7020,586.345489796346,527.406769579986,392.89597150038696
+7080,590.9082721241605,529.7392247588803,392.26613009146956
+7140,595.4598044102498,532.1497402376131,391.6106597952408
+7200,599.9999999999999,534.6356379136388,390.9297426825682
+7260,604.5287724546315,537.1941559341699,390.2235678927159
+7320,609.046035552845,539.8224517646178,389.492331580807
+7380,613.5517032924625,542.5176053466731,388.7362368633376
+7440,618.045689892065,545.2766223425134,387.9554937617562
+7500,622.5279097926256,548.0964374615378,387.15031914412657
+7560,626.9982776591386,550.97391786593,386.32093666488737
+7620,631.4567083822435,553.9058666512669,385.4675767027278
+7680,635.9031170798459,556.8890263983054,384.5904762965946
+7740,640.3374190987338,559.9200827920024,383.6898790798498
+7800,644.7595300161887,562.9956683037462,382.7660352125961
+7860,649.1693656415927,566.1123659327078,381.8192013121896
+7920,653.5668420180324,569.2667130021581,380.84964038195903
+7980,657.9518754238966,572.4552050065288,379.8576217381507
+8040,662.3243823744698,575.6742995049483,378.84342093512043
+8100,666.6842796235226,578.920420056922,377.8073196887921
+8160,671.0314841648961,582.1899601957873,376.74960579840547
+8220,675.3659132340822,585.4792874355277,375.67057306657335
+8280,679.6874843097994,588.7847473064945,374.570521217672
+8340,683.9961151155636,592.1026674155532,373.4497558145862
+8400,688.2917236212553,595.429361526142,372.30858817383245
+8460,692.5742280446804,598.761133653711,371.1473352790844
+8520,696.8435468531277,602.0942821719918,369.96631969312386
+8580,701.0995987649213,605.4251039255347,368.76586946824176
+8640,705.3423027509677,608.7498983439447,367.5463180551151
+8700,709.5715780362988,612.0649715532454,366.3080042101836
+8760,713.7873441016095,615.3666404798045,365.051271901553
+8820,717.9895206847907,618.6512369422576,363.77647021345035
+8880,722.1780277824579,621.9151117268885,362.4839532492589
+8940,726.3527856514733,625.1546386419361,361.1740800331578
+9000,730.5137148104648,628.3662185463227,359.8472144103956
+9060,734.6607360413392,631.5462833483292,358.5037249462249
+9120,738.79377039079,634.6912999697755,357.14398482352476
+9180,742.9127391718008,637.7977742712981,355.7683717391417
+9240,747.0175639651434,640.8622549343684,354.3772677989763
+9300,751.1081666208709,643.8813372957361,352.97105941184486
+9360,755.184469259805,646.8516671300378,351.5501371821464
+9420,759.2463942750193,649.7699443763689,350.11489580136384
+9480,763.2938643333167,652.6329268046792,348.6657339384298
+9540,767.3268023767016,655.4374336179161,347.20305412898824
+9600,771.3451316238471,658.180348985917,345.7272626635812
+9660,775.3487755715566,660.8586255071224,344.23876947479306
+9720,779.3376579962204,663.4692875942635,342.73798802338297
+9780,783.3117029552669,666.0094347802644,341.2253351834376
+9840,787.2708347886087,668.4762449406854,339.701231126575
+9900,791.2149781200826,670.866977429126,338.1660992052332
+9960,795.144057858885,673.1789761221055,336.620365835075
+10020,799.0579992010012,675.4096723700383,335.064460376542
+10080,802.9567276306299,677.5565878510249,333.4988150155905
+10140,806.8401689216016,679.6173373242879,331.9238646436425
+10200,810.7082491387922,681.5896312801933,330.3400467367856
+10260,814.5608946395301,683.471278483916,328.74780123425444
+10320,818.3980320749981,685.2601884099186,327.14757041622823
+10380,822.219588391631,686.954373564543,325.5397987809782
+10440,826.0254908325047,688.5519516941318,323.92493292139824
+10500,829.8156669387233,690.0511478762286,322.3034214009548
+10560,833.5900445507967,691.45029649153,320.67571462908836
+10620,837.3485518100161,692.7478430744036,319.0422647361027
+10680,841.0911171598211,693.9423460399113,317.40352544757627
+10740,844.817669347162,695.0324782854227,315.7599519583298
+10800,848.528137423857,696.0170286650366,314.1120008059867
+10860,852.2224507479424,696.8949033351741,312.46012974415964
+10920,855.9005389850179,697.6651269698482,310.80479761529983
+10980,859.562332109585,698.3268438442584,309.1464642232437
+11040,863.2077604063813,698.8793187855086,307.4855902054927
+11100,866.8367544717066,699.3219379893898,305.82263690526133
+11160,870.4492452147451,699.6542097023217,304.1580662433291
+11220,874.0451638588804,699.8757647676955,302.4923405897321
+11280,877.6244419430045,699.9863570360089,300.8259226353304
+11340,881.1870113228225,699.9858636383415,299.1592752632851
+11400,884.7328041721489,699.8742851228619,297.4928614204831
+11460,888.2617529841986,699.6517454542197,295.82714398894336
+11520,891.773790572873,699.3184918758193,294.162585657242
+11580,895.2688500740395,698.8748946351301,292.4996487919903
+11640,898.7468649468025,698.3214465723395,290.83879530940254
+11700,902.2077689747729,697.6587625728023,289.1804865469892
+11760,905.6514962673263,696.8875788838989,287.52518313541015
+11820,909.0779812608596,696.0087522970575,285.87334487052505
+11880,912.4871587200372,695.0232591958529,284.22543058567516
+11940,915.8789637390347,693.9321944712352,282.5818980242324
+12000,919.2533317427736,692.7367703050975,280.94320371245146
+12060,922.6101984881515,691.438314823532,279.30980283266
+12120,925.949500065264,690.0382706212703,277.68214909682195
+12180,929.2711728986227,688.5381931589495,276.06069462050914
+12240,932.575153748365,686.9397490349825,274.44588979731685
+12300,935.8613797114581,685.2447141339528,272.8381831737574
+12360,939.1297882228965,683.454971653593,271.23802132466625
+12420,942.3803170568938,681.5725100125356,269.6458487291571
+12480,945.6129043280663,679.599420641165,268.06210764715763
+12540,948.8274884926121,677.5378956580195,266.4872379965628
+12600,952.0240083494822,675.3902254343304,264.921677231038
+12660,955.2024030415465,673.158796049401,263.36586021850724
+12720,958.3626120567513,670.8460866396517,261.82021912035884
+12780,961.5045752292727,668.4546666442807,260.28518327140404
+12840,964.6282327406608,665.9871929505939,258.7611790606193
+12900,967.733525120979,663.4464069421833,257.2486298127085
+12960,970.820393249937,660.8351314532255,255.74795567051476
+13020,973.8887783580146,658.1562676322906,254.25957347831712
+13080,976.9386220275829,655.4127917191423,252.78389666604323
+13140,979.9698661940147,652.6077517381106,251.32133513443006
+13200,982.9824531467901,649.7442641117117,249.87229514116552
+13260,985.9763255305965,646.8255101982774,248.43717918804157
+13320,988.9514263464189,643.854732757439,247.01638590915064
+13380,991.9076989526263,640.835232347396,245.61030996015694
+13440,994.84508706605,637.7703636579668,244.21934190867177
+13500,997.7635347630543,634.6635317835025,242.84386812576562
+13560,1000.6629864806017,631.5181884397982,241.48427067864498
+13620,1003.5433870173114,628.3378281292082,240.1409272245252
+13680,1006.4046815345088,625.1259842582256,238.81421090572812
+13740,1009.2468155572709,621.886225211838,237.50449024603367
+13800,1012.0697349754628,618.6221503890231,236.2121290483148
+13860,1014.8733860447679,615.3373862037864,234.93748629348326
+13920,1017.6577153877112,612.0355820561853,233.68091604077497
+13980,1020.4226699946754,608.7204062778155,232.44276732940267
+14040,1023.1681972249106,605.395542056265,231.2233840816026
+14100,1025.8942448075359,602.0646833430616,230.02310500710308
+14160,1028.6007608425346,598.731530749663,228.84226350904035
+14220,1031.2876938017432,595.3997874360463,227.6811875913488
+14280,1033.9549925298309,592.0731549964676,226.54019976765102
+14340,1036.6026062452747,588.7553293469596,225.41961697167264
+14400,1039.2304845413264,585.4499966191387,224.31975046920718
+14460,1041.8385773869707,582.1608290648824,223.24090577165543
+14520,1044.4268351278797,578.8914809764275,222.1833825511631
+14580,1046.9952084873564,575.6455846264209,221.1474745573805
+14640,1049.5436485672749,572.426746232435,220.13346953586722
+14700,1052.0721068490093,569.2385419504303,219.14164914816473
+14760,1054.5805351943584,566.0845139016166,218.17228889355897
+14820,1057.0688858464628,562.9681662371272,217.2256580325543
+14880,1059.5371114307122,559.892961244878,216.30201951208085
+14940,1061.9851649556501,556.8623155029379,215.40162989245533
+15000,1064.412999813866,553.8795960836811,214.524739276116
+15060,1066.8205697828837,550.9481168129428,213.67159123815117
+15120,1069.2078290260415,548.0711345883315,212.8424227586412
+15180,1071.5747320933642,545.2518457607883,212.0374641568322
+15240,1073.92123392243,542.4933825834158,211.25693902715994
+15300,1076.2472898392261,539.7988097315176,210.50106417714164
+15360,1078.5528555590004,537.1711208977202,209.7700495671532
+15420,1080.8378871871055,534.6132354659549,209.06409825210807
+15480,1083.1023412198326,532.1279952679988,208.38340632505452
+15540,1085.3461745452416,529.7181614261758,207.72816286270654
+15600,1087.5693444439798,527.3864112857275,207.0985498729238
+15660,1089.7718085900974,525.1353354402602,206.49474224415508
+15720,1091.9535250518518,522.967434853574,205.91690769685925
+15780,1094.1144522925063,520.8851180810718,205.36520673691726
+15840,1096.254549171121,518.8906985938344,204.83979261104838
+15900,1098.3737749433365,516.9863922083346,204.34081126424252
+15960,1100.4720892621488,515.1743146246472,203.86840129922086
+16020,1102.549452178678,513.4564790758888,203.42269393793612
+16080,1104.6058241429284,511.8347940914992,203.00381298512275
+16140,1106.6411660045421,510.3110613768494,202.6118747939076
+16200,1108.655439013544,508.8869738115323,202.2469882334903
+16260,1110.6486048210797,507.56411356855915,201.9092546589025
+16320,1112.6206254801448,506.3439503565528,201.59876788285442
+16380,1114.571463446309,505.2278397868888,201.31561414967635
+16440,1116.5010815784294,504.21702186760047,201.05987211136232
+16500,1118.409443139358,503.3126196257195,200.83161280572307
+16560,1120.296511796642,502.51563785958365,200.63089963665357
+16620,1122.1622516232142,501.8269620224977,200.45778835652123
+16680,1124.0066270980772,501.24735723898726,200.31232705067976
+16740,1125.829603106981,500.7774674547396,200.19455612411207
+16800,1127.63114494309,500.41781472117424,200.10450829020718
+16860,1129.4112183076447,500.16879861543947,200.04220856167325
+16920,1131.169789310614,500.03069579647934,200.00767424358992
+16980,1132.906824471341,500.0036596976629,200.0009149286012
+17040,1134.62229071918,500.08772035631847,200.02193249425113
+17100,1136.3161553941268,500.28278438036216,200.0707211024622
+17160,1137.9883862474392,500.58863505205704,200.14726720115692
+17220,1139.6389514422524,501.00493256878843,200.25154952802234
+17280,1141.2678195541841,501.5312144205873,200.38353911641593
+17340,1142.8749595719341,502.1668959039828,200.54319930341182
+17400,1144.4603408978724,502.91127077161246,200.73048573998472
+17460,1146.023933348624,503.76351201686896,200.94534640332867
+17520,1147.5657071556425,504.7226727927113,201.18772161130767
+17580,1149.0856329657772,505.7876874636196,201.4575440390335
+17640,1150.5836818418315,506.9573727895247,201.75473873756675
+17700,1152.0598252631144,508.23042924039805,202.07922315473547
+17760,1153.5140351259827,509.6054424400402,202.4309071580657
+17820,1154.9462837443768,511.0808847374639,202.8096930598179
+17880,1156.3565438503476,512.6551169041277,203.21547564412163
+17940,1157.7447885945758,514.3263899551318,203.6481421962015
+18000,1159.110991546882,516.0928470923548,204.10757253368615
+18060,1160.4551266967317,517.9525257673716,204.5936390399916
+18120,1161.7771684537292,519.9033598618615,205.10620669976922
+18180,1163.0770916481054,521.9431819830817,205.64513313640936
+18240,1164.3548715311958,524.069725871859,206.21026865158927
+18300,1165.6104837759137,526.2806289204224,206.80145626685533
+18360,1166.843904477212,528.57343479728,207.41853176722674
+18420,1168.0551101525382,530.9455961762229,208.06132374681016
+18480,1169.2440777422823,533.3944775664263,208.72965365641102
+18540,1170.4107846102152,535.9173582405007,209.42333585312957
+18600,1171.5552085439201,538.5114352572432,210.14217765192703
+18660,1172.6773277552147,541.1738265757278,210.88597937914815
+18720,1173.7771208805668,543.9015742572772,211.6545344279847
+18780,1174.8545669815005,546.6916477517557,212.44762931586482
+18840,1175.9096455449956,549.5409472645372,213.26504374375264
+18900,1176.9423364838765,552.4463072004007,214.1065506573408
+18960,1177.9526201371968,555.4044996805343,214.97191631011972
+19020,1178.9404772706116,558.4122381287339,215.86090032830572
+19080,1179.9058890767453,561.466180922817,216.77325577760988
+19140,1180.848837175549,564.5629351071925,217.70872923182935
+19200,1181.7693036146495,567.699060162462,218.66706084324198
+19260,1182.6672708696933,570.8710718278654,219.64798441478445
+19320,1183.5427218446778,574.0754459723233,220.65122747399408
+19380,1184.3956398722783,577.3086225097759,221.67651134869433
+19440,1185.2260087141653,580.5670093544666,222.72355124440128
+19500,1186.0338125613127,583.8469864117808,223.792056323432
+19560,1186.8190360343,587.1449096001996,224.8817297856905
+19620,1187.5816641836043,590.4571148999049,225.99226895111056
+19680,1188.3216824898843,593.779922423535,227.1233653437314
+19740,1189.0390768642587,597.1096405045704,228.2747047773836
+19800,1189.7338336485725,600.442569798805,229.4459674429608
+19860,1190.4059396156579,603.7750073943491,230.63682799725387
+19920,1191.0553819695863,607.1032509255936,231.84695565332154
+19980,1191.6821483459116,610.4236026865697,233.0760142723738
+20040,1192.286226811905,613.7323737391304,234.3236624571415
+20100,1192.8676058667836,617.0258880113909,235.5895536467068
+20160,1193.426274441928,620.3004863818751,236.87333621276784
+20220,1193.9622219010942,623.5525307448295,238.1746535573109
+20280,1194.4754380406146,626.7784080521888,239.4931442116634
+20340,1194.965913089594,629.9745343277015,240.82844193689905
+20400,1195.4336377100947,633.137358648758,242.1801758255691
+20460,1195.8786029973137,636.2633670914939,243.54797040472943
+20520,1196.3008004797537,639.349086634789,244.93144574023626
+20580,1196.700222119383,642.3910890188223,246.330217542281
+20640,1197.076860311789,645.3859945538962,247.74389727213492
+20700,1197.4307078863242,648.3304758753006,249.17209225007417
+20760,1197.7617581062402,651.2212616400416,250.61440576445477
+20820,1198.0700046688185,654.0551401613312,252.0704371819079
+20880,1198.3554417054886,656.8289629767974,253.53978205862427
+20940,1198.6180637819407,659.5396483464516,255.02203225269713
+21000,1198.8578658982294,662.1841846765279,256.51677603749226
+21060,1199.0748434888674,664.7596338653876,258.023598216014
+21120,1199.2689924229148,667.263134567775,259.54208023623494
+21180,1199.4403090040555,669.6919053737954,261.07180030735776
+21240,1199.5887899706688,672.0432478990839,262.6123335169764
+21300,1199.714432495891,674.3145497827335,264.16325194910496
+21360,1199.8172341876696,676.5032875896488,265.7241248030403
+21420,1199.8971930888083,678.6070296141039,267.29451851302593
+21480,1199.9543076770055,680.6234385813868,268.87399686868457
+21540,1199.9885768648815,682.5502742445312,270.46212113618446
+21600,1200.0,684.3853958732492,272.0584501801074
+21660,1199.9885768648815,686.1267646323005,273.6625405859834
+21720,1199.9543076770055,687.7724458466549,275.27394678345826
+21780,1199.8971930888083,689.3206111509322,276.8922211700608
+21840,1199.8172341876696,690.7695405207315,278.5169142355335
+21900,1199.714432495891,692.1176241835919,280.1475746866938
+21960,1199.5887899706688,693.3633644074637,281.7837495727904
+22020,1199.4403090040555,694.5053771647003,283.42498441132
+22080,1199.2689924229148,695.5423936697248,285.07082331426943
+22140,1199.0748434888674,696.4732617886609,286.7208091147483
+22200,1198.8578658982294,697.2969473193638,288.37448349397704
+22260,1198.6180637819407,698.012535140426,290.03138710859446
+22320,1198.3554417054886,698.6192302278864,291.69105971825036
+22380,1198.0700046688185,699.1163585385068,293.3530403134471
+22440,1197.7617581062402,699.5033677586408,295.01686724359473
+22500,1197.4307078863242,699.779827917858,296.6820783452443
+22560,1197.076860311789,699.9454318666452,298.34821107046383
+22620,1196.700222119383,699.9999956176516,300.0148026153206
+22680,1196.3008004797537,699.9434585501004,301.681390048435
+22740,1195.8786029973137,699.7758834771395,303.3475104395694
+22800,1195.4336377100947,699.4974565760544,305.01270098821726
+22860,1194.965913089594,699.1084871814253,306.67649915215554
+22920,1194.4754380406146,698.6094074414523,308.3384427759258
+22980,1193.9622219010942,698.0007718378358,309.9980702192077
+23040,1193.4262744419282,697.2832565697436,311.6549204850494
+23100,1192.8676058667836,696.4576588025463,313.3085333479195
+23160,1192.286226811905,695.5248957821617,314.9584494815443
+23220,1191.6821483459116,694.4860038159861,316.6042105864957
+23280,1191.0553819695865,693.3421371215483,318.24535951749345
+23340,1190.4059396156579,692.0945665441646,319.881440410387
+23400,1189.7338336485725,690.7446781450196,321.5119988087815
+23460,1189.0390768642587,689.29397166124,323.1365817902732
+23520,1188.3216824898843,687.7440588396746,324.75473809225764
+23580,1187.5816641836043,686.0966616462307,326.36601823727784
+23640,1186.8190360343,684.3536103527553,327.96997465787626
+23700,1186.0338125613127,682.5168415035881,329.5661618209161
+23760,1185.2260087141651,680.5883957640451,331.1541363513378
+23820,1184.3956398722783,678.5704156532222,332.73345715531576
+23880,1183.5427218446778,676.4651431636389,334.30368554278175
+23940,1182.6672708696933,674.2749172703669,335.86438534928004
+24000,1181.7693036146495,672.0021713324084,337.415123057122
+24060,1180.848837175549,669.6494303892147,338.95546791580443
+24120,1179.9058890767456,667.2193083553468,340.48499206165985
+24180,1178.9404772706116,664.7145051163947,342.00327063670477
+24240,1177.9526201371968,662.137803529382,343.5098819066531
+24300,1176.9423364838765,659.4920663309892,345.0044073780618
+24360,1175.9096455449956,656.7802329570293,346.48643191457626
+24420,1174.8545669815005,654.0053162767108,347.9555438522436
+24480,1173.7771208805668,651.1703992453149,349.4113351138608
+24540,1172.6773277552147,648.2786314790069,350.85340132232704
+24600,1171.5552085439201,645.3332257555868,352.28134191296795
+24660,1170.4107846102154,642.3374544450666,353.6947602448011
+24720,1169.2440777422823,639.29464587404,355.0932637107115
+24780,1168.0551101525382,636.208180627885,356.4764638465065
+24840,1166.843904477212,633.0814877949047,357.84397643882005
+24900,1165.6104837759137,629.9180411565826,359.19542163183627
+24960,1164.3548715311958,626.7213553281827,360.53042403280244
+25020,1163.0770916481054,623.4949818539823,361.8486128163024
+25080,1161.7771684537292,620.2425052614784,363.14962182726106
+25140,1160.4551266967317,616.9675390789469,364.4330896826519
+25200,1159.110991546882,613.6737218207834,365.69865987187893
+25260,1157.7447885945755,610.364712945084,366.9459808558049
+25320,1156.3565438503476,607.0441887879568,368.1747061643991
+25380,1154.9462837443768,603.7158384790827,369.3844944929764
+25440,1153.5140351259827,600.3833598430609,370.57500979700217
+25500,1152.0598252631144,597.0504552910969,371.74592138543596
+25560,1150.5836818418315,593.7208277075919,372.8969040125876
+25620,1149.0856329657772,590.3981763362079,374.0276379684615
+25680,1147.5657071556427,587.0861926699783,375.1378091675628
+25740,1146.023933348624,583.7885563500282,376.22710923614113
+25800,1144.4603408978724,580.5089310774624,377.2952355978483
+25860,1142.8749595719341,577.2509605429643,378.3418915577853
+25920,1141.2678195541844,574.0182643786244,379.3667863849153
+25980,1139.6389514422524,570.8144341365004,380.36963539282095
+26040,1137.9883862474394,567.6430292983723,381.3501600187817
+26100,1136.3161553941268,564.5075733211295,382.30808790115054
+26160,1134.6222907191802,561.4115497221809,383.24315295500855
+26220,1132.9068244713412,558.3583982092384,384.1550954460755
+26280,1131.169789310614,555.3515108587734,385.04366206285647
+26340,1129.4112183076447,552.3942283473917,385.90860598700465
+26400,1127.63114494309,549.4898362403145,386.74968696188057
+26460,1125.8296031069808,546.6415613410883,387.5666713592882
+26520,1124.0066270980772,543.8525681065795,388.35933224437036
+26580,1122.1622516232142,541.1259551312369,389.12744943864465
+26640,1120.296511796642,538.4647517045279,389.8708095811627
+26700,1118.4094431393582,535.8719144453727,390.58920618777586
+26760,1116.5010815784294,533.3503240173162,391.2824397084904
+26820,1114.571463446309,530.9027819280874,391.95031758289707
+26880,1112.620625480145,528.5320074171012,392.5926542936584
+26940,1110.6486048210797,526.2406344343608,393.2092714180406
+27000,1108.655439013544,524.0312087141178,393.7999976774739
+27060,1106.6411660045424,521.9061849465405,394.364668985129
+27120,1104.6058241429284,519.8679240505327,394.90312849149586
+27180,1102.5494521786782,517.9186905507331,395.4152266279515
+27240,1100.4720892621488,516.0606500616109,395.9008211483064
+27300,1098.3737749433367,514.2958668814489,396.3597771683162
+27360,1096.2545491711212,512.626301698892,396.7919672031486
+27420,1094.1144522925065,511.0538094146056,397.1972712027952
+27480,1091.953525051852,509.5801370804658,397.57557658541765
+27540,1089.7718085900979,508.20692195857066,397.92677826862
+27600,1087.56934444398,506.93568970222793,398.2507786986373
+27660,1085.3461745452416,505.7678526609419,398.54748787743347
+27720,1083.1023412198326,504.70470831128193,398.8168233877
+27780,1080.8378871871055,503.74743781537467,399.05871041574926
+27840,1078.5528555590004,502.8971047086234,399.2730817722953
+27900,1076.2472898392261,502.1546537181116,399.4598779111176
+27960,1073.92123392243,501.5209097130033,399.61904694560087
+28020,1071.5747320933642,500.99657678810655,399.75054466314793
+28080,1069.2078290260415,500.5822374816185,399.8543345374605
+28140,1066.8205697828837,500.27835212792036,399.9303877386853
+28200,1064.4129998138662,500.0852583461423,399.97868314142215
+28260,1061.9851649556504,500.003170665066,399.99920733059184
+28320,1059.5371114307125,500.032180284781,399.99195460516285
+28380,1057.0688858464628,500.17225497536094,399.95692697973436
+28440,1054.5805351943584,500.4232391126711,399.8941341839772
+28500,1052.0721068490093,500.7848538512684,399.8035936599308
+28560,1049.543648567275,501.25669743420053,399.6853305571586
+28620,1046.9952084873566,501.83824563936156,399.539377725762
+28680,1044.4268351278797,502.52885236190696,399.3657757072557
+28740,1041.8385773869709,503.32775033208134,399.16457272330643
+28800,1039.2304845413264,504.2340519676615,398.9358246623382
+28860,1036.602606245275,505.2467503600677,398.67959506400814
+28920,1033.954992529831,506.36472039304687,398.3959551015572
+28980,1031.2876938017434,507.58671999268705,398.08498356203995
+29040,1028.6007608425348,508.91139150737047,397.74676682444016
+29100,1025.8942448075359,510.3372632161369,397.3813988356768
+29160,1023.1681972249107,511.86275096377653,396.9889810845086
+29220,1020.4226699946754,513.4861599208409,396.56962257334317
+29280,1017.6577153877113,515.2056864666108,396.12343978795946
+29340,1014.8733860447678,517.0194201929351,395.65055666515093
+29400,1012.0697349754628,518.9253460267088,395.1511045583001
+29460,1009.246815557271,520.921346468637,394.6252222008918
+29520,1006.4046815345089,523.0052039457928,394.0730556679773
+29580,1003.5433870173116,525.1746032753605,393.4947583355986
+29640,1000.6629864806018,527.427134236821,392.8904908381851
+29700,997.7635347630544,529.7602942497286,392.26042102393404
+29760,994.84508706605,532.1714911540971,391.6047239081869
+29820,991.9076989526263,534.6580460903109,390.9235816248153
+29880,988.951426346419,537.2171964753614,390.21718337562936
+29940,985.9763255305965,539.8460990720977,389.4857253778228
+30000,982.9824531467905,542.5418331480881,388.7294108094694
+30060,979.9698661940148,545.3014037205764,387.9484497530865
+30120,976.938622027583,548.1217448839341,387.14305913727895
+30180,973.8887783580149,550.9997232159046,386.3134626764838
+30240,970.820393249937,553.9321412588638,385.45989080882805
+30300,967.7335251209793,556.9157410722188,384.58258063211986
+30360,964.6282327406609,559.9472078520101,383.68177583798933
+30420,961.5045752292727,563.0231736136827,382.7577266441984
+30480,958.3626120567512,566.1402209339477,381.81068972513685
+30540,955.2024030415466,569.2948867475651,380.84092814052633
+30600,952.0240083494822,572.4836661948403,379.848711262349
+30660,948.8274884926118,575.7030165155497,378.8343147000246
+30720,945.6129043280664,578.9493609849778,377.7980202238534
+30780,942.3803170568938,582.2190928876885,376.7401156867487
+30840,939.1297882228969,585.5085795246132,375.6608949442798
+30900,935.8613797114581,588.8141662490125,374.5606577730466
+30960,932.5751537483652,592.132180526816,373.43970978741135
+31020,929.2711728986228,595.4589360168402,372.2983623546065
+31080,925.9495000652639,598.7907366663417,371.1369325082472
+31140,922.6101984881515,602.1238808173646,369.9557428602668
+31200,919.2533317427736,605.4546653193105,368.7551215113062
+31260,915.8789637390348,608.7793896431718,367.5354019595753
+31320,912.4871587200372,612.0943599928474,366.2969230082183
+31380,909.0779812608598,615.3958934089832,365.0400286712025
+31440,905.6514962673266,618.6803218607674,363.76506807776207
+31500,902.2077689747729,621.943996321146,362.47239537541924
+31560,898.7468649468028,625.1832908209191,361.16236963161293
+31620,895.2688500740395,628.394606477224,359.8353547339598
+31680,891.7737905728734,631.5743754919243,358.49171928917616
+31740,888.2617529841983,634.7190651154611,357.13183652069085
+31800,884.7328041721489,637.8251815717691,355.75608416497295
+31860,881.1870113228225,640.889273939888,354.36484436660885
+31920,877.6244419430047,643.907937987967,352.95850357215215
+31980,874.0451638588803,646.8778199553915,351.53745242278114
+32040,870.449245214745,649.7956202788415,350.10208564578846
+32100,866.8367544717067,652.6580972581313,348.65280194493874
+32160,863.2077604063813,655.462070657767,347.1900038897186
+32220,859.5623321095852,658.2044252402122,345.7140978035156
+32280,855.9005389850179,660.8821142269454,344.22549365075224
+32340,852.2224507479426,663.4921626834528,342.7246049230108
+32400,848.5281374238571,666.031670824408,341.21184852417565
+32460,844.8176693471619,668.4978172353551,339.6876446546308
+32520,841.0911171598212,670.8878620073245,338.1524166945395
+32580,837.3485518100163,673.1991497808947,336.6065910862411
+32640,833.590044550797,675.4291126963169,335.05059721579863
+32700,829.8156669387234,677.5752732464307,333.4848672937263
+32760,826.025490832505,679.6352470291923,331.9098362349352
+32820,822.2195883916312,681.6067453967671,330.325941537925
+32880,818.3980320749984,683.4875779982344,328.7336231632608
+32940,814.5608946395304,685.2756552130872,327.1333234113633
+33000,810.7082491387924,686.968990472816,325.5254867996518
+33060,806.8401689216021,688.5657024680041,323.9105599390686
+33120,802.9567276306296,690.0640172384768,322.2889914100248
+33180,799.0579992010012,691.462270144187,320.66123163779577
+33240,795.1440578588848,692.7589077146423,319.0277327674078
+33300,791.2149781200827,693.9524893748256,317.38894853804334
+33360,787.2708347886087,695.0416890456839,315.7453341570067
+33420,783.3117029552668,696.0252966174141,314.09734617328
+33480,779.3376579962205,696.902219293905,312.4454423507062
+33540,775.3487755715566,697.6714828068418,310.79008154083687
+33600,771.3451316238474,698.3322324981277,309.13172355547493
+33660,767.3268023767017,698.8837342694146,307.47082903895347
+33720,763.2938643333171,699.3253753976935,305.8078593401805
+33780,759.2463942750194,699.6566652160344,304.14327638449106
+33840,755.1844692598048,699.877235658721,302.4775425453358
+33900,751.1081666208711,699.9868416701754,300.81112051584864
+33960,747.0175639651436,699.9853614772162,299.1444731803224
+34020,742.912739171801,699.8727967243501,297.47806348563415
+34080,738.7937703907901,699.6492724719444,295.8123543126503
+34140,734.6607360413395,699.3150370572841,294.1478083476534
+34200,730.5137148104651,698.870461818667,292.48488795381905
+34260,726.3527856514734,698.3160406828438,290.82405504278665
+34320,722.1780277824583,697.652389616262,289.16577094635255
+34380,717.989520684791,696.880245940721,287.51049628832476
+34440,713.7873441016095,696.0004675142026,285.85869085657646
+34500,709.5715780362987,695.0140317777825,284.2108134753284
+34560,705.3423027509679,693.922034669687,282.56732187770206
+34620,701.0995987649213,692.7256894076947,280.92867257857324
+34680,696.8435468531278,691.4263251412428,279.2953207477667
+34740,692.5742280446804,690.0253854747305,277.6677200836216
+34800,688.2917236212551,688.5244268636632,276.04632268696844
+34860,683.9961151155638,686.9251168854166,274.43157893554695
+34920,679.6874843097994,685.2292323865464,272.82393735890577
+34980,675.3659132340824,683.4386575086946,271.223844513812
+35040,671.0314841648963,681.5553815952951,269.6317448602122
+35100,666.6842796235226,679.5814969813944,268.0480806377726
+35160,662.3243823744699,677.5191966690525,266.473291743039
+35220,657.9518754238966,675.3707718908979,264.9078156072452
+35280,653.5668420180327,673.1386095645497,263.3520870748072
+35340,649.1693656415929,670.8251896407329,261.8065382825364
+35400,644.7595300161889,668.4330823480317,260.27159853960376
+35460,640.337419098734,665.9649453373462,258.74769420829074
+35520,635.903117079846,663.4235207292179,257.235248585557
+35580,631.4567083822437,660.8116320673138,255.73468178546196
+35640,626.9982776591387,658.1321811814436,254.24641062246786
+35700,622.5279097926261,655.3881449636054,252.77084849566182
+35760,618.0456898920653,652.5825720606318,251.30840527392428
+35820,613.5517032924625,649.7185794871206,249.85948718208027
+35880,609.046035552845,646.7993491624028,248.42449668806074
+35940,604.5287724546317,643.8281243754078,247.00383239110957
+36000,599.9999999999999,640.8082061813392,245.59788891106302
+36060,595.4598044102496,637.7429497341776,244.20705677873661
+36120,590.9082721241606,634.6357605590762,242.8317223274464
+36180,586.3454897963459,631.4900907687934,241.47226758569636
+36240,581.7715442956046,628.3094352283691,240.12907017106264
+36300,577.1865227032652,625.0973276722985,238.80250318530116
+36360,572.5905123115303,621.8573367785262,237.49293511071178
+36420,567.9836006218121,618.5930622036137,236.20072970778335
+36480,563.3658753430689,615.308130583493,234.92624591415256
+36540,558.7374243901339,612.0061915042427,233.6698377449002
+36600,554.0983358820407,608.6909134473693,232.43185419421712
+36660,549.4486981403484,605.3659797140922,231.21263913846246
+36720,544.7885996874562,602.0350843331682,230.01253124064579
+36780,540.1181292449224,598.7019279567925,228.83186385635508
+36840,535.4373757317708,595.3702137491456,227.6709649411613
+36900,530.7464282628016,592.043643272146,226.53015695952047
+36960,526.0453761468932,588.7259123729858,225.40975679520335
+37020,521.3343088853006,585.4207070780133,224.3100756632738
+37080,516.6133161699546,582.1316994975266,223.23141902364176
+37140,511.8824878817498,578.8625437460307,222.17408649621603
+37200,507.14191408883937,575.616871882484,221.13837177767758
+37260,502.3916850449136,572.3982898750533,220.12456255989997
+37320,497.63189118748704,569.2103735948508,219.1329404500358
+37380,492.86262313617266,566.0566648431147,218.16378089229522
+37440,488.08397169096,562.9406674162359,217.21735309143463
+37500,483.2960278304848,559.8658432130142,216.2939199379801
+37560,478.4988827102954,556.8356083884578,215.39373793520298
+37620,473.69262766112087,553.8533295584091,214.51705712787174
+37680,468.8773541871285,550.9223200592045,213.66412103279555
+37740,464.05315396418507,548.0458362665329,212.83516657118332
+37800,459.22011883810785,545.2270739775731,212.030424002833
+37860,454.37834082291954,542.469164860439,211.25011686217243
+37920,449.5279120990947,539.7751729748693,210.49446189616793
+37980,444.66892501180354,537.1480913680311,209.76366900411762
+38040,439.8014720691571,534.5908387492227,209.05794117934835
+38100,434.9256459404421,532.1062562471598,208.37747445282895
+38160,430.0415394543608,529.6971042534614,207.72245783871935
+38220,425.14924559725927,527.3660593558297,207.09307328186713
+38280,420.24885751136105,525.115711364341,206.48949560726908
+38340,415.34046849299193,522.9485604341431,205.91189247150922
+38400,410.4241719908027,520.867014287765,205.3604243161892
+38460,405.5000616039929,518.8733855401147,204.83524432336168
+38520,400.56823108052487,516.9698891291475,204.33649837298123
+38580,395.6287743153448,515.1586398550494,203.86432500238254
+38640,390.6817853485879,513.4416500306785,203.41885536779927
+38700,385.7273583637941,511.8208272458676,203.00021320793215
+38760,380.76558768611056,510.29797224807703,202.6085148095782
+38820,375.7965677804993,508.87477694174913,202.24386897532952
+38880,370.820393249937,507.55282250858784,201.90637699335085
+38940,365.83715883361293,506.333577650854,201.596132609245
+39000,360.846959405128,505.21839695962393,201.31322200001256
+39060,355.8498899706852,504.20851940982845,201.0577237501149
+39120,350.84604566728444,503.30506698374074,200.82970882964514
+39180,345.83552176090734,502.5090434244458,200.62924057461515
+39240,340.8184136447071,501.8213331206723,200.45637466936228
+39300,335.79481683719075,501.24270012423085,200.31115913108198
+39360,330.76482698039905,500.77378730114486,200.1936342964901
+39420,325.7285398380895,500.4151156174221,200.10383281061831
+39480,320.6860512939084,500.1670835602564,200.04177961774639
+39540,315.6374573495703,500.0299666953058,200.00749195447338
+39600,310.58285412302524,500.0039173605363,200.00097934492965
+39660,305.5223378466332,500.08896449697323,200.02224359813158
+39720,300.4560048653301,500.2850136165478,200.0712788074789
+39780,295.38395163479186,500.59184690707383,200.14807135239545
+39840,290.3062747196013,501.0091234742389,200.25259990211248
+39900,285.2230707914075,501.5363797203411,200.38483542159383
+39960,280.13443662708664,502.17302985934924,200.5447411796011
+40020,275.0404691068969,502.91836656771676,200.73227275889627
+40080,269.94126521263826,503.7715617702231,200.94737806857992
+40140,264.83692202580136,504.7316675599732,201.1899973585602
+40200,259.72753672572327,505.79761725152844,201.4600632361499
+40260,254.6132065877357,506.9682265660046,201.7575006847863
+40320,249.49402898131117,508.24219494681387,202.08222708486826
+40380,244.37010136821326,509.6181070045953,202.43415223670613
+40440,239.2415213006367,511.0944340897227,202.81317838557604
+40500,234.10838641935433,512.6695359906485,203.21920024887385
+40560,228.97079445185398,514.3416627561905,203.65210504535935
+40620,223.82884321048155,516.1089566397436,204.11177252648366
+40680,218.68263059057728,517.9694541632512,204.5980750097911
+40740,213.53225456861023,519.921088298644,205.11087741438539
+40800,208.37781320031687,521.9616907643283,205.65003729845154
+40860,203.21940461882977,524.088994434161,206.21540489882176
+40920,198.05712703281318,526.3006358562488,206.80682317257578
+40980,192.89107872459167,528.5941578787601,207.42412784066252
+41040,187.72135804827718,530.9670123798428,208.06714743353243
+41100,182.5480634279005,533.416563098607,208.73570333876643
+41160,177.371293355533,535.9400885640343,209.4296098506897
+41220,172.19114638941522,538.534785118553,210.14867422195513
+41280,167.0077211520784,541.197770032926,210.89269671708328
+41340,161.8211163284678,543.9260847089852,211.66147066794275
+41400,156.6314306640619,546.7166979666603,212.45478253115715
+41460,151.43876296299547,549.5665094116431,213.272411947421
+41520,146.24321208617707,552.472352879952,214.11413180270924
+41580,141.04487694940505,555.4309999555667,214.97970829136335
+41640,135.84385652148828,558.4391635572233,215.86890098103535
+41700,130.64024982235748,561.493501590391,216.78146287947348
+41760,125.43415592118448,564.5906206603642,217.71714050312912
+41820,120.22567393449167,567.727079842354,218.67567394756782
+41880,115.01490302426869,570.8993945043825,219.6567969596626
+41940,109.80194239608309,574.1040401787388,220.66023701155228
+42000,104.58689129718984,577.3374564776906,221.685715376341
+42060,99.36984901464557,580.5960510491052,222.7329472055214
+42120,94.15091487341408,583.8762035675811,223.80164160809667
+42180,88.93018823447954,587.1742697566614,224.89150173138242
+42240,83.70776849295063,590.4865854376521,226.00222484346307
+42300,78.48375507617175,593.8094706005579,227.133502417283
+42360,73.25824744182863,597.139233492601,228.28502021634566
+42420,68.03134507605319,600.4721747197897,229.45645838199988
+42480,62.8031474915331,603.8045913569771,230.64749152228777
+42540,57.5737542256125,607.1327810618385,231.85778880232843
+42600,52.343264838403286,610.4530461882073,233.0870140362154
+42660,47.11177891088221,613.7616978941876,234.3348257803985
+42720,41.87939604300137,617.0550602404903,235.60087742852792
+42780,36.64621585178719,620.3294742744297,236.88481730773083
+42840,31.412337969447574,623.5813020950523,238.18628877629666
+42900,26.17786204147346,626.8069308948723,239.50493032274127
+42960,20.942887724740128,630.002776973733,240.8403756662253
+43020,15.707514685613562,633.165289720325,242.1922538582955
+43080,10.47184259804875,636.2909555569462,243.56018938592467
+43140,5.235971141696239,639.376301843114,244.94380227581732
+43200,1.4695761589768238e-13,642.4179007336998,246.34270819995652
+43260,0.0,645.41237298729,247.75651858235807
+43320,0.0,648.3563917205538,249.18484070700643
+43380,0.0,651.2466861044371,250.62727782694026
+43440,0.0,654.0800449980793,252.08342927445628
+43500,0.0,656.8533205164197,253.5528905724047
+43560,0.0,659.5634315275208,255.03525354653985
+43620,0.0,662.2073670757343,256.5301064389009
+43680,0.0,664.7821897268944,258.03703402218474
+43740,0.0,667.2850388318346,259.555617715086
+43800,0.0,669.7131337045898,261.0854356985651
+43860,0.0,672.0637767117631,262.6260630330193
+43920,0.0,674.3343562696173,264.1770717763171
+43980,0.0,676.5223497455676,265.73803110266977
+44040,0.0,678.6253262608451,267.3085074223011
+44100,0.0,680.6409493912255,268.88806450188724
+44160,0.0,682.5669797628134,270.4762635857293
+44220,0.0,684.4012775400064,272.072663517627
+44280,0.0,686.1418048028703,273.6768208634199
+44340,0.0,687.7866278112838,275.2882900341597
+44400,0.0,689.333919153341,276.9066234098832
+44460,0.0,690.7819597756195,278.53137146394715
+44520,0.0,692.129140893065,280.16208288789505
+44580,0.0,693.3739657763626,281.79830471681646
+44640,0.0,694.5150514148171,283.43958245516905
+44700,0.0,695.5511300528877,285.085460203023
+44760,0.0,696.4810505986766,286.73548078269823
+44820,0.0,697.3037799028,288.38918586575414
+44880,0.0,698.0184039062264,290.04611610030133
+44940,0.0,698.6241286558037,291.705811238595
+45000,0.0,699.1202811863474,293.36781026487995
+45060,0.0,699.5063102683106,295.03165152344644
+45120,0.0,699.7817870202045,296.6968728468654
+45180,0.0,699.9464053850895,298.36301168436586
+45240,0.0,699.9999824706067,300.02960523031686
+45300,0.0,699.942458752173,301.6961905527833
+45360,0.0,699.773898139113,303.36230472211366
+45420,0.0,699.494487903656,305.0274849395298
+45480,0.0,699.1045384728743,306.6912686656781
+45540,0.0,698.6044830837964,308.3531937491108
+45600,0.0,697.994877302077,310.01279855465725
+45660,0.0,697.2763984047596,311.66962209165416
+45720,0.0,696.449844627815,313.3232041419942
+45780,0.0,695.5161342792954,314.97308538796295
+45840,0.0,694.4763047190867,316.6188075398226
+45900,0.0,693.3315112063922,318.2599134631134
+45960,0.0,692.0830256162312,319.8959473056309
+46020,0.0,690.7322350263748,321.5264546240494
+46080,0.0,689.280640176291,323.1509825101539
+46140,0.0,687.7298537998112,324.76907971664355
+46200,0.0,686.0815988333688,326.3802967824769
+46260,0.0,684.3377065018043,327.98418615771743
+46320,0.0,682.5001142838572,329.5803023278522
+46380,0.0,680.5708637596133,331.16820193754097
+46440,0.0,678.5520983422907,332.7474439137693
+46500,0.0,676.4460608968913,334.3175895883647
+46560,0.0,674.2550912483582,335.87820281984716
+46620,0.0,671.9816235820126,337.42885011457565
+46680,0.0,669.6281837391514,338.9691007471616
+46740,0.0,667.1973864108195,340.4985268801104
+46800,0.0,664.691932232864,342.0167036826641
+46860,0.0,662.1146047855075,343.5232094488059
+46920,0.0,659.4682675007664,345.0176257143986
+46980,0.0,656.7558604811544,346.49953737342213
+47040,0.0,653.9803972332054,347.9685327932774
+47100,0.0,651.1449613194412,349.42420392912703
+47160,0.0,648.2527029325105,350.8661464372374
+47220,0.0,645.3068353952959,352.2939597872948
+47280,0.0,642.3106315908891,353.7072473736604
+47340,0.0,639.2674203263875,355.10561662553664
+47400,0.0,636.1805826345656,356.4886791160116
+47460,0.0,633.0535480175157,357.8560506699534
+47520,0.0,629.8897906364472,359.2073514707223
+47580,0.0,626.6928254518608,360.54220616567386
+47640,0.0,623.466204318402,361.86024397042013
+47700,0.0,620.2135120387182,363.16109877182384
+47760,0.0,616.9383623807161,364.44440922969346
+47820,0.0,613.6443940626343,365.7098188771532
+47880,0.0,610.335266710397,366.95697621966025
+47940,0.0,607.0146567917417,368.18553483263815
+48000,0.0,603.6862535316297,369.39515345770565
+48060,0.0,600.353754813491,370.58549609746643
+48120,0.0,597.0208630708412,371.7562321088416
+48180,0.0,593.6912811738509,372.9070362949109
+48240,0.0,590.368708315424,374.03758899524485
+48300,0.0,587.0568359013654,375.1475761746959
+48360,0.0,583.7593434491976,376.2366895106295
+48420,0.0,580.4798945001878,377.30462647856604
+48480,0.0,577.2221325491216,378.35109043621486
+48540,0.0,573.9896769963527,379.3757907058718
+48600,0.0,570.7861191266164,380.3784426551621
+48660,0.0,567.6150181190851,381.3587677761023
+48720,0.0,564.4798970930904,382.3164937624622
+48780,0.0,561.3842391939068,383.251354585404
+48840,0.0,558.3314837229514,384.16309056737657
+48900,0.0,555.3250223166889,385.05144845424775
+48960,0.0,552.3681951784985,385.9161814856496
+49020,0.0,549.4642873676772,386.7570494635228
+49080,0.0,546.6165251497151,387.5738188188356
+49140,0.0,543.828072411885,388.3662626764635
+49200,0.0,541.10202714814,389.134160918208
+49260,0.0,538.4414180172145,389.87730024393966
+49320,0.0,535.8492009777617,390.5954742308462
+49380,0.0,533.3282560042569,391.28848339077143
+49440,0.0,530.8813838873243,391.9561352256269
+49500,0.0,528.5113031220335,392.59824428086273
+49560,0.0,526.2206468876309,393.21463219698103
+49620,0.0,524.0119601220549,393.80512775907965
+49680,0.0,521.8876966944886,394.3695669444105
+49740,0.0,519.8502166790921,394.90779296794017
+49800,0.0,517.9017837329395,395.41965632590086
+49860,0.0,516.044562581078,395.9050148373176
+49920,0.0,514.2806166114999,396.3637336835026
+49980,0.0,512.6119055827025,396.7956854455036
+50040,0.0,511.04028344637914,397.2007501394976
+50100,0.0,509.56749628766477,397.5788152501185
+50160,0.0,508.1951803852196,397.9297757617111
+50220,0.0,506.92486039331163,398.253534187501
+50280,0.0,505.75794764791175,398.55000059667384
+50340,0.0,504.695738598689,398.81909263935563
+50400,0.0,503.73941336864334,399.06073556948706
+50460,0.0,502.8900344429801,399.2748622655856
+50520,0.0,502.1485454886798,399.4614132493904
+50580,0.0,501.51577030607615,399.62033670238316
+50640,0.0,500.99241191360755,399.75158848018225
+50700,0.0,500.5790517667574,399.85513212480436
+50760,0.0,500.2761491120526,399.93093887479176
+50820,0.0,500.0840404768368,399.97898767320146
+50880,0.0,500.00293929538543,399.9992651734538
+50940,0.0,500.032935671779,399.99176574304033
+51000,0.0,500.17399627979637,399.9564914650879
+51060,0.0,500.4259643999404,399.8934521377802
+51120,0.0,500.7885600935549,399.80266527163616
+51180,0.0,501.2613805138386,399.684156084646
+51240,0.0,501.84390035341113,399.5379574952663
+51300,0.0,502.5354724279342,399.36411011327624
+51360,0.0,503.33532839513845,399.1626622284975
+51420,0.0,504.2425796084579,398.93366979738045
+51480,0.0,505.25621810432426,398.67719642746135
+51540,0.0,506.37511772202174,398.3933133596939
+51600,0.0,507.5980353548622,398.0820994486603
+51660,0.0,508.92361233128526,397.7436411406683
+51720,0.0,510.3503759243554,397.37803244973804
+51780,0.0,511.87674098797197,396.98537493148854
+51840,0.0,513.5010117179811,396.56577765492773
+51900,0.0,515.2213835362269,396.1193571721568
+51960,0.0,517.0359450954535,395.64623748599485
+52020,0.0,518.9426804028267,395.1465500155348
+52080,0.0,520.93947105972,394.62043355963874
+52140,0.0,523.0240986152706,394.06803425838376
+52200,0.0,525.1942470311,393.4895055524683
+52260,0.0,527.447505254448,392.8850081405907
+52320,0.0,529.7813698968743,392.2547099348119
+52380,0.0,532.1932480155441,391.5987860139139
+52440,0.0,534.680459994007,390.9174185747684
+52500,0.0,537.2402425192752,390.2107968817272
+52560,0.0,539.8697516518845,389.4791172140504
+52620,0.0,542.5660659855371,388.72258281138545
+52680,0.0,545.3261898928057,387.941403817313
+52740,0.0,548.147056853302,387.1357972209751
+52800,0.0,551.0255328606049,386.30598679680196
+52860,0.0,553.9584199041686,385.4522030423533
+52920,0.0,556.942459522337,384.5746831142934
+52980,0.0,559.9743364225235,383.6736707625154
+53040,0.0,563.050682164526,382.74941626243475
+53100,0.0,566.1680789028944,381.80217634546943
+53160,0.0,569.3230631841827,380.8322141277279
+53220,0.0,572.5121297948751,379.83979903692295
+53280,0.0,575.731735655708,378.82520673753163
+53340,0.0,578.9783037580584,377.7887190542246
+53400,0.0,582.248227138032,376.73062389358245
+53460,0.0,585.5378728838276,375.6512151641242
+53520,0.0,588.8435861719347,374.55079269466734
+53580,0.0,592.1616943276694,373.42966215104445
+53640,0.0,595.4885109055489,372.2881349511976
+53700,0.0,598.8203397849608,371.1265281786758
+53760,0.0,602.1534792765856,369.9451644945576
+53820,0.0,605.4842262350014,368.74437204782606
+53880,0.0,608.808880172912,367.52448438421726
+53940,0.0,612.1237473724154,366.2858403535719
+54000,0.0,615.4251449887583,365.02878401571166
+54060,0.0,618.7094051420041,363.7536645448698
+54120,0.0,621.9728789920786,362.46083613270054
+54180,0.0,625.2119407926627,361.1506578898941
+54240,0.0,628.4229919194241,359.82349374642706
+54300,0.0,631.6024648681209,358.479712350472
+54360,0.0,634.746827218126,357.1196869659989
+54420,0.0,637.8525855569775,355.7437953690913
+54480,0.0,640.9162893615868,354.35241974301294
+54540,0.0,643.9345348318001,352.9459465720464
+54600,0.0,646.9039686720465,351.52476653414044
+54660,0.0,649.8212918168789,350.08927439238914
+54720,0.0,652.6832630962609,348.63986888538
+54780,0.0,655.4867028365354,347.1769526164337
+54840,0.0,658.228496393066,345.7009319417744
+54900,0.0,660.9055976106356,344.21221685765397
+54960,0.0,663.515032207747,342.71122088646786
+55020,0.0,666.0539010810759,341.19836096189033
+55080,0.0,668.5193835263987,339.6740573130612
+55140,0.0,670.9087403724168,338.1387333478599
+55200,0.0,673.2193170240002,336.5928155352931
+55260,0.0,675.4485464114626,335.0367332870356
+55320,0.0,677.5939518425989,333.47091883815045
+55380,0.0,679.6531497543089,331.89580712702775
+55440,0.0,681.623852360757,330.3118356745702
+55500,0.0,683.5038701951199,328.7194444626633
+55560,0.0,685.2911145421034,327.11907581195993
+55620,0.0,686.9835997585209,325.5111742590174
+55680,0.0,688.5794454793611,323.896186432817
+55740,0.0,690.0768787068871,322.27456093070407
+55800,0.0,691.4742357804531,320.6467481937797
+55860,0.0,692.7699642248442,319.01320038178187
+55920,0.0,693.962624475089,317.3743712474883
+55980,0.0,695.0508914758292,315.730716010676
+56040,0.0,696.0335561534656,314.0826912316758
+56100,0.0,696.90952675945,312.43075468455146
+56160,0.0,697.6778300832261,310.7753652299444
+56220,0.0,698.3376125334748,309.11698268761427
+56280,0.0,698.888141086461,307.4560677087153
+56340,0.0,699.3288041004294,305.7930816478395
+56400,0.0,699.6591119951424,304.1284864348666
+56460,0.0,699.8786977958097,302.4627444466522
+56520,0.0,699.9873175407982,300.79631837859375
+56580,0.0,699.9848505526772,299.1296711161058
+56640,0.0,699.8712995722906,297.4632656060451
+56700,0.0,699.6467907557125,295.7975647281159
+56760,0.0,699.311573534087,294.1330311662962
+56820,0.0,698.8660203365075,292.47012728031706
+56880,0.0,698.3106261762453,290.80931497723185
+56940,0.0,697.6460081007858,289.15105558311274
+57000,0.0,696.8729045062834,287.4958097149063
+57060,0.0,695.9921743171975,285.8440371524886
+57120,0.0,695.0047960320204,284.1961967109503
+57180,0.0,693.9118666361574,282.552746113152
+57240,0.0,692.7146003831664,280.91414186258106
+57300,0.0,691.4143274457134,279.280839116549
+57360,0.0,690.0124924377386,277.6532915597605
+57420,0.0,688.510652809479,276.03195127829446
+57480,0.0,686.9104771171279,274.41726863402573
+57540,0.0,685.2137431690562,272.809692139529
+57600,0.0,683.422336050651,271.2096683334935
+57660,0.0,681.5382460299724,269.61764165668865
+57720,0.0,679.5635663465466,268.0340543285097
+57780,0.0,677.5004908857629,266.45934622414387
+57840,0.0,675.3513117414458,264.8939547523833
+57900,0.0,673.1184166693214,263.338314734127
+57960,0.0,670.8042864342005,261.7928582815991
+58020,0.0,668.4114920538308,260.25801467832207
+58080,0.0,665.9426919424709,258.734210259872
+58140,0.0,663.4006289573736,257.22186829545586
+58200,0.0,660.7881273514465,255.72140887033922
+58260,0.0,658.1080896354872,254.23324876915947
+58320,0.0,655.3634933534655,252.7578013601534
+58380,0.0,652.5573877744506,251.29547648033633
+58440,0.0,649.6928905048462,249.8466803216608
+58500,0.0,646.7731840247073,248.41181531818907
+58560,0.0,643.8015121519743,246.99128003430678
+58620,0.0,640.7811764385666,245.58546905401397
+58680,0.0,637.7155325023334,244.19477287132207
+58740,0.0,634.6079862989304,242.81957778178375
+58800,0.0,631.4619903377782,241.46026577519254
+58860,0.0,628.2810398462935,240.1172144294763
+58920,0.0,625.068668886667,238.79079680581805
+58980,0.0,621.8284464294852,237.48138134502858
+59040,0.0,618.5639723885791,236.18933176520525
+59100,0.0,615.2788736214881,234.91500696070102
+59160,0.0,611.9767998999939,233.65876090243486
+59220,0.0,608.6614198551907,232.4209425395673
+59280,0.0,605.3364169016081,231.20189570257335
+59340,0.0,602.0054851449056,230.00195900773565
+59400,0.0,598.6723252776941,228.8214657630877
+59460,0.0,595.3406404680314,227.6607438758289
+59520,0.0,592.0141322451746,226.52011576124204
+59580,0.0,588.6964963871517,225.3998982531363
+59640,0.0,585.3914188147188,224.30040251583856
+59700,0.0,582.1025714962735,223.2219339577616
+59760,0.0,578.8336083682675,222.16479214657016
+59820,0.0,575.5881612756546,221.12927072597037
+59880,0.0,572.3698359368778,220.1156573341427
+59940,0.0,569.1822079378893,219.12423352384513
+60000,0.0,566.028818759643,218.1552746842055
diff --git a/benchmarks/native_study.py b/benchmarks/native_study.py
new file mode 100644
index 00000000..31aba37e
--- /dev/null
+++ b/benchmarks/native_study.py
@@ -0,0 +1,269 @@
+"""Benchmark annual simulation throughput and recorder overhead.
+
+Run with .venv/bin/python benchmarks/native_study.py.
+Writes raw timings and profiles under results/benchmark-native.
+No simulation models or engine methods are replaced.
+"""
+
+from __future__ import annotations
+
+import argparse
+from concurrent.futures import ProcessPoolExecutor
+import cProfile
+import io
+import json
+import multiprocessing
+import os
+from pathlib import Path
+import platform
+import pstats
+import statistics
+import sys
+import tempfile
+import time
+
+import numpy as np
+import pandas as pd
+from loguru import logger
+
+ROOT = Path(__file__).resolve().parents[1]
+sys.path.insert(0, str(ROOT / "examples"))
+
+import vessim as vs # noqa: E402
+from infrastructure_study import inputs # noqa: E402
+
+CONFIGS = [
+ dict(wind_kw=0, solar_kw=0, battery_kwh=0, battery_c_rate=0.5),
+ dict(wind_kw=1500, solar_kw=2000, battery_kwh=4000, battery_c_rate=0.25),
+ dict(wind_kw=5000, solar_kw=6000, battery_kwh=12000, battery_c_rate=1.0),
+]
+
+
+class Observer(vs.Controller):
+ """Request normal detached state observations without recording metrics."""
+
+ def step(self, now, microgrid_states):
+ pass
+
+
+def run_one(config, seed, mode="metrics", step_seconds=3600, output=None):
+ started = time.perf_counter()
+ frame = inputs(seed=seed, step_seconds=step_seconds)
+ hours = (frame.index.to_numpy() / 3600) % 24
+ frame["price"] = np.where((hours >= 17) & (hours < 21), 0.30, 0.15)
+ frame["export_price"] = 0.04
+ input_done = time.perf_counter()
+ battery = (
+ vs.SimpleBattery("battery", config["battery_kwh"] * 1000, c_rate=config["battery_c_rate"])
+ if config["battery_kwh"]
+ else None
+ )
+ with vs.Environment("2026-01-01", step_size=step_seconds, backend="native") as env:
+ env.add_microgrid(
+ name="site",
+ actors=[
+ vs.Producer("wind", vs.Trace(frame.wind_cf * config["wind_kw"] * 1000)),
+ vs.Producer("solar", vs.Trace(frame.solar_cf * config["solar_kw"] * 1000)),
+ vs.Consumer("datacenter", vs.Trace(frame.load_kw * 1000)),
+ ],
+ dispatchables=([battery] if battery is not None else []) + [vs.GridConnection()],
+ grid_signals={
+ key: vs.Trace(frame[key]) for key in ("carbon", "price", "export_price")
+ },
+ )
+ recorder = None
+ if mode == "observer":
+ env.add_controller(Observer())
+ elif mode != "none":
+ custom = (
+ {
+ "grid_emissions": vs.GridImportEmissions("site", "carbon"),
+ "electricity_cost": vs.ElectricityImportCost("site", "price"),
+ "export_revenue": vs.ElectricityExportRevenue("site", "export_price"),
+ }
+ if mode != "physical"
+ else {}
+ )
+ recorder = env.add_controller(
+ vs.Recorder(
+ output,
+ timeseries=mode in ("trajectory_memory", "trajectory_csv"),
+ custom_metrics=custom,
+ parameters={**config, "seed": seed},
+ )
+ )
+ setup_done = time.perf_counter()
+ result = env.run(365 * 86400)
+ run_done = time.perf_counter()
+ assert result.steps == 365 * 86400 // step_seconds
+ checksum = recorder.metrics["microgrids"]["site"] if recorder is not None else None
+ return {
+ "config": config,
+ "seed": seed,
+ "mode": mode,
+ "step_seconds": step_seconds,
+ "input_seconds": input_done - started,
+ "setup_seconds": setup_done - input_done,
+ "run_seconds": run_done - setup_done,
+ "total_seconds": time.perf_counter() - started,
+ "steps": result.steps,
+ "metrics": checksum,
+ }
+
+
+def worker(task):
+ config, seed, output = task
+ return run_one(config, seed, output=output)
+
+
+def configure_logging(quiet):
+ if quiet:
+ logger.disable("vessim")
+
+
+def summary(rows):
+ return {
+ name: {
+ "median": statistics.median(row[name] for row in rows),
+ "min": min(row[name] for row in rows),
+ "max": max(row[name] for row in rows),
+ }
+ for name in ("input_seconds", "setup_seconds", "run_seconds", "total_seconds")
+ }
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--output", type=Path, default=ROOT / "results/benchmark-native")
+ parser.add_argument("--repeats", type=int, default=6)
+ parser.add_argument("--jobs", type=int, default=100)
+ parser.add_argument("--workers", nargs="+", type=int, default=[1, 2, 4, 8])
+ parser.add_argument("--quiet", action="store_true", help="Disable Vessim log messages")
+ parser.add_argument("--throughput-only", action="store_true")
+ args = parser.parse_args()
+ if args.repeats < 1 or args.jobs < 1 or any(w < 1 for w in args.workers):
+ parser.error("repeats, jobs and workers must be positive")
+ configure_logging(args.quiet)
+ args.output.mkdir(parents=True, exist_ok=True)
+ report = {
+ "system": {
+ "python": sys.version,
+ "platform": platform.platform(),
+ "machine": platform.machine(),
+ "logical_cpus": os.cpu_count(),
+ "numpy": np.__version__,
+ "pandas": pd.__version__,
+ },
+ "scope": "365 days, three actors, battery where configured, grid, six traces",
+ "input_source": "examples/infrastructure_study.py:inputs",
+ "logging": "disabled" if args.quiet else "default",
+ "ablations": {},
+ "parallel": [],
+ }
+
+ def save():
+ (args.output / "timings.json").write_text(json.dumps(report, indent=2) + "\n")
+
+ # Warm imports and allocation paths before collecting unprofiled timings.
+ run_one(CONFIGS[1], 9999)
+ with tempfile.TemporaryDirectory(prefix="native-benchmark-", dir=args.output) as scratch:
+ modes = [
+ "none",
+ "observer",
+ "physical",
+ "metrics",
+ "metrics_disk",
+ "trajectory_memory",
+ "trajectory_csv",
+ ]
+ rows = {mode: [] for mode in modes}
+ # Rotate execution order so later modes do not always get a warmer CPU.
+ for repeat in range(0 if args.throughput_only else args.repeats):
+ order = modes[repeat % len(modes) :] + modes[: repeat % len(modes)]
+ for mode in order:
+ output = (
+ Path(scratch) / f"{mode}-{repeat}"
+ if mode in ("metrics_disk", "trajectory_csv")
+ else None
+ )
+ rows[mode].append(
+ run_one(CONFIGS[repeat % len(CONFIGS)], 10000 + repeat, mode, output=output)
+ )
+ print(f"Ablations {repeat + 1}/{args.repeats}", flush=True)
+ for mode in modes:
+ if not rows[mode]:
+ continue
+ report["ablations"][mode] = {"summary": summary(rows[mode]), "runs": rows[mode]}
+ print(mode, json.dumps(summary(rows[mode])), flush=True)
+ save()
+
+ if not args.throughput_only:
+ fine = [run_one(CONFIGS[1], 10000 + i, step_seconds=300) for i in range(3)]
+ report["five_minute"] = {"summary": summary(fine), "runs": fine}
+ save()
+ print("Five-minute", json.dumps(summary(fine)), flush=True)
+
+ for mode in (() if args.throughput_only else ("none", "metrics", "trajectory_csv")):
+ profiler = cProfile.Profile()
+ for i in range(3):
+ profiler.runcall(
+ run_one,
+ CONFIGS[1],
+ 10000 + i,
+ mode,
+ output=(
+ Path(scratch) / f"profile-{mode}-{i}" if mode == "trajectory_csv" else None
+ ),
+ )
+ profiler.dump_stats(str(args.output / f"{mode}.prof"))
+ stream = io.StringIO()
+ stats = pstats.Stats(profiler, stream=stream)
+ stats.strip_dirs().sort_stats("cumulative").print_stats(50)
+ stats.sort_stats("tottime").print_stats(40)
+ (args.output / f"{mode}-profile.txt").write_text(stream.getvalue())
+ print(f"Profiled {mode}", flush=True)
+
+ configs = CONFIGS
+ report["parallel_configuration_source"] = "three reference configurations"
+ reference = None
+ for workers in args.workers:
+ tasks = [
+ (configs[i % len(configs)], 10000 + i, Path(scratch) / f"parallel-{workers}-{i}")
+ for i in range(args.jobs)
+ ]
+ started = time.perf_counter()
+ if workers == 1:
+ results = list(map(worker, tasks))
+ else:
+ with ProcessPoolExecutor(
+ max_workers=workers,
+ mp_context=multiprocessing.get_context("spawn"),
+ initializer=configure_logging,
+ initargs=(args.quiet,),
+ ) as pool:
+ results = list(pool.map(worker, tasks, chunksize=1))
+ elapsed = time.perf_counter() - started
+ metrics = [r["metrics"] for r in results]
+ if reference is None:
+ reference = metrics
+ assert metrics == reference, "Parallel execution changed results"
+ item = {
+ "workers": workers,
+ "runs": args.jobs,
+ "wall_seconds": elapsed,
+ "runs_per_second": args.jobs / elapsed,
+ "includes_pool_startup": True,
+ "includes_recorded_json": True,
+ "timings": results,
+ }
+ report["parallel"].append(item)
+ save()
+ print(
+ "Throughput",
+ json.dumps({k: v for k, v in item.items() if k != "timings"}),
+ flush=True,
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/benchmarks/run.py b/benchmarks/run.py
new file mode 100644
index 00000000..ab4aa999
--- /dev/null
+++ b/benchmarks/run.py
@@ -0,0 +1,164 @@
+"""Reproducible backend comparison using public Vessim APIs.
+
+Run from the repository: python benchmarks/run.py --backend native --output result.json
+"""
+
+from __future__ import annotations
+
+import argparse
+import cProfile
+import gc
+import hashlib
+import importlib.metadata
+import json
+import platform
+import statistics
+import sys
+import tempfile
+import time
+import tracemalloc
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
+import numpy as np
+import pandas as pd
+from loguru import logger
+import vessim as vs
+
+CASES = ("static", "traces", "csv", "many_grids", "mixed_rates", "feedback")
+
+
+def environment_info():
+ versions = {}
+ for package in ("vessim", "numpy", "pandas", "mosaik"):
+ try:
+ versions[package] = importlib.metadata.version(package)
+ except importlib.metadata.PackageNotFoundError:
+ versions[package] = None
+ return {"python": platform.python_version(), "platform": platform.platform(), **versions}
+
+
+class Feedback(vs.Controller):
+ def start(self, environment):
+ self.grids = environment.microgrids
+
+ def step(self, now, microgrid_states):
+ for grid in self.grids.values():
+ grid.policy.charge_power = 100 if now.minute % 2 else -100
+
+
+def build(backend, case, traces, directory):
+ env = vs.Environment(sim_start="2026-01-01", step_size=60, backend=backend)
+ for i in range(10 if case == "many_grids" else 1):
+ traced = case != "static"
+ env.add_microgrid(
+ name=f"grid{i}",
+ actors=[
+ vs.Producer(
+ "solar",
+ traces["solar"] if traced else 900,
+ step_size=180 if case == "mixed_rates" else None,
+ ),
+ vs.Consumer("load", traces["load"] if traced else 600),
+ ],
+ dispatchables=[
+ vs.SimpleBattery("battery", 1500, initial_soc=0.5, min_soc=0.1),
+ vs.GridConnection(),
+ ],
+ grid_signals={"carbon": traces["carbon"]} if traced else None,
+ )
+ if case == "feedback":
+ env.add_controller(Feedback())
+ memory = vs.Recorder(metrics=False)
+ env.add_controller(memory)
+ if case == "csv":
+ env.add_controller(vs.Recorder(directory, metrics=False))
+ return env, memory
+
+
+def digest(memory):
+ rows = [(str(t), value) for t, value in memory.log.items()]
+ return hashlib.sha256(json.dumps(rows, sort_keys=True).encode()).hexdigest()
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--backend", choices=["native", "mosaik"], default="native")
+ parser.add_argument("--steps", type=int, default=1000)
+ parser.add_argument("--repeats", type=int, default=3)
+ parser.add_argument("--output", type=Path, required=True)
+ parser.add_argument("--profile", type=Path, help="Directory for per-workload cProfile files")
+ args = parser.parse_args()
+ if args.steps < 1 or args.repeats < 1:
+ parser.error("steps and repeats must be positive")
+ logger.remove()
+ results = {
+ "backend": args.backend,
+ **environment_info(),
+ "cases": {},
+ }
+ with tempfile.TemporaryDirectory() as tmp:
+ directory = Path(tmp)
+ if args.steps == 1000:
+ (directory / "input.csv").write_bytes(
+ Path(__file__).with_name("input.csv").read_bytes()
+ )
+ else:
+ offsets = np.arange(args.steps + 1) * 60
+ pd.DataFrame(
+ {
+ "solar": 1200 * np.maximum(0, np.sin(offsets / 86400 * 2 * np.pi)),
+ "load": 600 + 100 * np.cos(offsets / 1800),
+ "carbon": 300 + 100 * np.sin(offsets / 3600),
+ },
+ index=offsets,
+ ).to_csv(directory / "input.csv")
+ t0 = time.perf_counter()
+ traces = {
+ c: vs.Trace.from_csv(directory / "input.csv", column=c)
+ for c in ("solar", "load", "carbon")
+ }
+ results["trace_load_seconds"] = time.perf_counter() - t0
+ for case in CASES:
+ steps = max(1, args.steps // 5) if case == "many_grids" else args.steps
+ timings, setups = [], []
+ checksums = set()
+ for repeat in range(args.repeats):
+ gc.collect()
+ t0 = time.perf_counter()
+ env, memory = build(args.backend, case, traces, directory / f"{case}-{repeat}")
+ setups.append(time.perf_counter() - t0)
+ t0 = time.perf_counter()
+ env.run(until=steps * 60, print_progress=False)
+ timings.append(time.perf_counter() - t0)
+ checksum = digest(memory)
+ checksums.add(checksum)
+ if len(checksums) != 1:
+ raise RuntimeError(f"Repeated {case} runs produced different trajectories")
+ gc.collect()
+ tracemalloc.start()
+ env, memory = build(args.backend, case, traces, directory / f"{case}-memory")
+ env.run(until=steps * 60, print_progress=False)
+ _, peak = tracemalloc.get_traced_memory()
+ tracemalloc.stop()
+ results["cases"][case] = {
+ "steps": steps,
+ "run_seconds": statistics.median(timings),
+ "setup_seconds": statistics.median(setups),
+ "runs": timings,
+ "peak_traced_bytes": peak,
+ "trajectory_sha256": checksum,
+ }
+ print(case, results["cases"][case], flush=True)
+ if args.profile:
+ args.profile.mkdir(parents=True, exist_ok=True)
+ env, _ = build(args.backend, case, traces, directory / f"{case}-profile")
+ profiler = cProfile.Profile()
+ profiler.runcall(env.run, until=steps * 60, print_progress=False)
+ profiler.dump_stats(str(args.profile / f"{case}.prof"))
+ args.output.parent.mkdir(parents=True, exist_ok=True)
+ args.output.write_text(json.dumps(results, indent=2) + "\n")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/benchmarks/studies.py b/benchmarks/studies.py
new file mode 100644
index 00000000..9d71f595
--- /dev/null
+++ b/benchmarks/studies.py
@@ -0,0 +1,79 @@
+"""Measure multi-run throughput and bounded versus trajectory result retention."""
+
+from __future__ import annotations
+
+import argparse
+import gc
+import json
+import sys
+import time
+import tracemalloc
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
+import pandas as pd
+from loguru import logger
+import vessim as vs
+from benchmarks.run import environment_info
+
+
+def scenario(trace, steps, backend, output):
+ env = vs.Environment("2026-01-01", step_size=60, backend=backend)
+ env.add_microgrid(
+ [vs.Consumer("load", signal=trace)],
+ [vs.SimpleBattery("battery", 1500, initial_soc=0.5), vs.GridConnection()],
+ name="site",
+ )
+ if output == "metrics":
+ env.add_controller(vs.Recorder(timeseries=False))
+ elif output == "trajectory":
+ env.add_controller(vs.Recorder())
+ env.run(steps * 60)
+ return env
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--backend", choices=["native", "mosaik"], default="native")
+ parser.add_argument("--runs", type=int, default=100)
+ parser.add_argument("--steps", type=int, default=288)
+ parser.add_argument("--memory-steps", nargs="+", type=int, default=[1000, 10000])
+ parser.add_argument("--output", type=Path, required=True)
+ args = parser.parse_args()
+ if args.runs < 1 or args.steps < 1 or any(n < 1 for n in args.memory_steps):
+ parser.error("runs, steps, and memory-steps must be positive")
+ logger.remove()
+ data = pd.Series([500, 700, 600], index=[0, 3600, 7200])
+ horizon = max(args.steps, *args.memory_steps) * 60
+ if horizon > data.index[-1]:
+ data.loc[horizon] = data.iloc[-1]
+ trace = vs.Trace(data)
+ result = {
+ **environment_info(),
+ "backend": args.backend,
+ "runs": args.runs,
+ "steps_per_run": args.steps,
+ }
+ scenario(trace, 1, args.backend, "metrics") # Optional import warm-up.
+ t0 = time.perf_counter()
+ for _ in range(args.runs):
+ scenario(trace, args.steps, args.backend, "metrics")
+ result["total_seconds"] = time.perf_counter() - t0
+ result["scenarios_per_second"] = args.runs / result["total_seconds"]
+ result["memory"] = []
+ for output in ("none", "metrics", "trajectory"):
+ for steps in args.memory_steps:
+ gc.collect()
+ tracemalloc.start()
+ env = scenario(trace, steps, args.backend, output)
+ _, peak = tracemalloc.get_traced_memory()
+ tracemalloc.stop()
+ result["memory"].append({"output": output, "steps": steps, "peak_traced_bytes": peak})
+ del env
+ args.output.parent.mkdir(parents=True, exist_ok=True)
+ args.output.write_text(json.dumps(result, indent=2) + "\n")
+ print(json.dumps(result, indent=2))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/datasets/replay_example.csv b/datasets/replay_example.csv
new file mode 100644
index 00000000..d0ad05c3
--- /dev/null
+++ b/datasets/replay_example.csv
@@ -0,0 +1,8 @@
+seconds,load,solar
+0,700,0
+3600,700,1200
+7200,700,2400
+10800,700,1200
+14400,700,0
+18000,700,0
+21600,700,0
diff --git a/docs/about.md b/docs/about.md
index b4910aa2..78c85df4 100644
--- a/docs/about.md
+++ b/docs/about.md
@@ -1,21 +1,21 @@
# About Vessim
-Research on microgrids and energy management, and in particular on energy- and carbon-aware computing where Vessim originated, often relies on simulation, as physical testbeds are rare and costly.
-Vessim addresses this gap by simplifying and unifying simulation-based evaluations and enabling continuous testing of emerging applications.
+Vessim is developed by [Philipp Wiesner](https://philippwiesner.org) at the [University of Cambridge](https://www.cam.ac.uk/) in the [Machine Learning Systems Lab](https://mlsys.cst.cam.ac.uk/).
+For feedback, ideas, or questions, feel free to post on [GitHub Discussions](https://github.com/dos-group/vessim/discussions).
+If you’re interested in collaborating or want advice on using Vessim in a commercial project, drop me an email at _pw560 at cam.ac.uk_.
-Vessim is developed by [Philipp Wiesner](https://philippwiesner.org) at the [Distributed and Operating Systems (DOS)](https://www.dos.tu-berlin.de) group at TU Berlin.
-Active development for Vessim began in March 2023 with initial funding from the [German Federal Ministry of Education and Research (BMBF)](https://www.bmbf.de/) as part of the Software Campus project [SYNERGY](https://softwarecampus.de/en/projekt/synergy-synergies-of-distributed-artificial-intelligence-and-renewable-energy-generation/).
-We thank everyone who has contributed to Vessim over the past years, especially Marvin Steinke, Paul Kilian, and Amanda Malkowski.
+Development of Vessim began in March 2023 at the [Distributed and Operating Systems (DOS)](https://www.dos.tu-berlin.de) group at TU Berlin, with initial funding from the [German Federal Ministry of Education and Research (BMBF)](https://www.bmbf.de/) as part of the Software Campus project [SYNERGY](https://softwarecampus.de/en/projekt/synergy-synergies-of-distributed-artificial-intelligence-and-renewable-energy-generation/).
+
+We thank everyone who has contributed to Vessim over the past years, especially Marvin Steinke, Paul Kilian, Amanda Malkowski, and Julius Irion.
## Roadmap
We are currently working on the following aspects and features:
-- Datasets: We are working on better integrating exiting data sources like electricity prices, carbon intensity data, or weather forecasts into Vessim.
+- Probabilistic modelling: Better support for Monte Carlo simulation runs with inputs sampled from probability distributions.
+- Datasets: We are working on better integrating existing data sources like electricity prices, carbon intensity data, or weather forecasts into Vessim.
- System Advisor Model (SAM): We are working on integrating NREL's [SAM](https://sam.nrel.gov/) as a subsystem in Vessim, allowing for better simulation of solar arrays, wind farms, and other types of renewable energy generators.
- Battery degradation: We are working on integrating NREL's [BLAST-Lite](https://github.com/NREL/BLAST-Lite) for modeling battery lifetime and degradation
-- Improved modeling: We are working on more sophisticated modeling of e.g. demand response signals, time-varying PUE, ...
-- Calibration: We are working on a methodology for calibrating Vessim simulations on real hardware testbeds.
## License
diff --git a/docs/api_reference/actor.md b/docs/api_reference/actor.md
index 58cf32fc..b00adf50 100644
--- a/docs/api_reference/actor.md
+++ b/docs/api_reference/actor.md
@@ -1,3 +1,14 @@
-# Actor
+# Actors
+
+[`Consumer`](#vessim.Consumer) models demand, [`Producer`](#vessim.Producer) models generation, and [`Actor`](#vessim.Actor) supports signed inputs that can change direction.
+See [Actors and signals](../concepts/signals.md) for examples.
+
+::: vessim.Consumer
+ options:
+ inherited_members: [signal]
+
+::: vessim.Producer
+ options:
+ inherited_members: [signal]
::: vessim.Actor
diff --git a/docs/api_reference/cli.md b/docs/api_reference/cli.md
deleted file mode 100644
index b0f01fee..00000000
--- a/docs/api_reference/cli.md
+++ /dev/null
@@ -1,34 +0,0 @@
-# Command-Line Interface
-
-Vessim ships with a small CLI for inspecting experiment results in the browser.
-
-## `vessim view`
-
-Launches the experiment viewer on a results directory written by [`CsvLogger`](controller.md).
-
-```
-usage: vessim view [-h] [-p PORT] [--no-browser] directory
-
-positional arguments:
- directory Path to a results directory
-
-options:
- -p PORT, --port PORT Port to serve on (default: 8710)
- --no-browser Do not open the browser automatically
-```
-
-### Single-experiment mode
-
-Pass a directory that contains a `metadata.yaml` and `timeseries.csv` directly. The viewer opens that single experiment:
-
-```console
-vessim view results/basic_example
-```
-
-### Multi-experiment mode
-
-Pass a parent directory that contains several experiment subdirectories. The viewer adds a sidebar from which you can browse runs:
-
-```console
-vessim view results/
-```
diff --git a/docs/api_reference/controller.md b/docs/api_reference/controller.md
index 255be86c..c8ae7509 100644
--- a/docs/api_reference/controller.md
+++ b/docs/api_reference/controller.md
@@ -1,10 +1,20 @@
-# Controller
+# Controllers
-Controllers run at every simulation step.
+Controllers run after dispatch at each simulation step.
They can observe the state of all microgrids and modify their components.
See the [Controllers](../concepts/controllers.md) concept page for usage examples.
::: vessim.Controller
-::: vessim.CsvLogger
-::: vessim.MemoryLogger
+
::: vessim.Api
+
+### REST API commands
+
+Install `vessim[live]` to use `Api`.
+Its running server exposes interactive endpoint documentation at `http://localhost:8700/docs` with the default port.
+
+API commands are acknowledged when queued.
+Changes appear in observations after the command is applied and the affected component is sampled again.
+Assigning an actor's `signal` through the API replaces its input with a static signal.
+
+For recording and custom metric APIs, see [Recording and metrics](recorder.md).
diff --git a/docs/api_reference/dispatch_policy.md b/docs/api_reference/dispatch_policy.md
index ffa9c9d3..23fea286 100644
--- a/docs/api_reference/dispatch_policy.md
+++ b/docs/api_reference/dispatch_policy.md
@@ -1,11 +1,10 @@
-# Dispatch Policy
+# Dispatch policy
-A dispatch policy receives the microgrid's current power delta and allocates it
-across [Dispatchables](dispatchable.md), returning the remainder to be exchanged
-with the public grid. Subclass [`DispatchPolicy`](#vessim.DispatchPolicy) to
-implement custom strategies. See the
-[Dispatchables and Dispatch Policies](../concepts/dispatchables.md) concept page
-for usage examples.
+A dispatch policy allocates the microgrid's power surplus or deficit across [dispatchables](dispatchable.md).
+Each `apply()` call assigns feasible resource setpoints and returns `None`.
+The microgrid validates bounds and power balance.
+
+See [Dispatchables and policies](../concepts/dispatchables.md) for examples.
-::: vessim.DispatchPolicy
::: vessim.DefaultDispatchPolicy
+::: vessim.DispatchPolicy
diff --git a/docs/api_reference/dispatchable.md b/docs/api_reference/dispatchable.md
index 769cf0a9..e219cd89 100644
--- a/docs/api_reference/dispatchable.md
+++ b/docs/api_reference/dispatchable.md
@@ -1,12 +1,21 @@
-# Dispatchable
+# Dispatchables
-Controllable energy resources whose power is allocated by a
-[Dispatch Policy](dispatch_policy.md). Subclass [`Dispatchable`](#vessim.Dispatchable)
-to model any device with a controllable power setpoint. See the
-[Dispatchables and Dispatch Policies](../concepts/dispatchables.md) concept page for
-usage examples and a guide to writing custom dispatchables.
+A [dispatch policy](dispatch_policy.md) assigns power to these resources.
+Use the built-in grid and battery models, or subclass `Dispatchable` for a custom resource.
+See [Dispatchables and policies](../concepts/dispatchables.md) for examples.
-::: vessim.Dispatchable
-::: vessim.Storage
+Positive power means absorption or export.
+Negative power means supply or import.
+Battery observations include `current_power` in W, `charge_level` in Wh, and `soc` between 0 and 1.
+
+::: vessim.GridConnection
+ options:
+ inherited_members: [set_power]
::: vessim.SimpleBattery
+ options:
+ inherited_members: [set_power]
::: vessim.ClcBattery
+ options:
+ inherited_members: [set_power]
+::: vessim.Dispatchable
+::: vessim.Storage
diff --git a/docs/api_reference/environment.md b/docs/api_reference/environment.md
index 6defb6f2..7991590b 100644
--- a/docs/api_reference/environment.md
+++ b/docs/api_reference/environment.md
@@ -1 +1,32 @@
+# Environment and RunResult
+
+Create and run simulations with `Environment`.
+Its `run()` method returns a `RunResult` containing the final state.
+See [Getting Started](../getting_started.md#run-your-first-simulation) for a walkthrough or [Connect External Simulators](../concepts/coupling.md) for Mosaik.
+
::: vessim.Environment
+
+::: vessim.RunResult
+
+## Time and final state
+
+Actor sampling intervals must be whole multiples of the environment's `step_size`.
+Inputs are held between samples.
+Controller changes to an actor's input take effect at its next sample, while changes to policy parameters affect the next dispatch.
+
+Ticks occur strictly before `until`.
+The final integration interval is shortened as needed to reach that horizon.
+`RunResult.states` contains the final resource state, while `last_time` and recorded observations refer to the last tick.
+Energy metrics include the final interval.
+
+## Mosaik connections
+
+With `backend="mosaik"`, `env.world` exposes the Mosaik world.
+Call `env.prepare()` before connecting to microgrids through `env.entities[name]`, then use `env.run()` to execute and clean up the coupled simulation.
+
+The microgrid input `power` is in W, with generation positive and consumption negative.
+Multiple external inputs are summed with local actor power.
+A microgrid fed entirely by external simulators may have no actors, but still needs dispatchables to balance its power.
+
+Available outputs are `p_delta`, `p_grid`, `actor_states`, `dispatch_states`, `policy_state`, and `grid_signals`.
+Vessim uses integer-second scheduling with Mosaik's `time_resolution=1.0`.
diff --git a/docs/api_reference/microgrid.md b/docs/api_reference/microgrid.md
index 9b937a2c..fd10e2a2 100644
--- a/docs/api_reference/microgrid.md
+++ b/docs/api_reference/microgrid.md
@@ -1,4 +1,22 @@
# Microgrid
+Create microgrids through [`Environment.add_microgrid()`](environment.md#vessim.Environment.add_microgrid).
+Controllers receive their observations as `MicrogridState` dictionaries.
+
+## Power balance and units
+
+The sum of actor and external simulator power is `p_delta`, the surplus or deficit allocated across dispatchables.
+A grid connection must be explicitly configured.
+Unmet demand or surplus that cannot be absorbed raises an error.
+
+| Quantity | Positive | Negative |
+| --- | --- | --- |
+| Actor power and `p_delta` | Generation or surplus | Consumption or deficit |
+| Dispatchable power | Absorbing power | Supplying power |
+| Grid exchange, `p_grid` | Export | Import |
+
+Power is in W, energy and battery capacity in Wh, and state of charge ranges from 0 to 1.
+Multiple microgrids share simulation time but have independent power balances unless explicitly coupled.
+
::: vessim.Microgrid
::: vessim.MicrogridState
diff --git a/docs/api_reference/recorder.md b/docs/api_reference/recorder.md
new file mode 100644
index 00000000..30015872
--- /dev/null
+++ b/docs/api_reference/recorder.md
@@ -0,0 +1,52 @@
+# Recording and metrics
+
+See [Results and metrics](../concepts/results.md) for recording choices and custom calculations, or [Simulation studies](../concepts/studies.md) for comparing runs.
+
+::: vessim.Recorder
+::: vessim.ExperimentResult
+::: vessim.Metric
+::: vessim.ElectricityImportCost
+::: vessim.ElectricityExportRevenue
+::: vessim.GridImportEmissions
+
+## Standard metrics
+
+| Scope | Metrics | Unit |
+| --- | --- | --- |
+| Actor and microgrid | `energy_produced`, `energy_consumed` | Wh |
+| Microgrid | `grid_import`, `grid_export` | Wh |
+| Microgrid | `peak_grid_import`, `peak_grid_export` | W |
+
+Actor production and consumption are accumulated separately from sampled power.
+Microgrid production and consumption sum the actors, excluding dispatchables and external simulator inputs.
+Grid import and export integrate the net exchange of all grid connections.
+Peak values are sampled maxima, not rolling billing demand.
+
+Energy totals cover the full [simulation duration](environment.md#time-and-final-state), including a shortened final interval.
+For incomplete runs, check `execution["status"]` and the [partial-recording semantics](#partial-recordings).
+
+## Saved records
+
+An experiment uses `format="vessim.experiment"` and `schema_version=1`.
+Use `ExperimentResult.load()` and `save()` to read and write validated records.
+`to_dict()` provides a detached dictionary for other tools.
+
+`timeseries` is a relative CSV path or null.
+`timeseries_path` resolves it from the original record location.
+Saving a loaded record elsewhere copies the CSV beside the new JSON.
+Saving in-memory observations does not persist a trajectory unless the recorder was configured with an output directory.
+
+Keep JSON and CSV together when moving recordings.
+For embedded `vessim.study` records, CSV paths are relative to the study file.
+Prefer opening a collection of experiment directories in the viewer when each run has its own CSV.
+
+CSV columns are fixed by the first observation.
+Nested state fields use dot-separated paths, escaping literal `.` as `%2E` and `%` as `%25` within names.
+Changing the state schema during a run raises an error.
+
+## Partial recordings
+
+`Recorder` accumulates metrics for an interval when its starting observation is recorded.
+The interval is clipped to the requested simulation horizon.
+If a run fails or is interrupted, the last accumulated interval can extend beyond the point execution reached, particularly in live mode.
+Check `execution["status"]` before using such totals in a comparison.
diff --git a/docs/api_reference/signal.md b/docs/api_reference/signal.md
index 9b7ad670..b043047b 100644
--- a/docs/api_reference/signal.md
+++ b/docs/api_reference/signal.md
@@ -1,18 +1,35 @@
-# Signal
+# Signals
-A [`Signal`](#vessim.Signal) provides a time-varying value to actors and grid
-signals. See the [Signals and Datasets](../concepts/signals.md) concept page for
-usage examples.
+A `Signal` provides numeric input to actors or contextual data such as electricity prices.
+See [Actors and signals](../concepts/signals.md) for usage examples.
-::: vessim.Signal
::: vessim.StaticSignal
::: vessim.Trace
+::: vessim.Signal
+
+## Custom signal lifecycle
+
+The environment calls `start()` before a signal is first sampled and `finalize()` when the environment closes, including after a failure.
+Shared signals are started and finalized once per environment.
+Replaced signals remain owned until cleanup.
+
+A signal may be read by several components at the same simulation time.
+Stateful implementations should handle repeated reads consistently.
+Create fresh stateful signals for each run.
+Immutable traces can be reused, but live signals must not be shared between environments.
+
+## Live signals
-## Software-in-the-Loop signals
+These signals require the `live` extra (`pip install 'vessim[live]'`).
+See the [Live simulation](../concepts/live.md) page for setup and usage.
-These signals require the `sil` extra (`pip install vessim[sil]`). See the
-[Software-in-the-Loop](../concepts/sil.md) concept page for the full picture.
+Live signals require `Environment.live()` and poll in the background.
+They return zero until the first reading and retain the last value if polling fails.
+There is no automatic freshness check.
+Custom `_fetch_current_value()` implementations should use bounded request timeouts.
-::: vessim.SilSignal
+::: vessim.LiveSignal
+ options:
+ members: [at, start, finalize, _fetch_current_value]
::: vessim.PrometheusSignal
::: vessim.WatttimeSignal
diff --git a/docs/assets/optuna-search-results.png b/docs/assets/optuna-search-results.png
new file mode 100644
index 00000000..6778ad9c
Binary files /dev/null and b/docs/assets/optuna-search-results.png differ
diff --git a/docs/concepts/controllers.md b/docs/concepts/controllers.md
index 2a18d5fd..0439a8c1 100644
--- a/docs/concepts/controllers.md
+++ b/docs/concepts/controllers.md
@@ -1,84 +1,64 @@
# Controllers
-A [`Controller`](../api_reference/controller.md) is executed at every simulation step.
-It can observe the state of all microgrids, log it, and modify components of the running simulation, for example by changing parameters of a [`DispatchPolicy`](../api_reference/dispatch_policy.md) on the fly.
+Controllers let you observe and influence a running simulation.
+Use them to record results, connect an application, or adjust how a datacenter operates.
+A controller can access every microgrid in its environment.
-Vessim ships with two built-in loggers ([`CsvLogger`](../api_reference/controller.md#vessim.CsvLogger) and [`MemoryLogger`](../api_reference/controller.md#vessim.MemoryLogger)), an [`Api`](../api_reference/controller.md#vessim.Api) controller for software-in-the-loop experiment, and you can write your own by subclassing `Controller`.
+Register controllers with `env.add_controller()` before running the simulation.
+You can combine several, for example to record results while testing a control strategy.
-## Logging with CsvLogger
+## Recorder
-[`CsvLogger`](../api_reference/controller.md#vessim.CsvLogger) is the recommended way to record an experiment. It writes the static configuration and the full timeseries to a directory:
+[`vs.Recorder`](../api_reference/recorder.md#vessim.Recorder) collects time series and calculates metrics such as grid imports and exports:
```python
-environment.add_controller(vs.CsvLogger("results/my_experiment"))
+recorder = env.add_controller(vs.Recorder("results/experiment"))
```
-After the run, the directory contains:
+See [Results and metrics](results.md) to inspect recordings and add your own calculations.
-```
-results/my_experiment/
- metadata.yaml # static experiment configuration
- timeseries.csv # power flows, battery state, grid signals at every step
-```
-
-These two files are exactly what the [experiment viewer](../getting_started.md#7-exploring-the-results) consumes. Open them in your browser with `vessim view results/my_experiment`.
-
-!!! tip "MemoryLogger"
- For interactive exploration in a notebook, use [`vs.MemoryLogger()`](../api_reference/controller.md#vessim.MemoryLogger) instead. It keeps the simulation state in memory and exposes `.to_df()` for direct DataFrame analysis.
-
-## Writing a custom controller
-
-Subclass [`Controller`](../api_reference/controller.md) and implement `step`. Optionally override `start` to grab references to the environment before the simulation begins:
-
-```python
-import vessim as vs
+## Api
-class MyController(vs.Controller):
- def start(self, environment):
- # Called once before the simulation starts
- self.microgrids = environment.microgrids
+[`vs.Api`](../api_reference/controller.md#vessim.Api) lets an external application read simulation state and send commands through a REST API.
+For example, a job scheduler can observe available solar power and adjust datacenter demand.
- def step(self, now, microgrid_states):
- # Called at every simulation step
- ...
-```
-
-The `step` method receives:
-
-- `now` — the current simulation time as a `datetime`.
-- `microgrid_states` — a `dict[str, MicrogridState]` keyed by microgrid name. Each [`MicrogridState`](../api_reference/microgrid.md#vessim.MicrogridState) exposes `p_delta`, `p_grid`, `actor_states`, `dispatch_states`, `policy_state`, and `grid_signals`.
-
-## Example: a scheduled charger
+Use it with `vs.Environment.live()` to test the application against a simulation running in real time.
+[Live simulation](live.md#connect-an-application) walks through the setup and requests.
-Cheap and clean grid hours are typically at night. Let's force-charge the battery between midnight and 6 AM by mutating the `charge_power` of the dispatch policy at runtime:
+## Custom controllers
-```python
-import vessim as vs
-
-class ScheduledCharger(vs.Controller):
- """Force-charges storage at 500 W between 00:00 and 06:00."""
+Subclass [`vs.Controller`](../api_reference/controller.md#vessim.Controller) and implement `step()` to respond to observations.
+Vessim calls it after each dispatch.
+Changes to model inputs or parameters affect subsequent steps.
- def __init__(self, microgrid_name: str):
- self.microgrid_name = microgrid_name
+### Demand response
- def start(self, environment):
- mg = environment.microgrids[self.microgrid_name]
- assert isinstance(mg.policy, vs.DefaultDispatchPolicy)
- self.policy = mg.policy
-
- def step(self, now, microgrid_states):
- self.policy.charge_power = 500.0 if now.hour < 6 else 0.0
-```
+Consider a datacenter with a flexible connection agreement.
+In this scenario, the grid operator can temporarily lower its import limit with advance notice.
+The datacenter responds by pausing flexible workloads until more power is available.
-Add it to the environment alongside the logger:
+The controller passes the announced limit to a datacenter simulator, whose `set_power_budget()` method adjusts workload concurrency.
+Its power consumption feeds a `vs.Consumer` through a [custom signal](signals.md#custom-signals).
```python
-environment.add_controller(ScheduledCharger("datacenter"))
-environment.add_controller(vs.CsvLogger("results/scheduled"))
+class DemandResponse(vs.Controller):
+ def __init__(self, datacenter, grid):
+ self.datacenter = datacenter
+ self.grid = grid
+
+ def step(self, now, states):
+ limit = states["site"]["grid_signals"]["next_import_limit"]
+ self.datacenter.set_power_budget(limit)
+ self.grid.max_import = limit
```
-The same pattern works for any policy parameter: switch between policies, raise/lower a battery's `min_soc`, or react to grid signals exposed via `microgrid_states[name]["grid_signals"]`.
+Here, each notice applies to the next simulation step, when both the reduced workload and the new connection limit take effect.
+When the limit rises again, the simulator resumes more workloads.
+`set_power_budget()` belongs to your datacenter model, so you decide how it responds and which workloads can wait.
-## Going further: real-time control
+A controller changes how the system operates.
+The [dispatch policy](dispatchables.md#dispatch-policies) then allocates resource power to serve the resulting demand.
+Use a policy to choose battery or generator power, and a controller to adjust workload or coordinate several sites.
-Custom controllers can also expose the simulation to the outside world. The built-in [`Api`](../api_reference/controller.md#vessim.Api) controller starts a REST server that lets external programs query the state and send control commands while the simulation runs. See [Software-in-the-Loop](sil.md) for the full picture.
+The [complete example](https://github.com/dos-group/vessim/blob/main/examples/demand_response.py) connects a simple datacenter model and runs through a temporary restriction.
+See the [Controller API](../api_reference/controller.md) for initialization and cleanup hooks.
diff --git a/docs/concepts/coupling.md b/docs/concepts/coupling.md
new file mode 100644
index 00000000..ec14cc6e
--- /dev/null
+++ b/docs/concepts/coupling.md
@@ -0,0 +1,55 @@
+# Connect External Simulators
+
+Vessim can connect domain-specific simulators written in any programming language, including simulators running on other computers, through its Mosaik backend.
+[Mosaik](https://mosaik.offis.de/) is a co-simulation framework that coordinates simulation time and exchanges data between models.
+Each simulator implements Mosaik's interface so it can participate in a shared simulation.
+
+For example, a datacenter simulator can model job scheduling and server power while Vessim models its electricity supply.
+You can reuse that simulator and explore how its workloads interact with batteries, renewable generation, and grid constraints.
+
+## Connect a datacenter model
+
+Install the backend:
+
+```console
+python -m pip install 'vessim[mosaik]'
+```
+
+The [complete example](https://github.com/dos-group/vessim/blob/main/examples/external_simulator_coupling.py) provides a small datacenter simulator whose demand increases as jobs arrive.
+Download it as `external_simulator_coupling.py`.
+With that file alongside your script, the connection looks like this:
+
+```python
+import vessim as vs
+
+with vs.Environment(
+ "2026-01-01",
+ step_size=60,
+ backend="mosaik",
+ sim_config={
+ "Datacenter": {"python": "external_simulator_coupling:DatacenterSimulator"},
+ },
+) as env:
+ datacenter = env.world.start("Datacenter").Datacenter()
+ env.add_microgrid(name="site", actors=[], dispatchables=[vs.GridConnection()])
+ recorder = env.add_controller(vs.Recorder())
+
+ env.prepare()
+ env.world.connect(datacenter, env.entities["site"], "power")
+ env.run(180)
+```
+
+Here, the external simulator supplies the microgrid's demand.
+`env.prepare()` makes the microgrid available to Mosaik, and `connect()` links the datacenter's power output to it.
+When the environment runs, Mosaik advances both simulators and passes the demand to Vessim at each step.
+Inspect the recording as usual with `recorder.to_df()` or [calculate metrics](results.md).
+
+## Connect your own simulator
+
+Wrap your model with the [Mosaik simulator API](https://mosaik.readthedocs.io/en/3.6.0/mosaik-api/), or use an existing Mosaik-compatible simulator.
+The example loads a Python class locally.
+Mosaik can also start a separate process or connect to a simulator already running on another computer through its [simulator configuration](https://mosaik.readthedocs.io/en/3.6.0/simmanager.html).
+
+You can also send Vessim observations back to another simulator, for example to let a scheduler respond to battery charge.
+See Mosaik's [feedback connections](https://mosaik.readthedocs.io/en/3.6.0/scenario-definition.html#how-to-achieve-cyclic-data-flows) and the [Vessim connection reference](../api_reference/environment.md#mosaik-connections) for the interface details.
+Use `vs.Environment.live(backend="mosaik", ...)` when the coupled simulation should run in real time.
diff --git a/docs/concepts/dispatchables.md b/docs/concepts/dispatchables.md
index ee2d3fd0..d39992f7 100644
--- a/docs/concepts/dispatchables.md
+++ b/docs/concepts/dispatchables.md
@@ -1,170 +1,162 @@
-# Dispatchables and Dispatch Policies
+# Dispatchables and policies
-A [`Dispatchable`](../api_reference/dispatchable.md) is a controllable energy resource (a battery, diesel/gas generator, or electrolyzer) whose power setpoint is decided each step by a [`DispatchPolicy`](../api_reference/dispatch_policy.md).
-Unlike actors (whose power comes from a signal), a dispatchable's power is allocated by the policy from the microgrid's current power delta.
+Dispatchables are resources whose power is chosen by a dispatch policy, such as batteries, generators, and grid connections.
+Each microgrid can contain any combination of them.
+At each step, the policy decides how to use these resources to handle the actors' combined power surplus or deficit.
-Each `Dispatchable` reports a *feasible range* `(min_power, max_power)` for the upcoming timestep. Negative values represent discharging/generation; positive values represent charging/consumption.
+## Dispatchables
-## Battery models
+A dispatchable can:
-### SimpleBattery
+- **Absorb surplus power**, for example a dump load or a mechanism for accounting for curtailed generation.
+- **Supply missing power**, for example a diesel or gas generator.
+- **Do both**, for example a battery or a grid connection.
-[`SimpleBattery`](../api_reference/dispatchable.md#vessim.SimpleBattery) is an ideal, capacity-based model which is fast and easy to parameterize. It is a sensible default for simple simulations:
+Each resource defines its operating constraints.
+Batteries have capacity and charge-rate limits, grid connections can limit power and ramp rates, and a generator model may include startup delays or minimum output.
-```python
-import vessim as vs
-
-battery = vs.SimpleBattery(
- name="battery",
- capacity=5000, # Wh (total energy capacity
- initial_soc=0.8, # 80 % charged at simulation start
- min_soc=0.2, # never discharge below 20 % SoC
- c_rate=0.5, # optional: cap charge/discharge rate at 0.5C
-)
-```
+### Batteries
-`c_rate` is the maximum charge/discharge rate as a fraction of capacity per hour. A 1 kWh battery with `c_rate=0.5` can charge or discharge at most 500 W. If omitted, the only limit is the available energy.
-
-### ClcBattery
-
-[`ClcBattery`](../api_reference/dispatchable.md#vessim.ClcBattery) implements the C-L-C model for lithium-ion batteries ([Kazhamiaka et al., 2019](https://doi.org/10.1186/s42162-019-0070-6)). It captures realistic degradation of charge/discharge limits as a function of current and is appropriate for high-fidelity studies. The default parameterization models a pack of LGM50 21700 Li-ion cells:
+[`vs.SimpleBattery`](../api_reference/dispatchable.md#vessim.SimpleBattery) stores surplus energy and supplies it later.
+This creates a 1 kWh battery, initially half full, with a charge and discharge limit of 500 W:
```python
-battery = vs.ClcBattery(
- name="battery",
- number_of_cells=100,
- initial_soc=0.5,
- min_soc=0.1,
-)
+import vessim as vs
+
+battery = vs.SimpleBattery("battery", capacity=1000, initial_soc=0.5, c_rate=0.5)
```
-!!! note
- `ClcBattery` should not be used with large simulation step sizes. For coarse-grained simulations (15 min or more), prefer `SimpleBattery`.
+The simple model assumes ideal charging and discharging.
+[`vs.ClcBattery`](../api_reference/dispatchable.md#vessim.ClcBattery) adds conversion losses and current-dependent limits for more detailed battery simulations.
-## Multiple dispatchables
+### Grid connections
-[`add_microgrid`](../api_reference/environment.md#vessim.Environment.add_microgrid) accepts a list of dispatchables. The default policy serves them in order, so the first one is used until it is exhausted before the next one is touched:
+[`vs.GridConnection`](../api_reference/dispatchable.md#vessim.GridConnection) imports power to meet demand and exports surplus.
+For example, limit imports to 5 kW and exports to 2 kW:
```python
-environment.add_microgrid(
- name="datacenter",
- actors=[...],
- dispatchables=[
- vs.SimpleBattery(name="primary", capacity=10000, initial_soc=0.9),
- vs.SimpleBattery(name="backup", capacity=5000, initial_soc=0.5),
- ],
-)
+grid = vs.GridConnection("grid", max_import=5000, max_export=2000)
```
-## Dispatch policies
-
-A [`DispatchPolicy`](../api_reference/dispatch_policy.md) receives the microgrid's power delta and allocates it across dispatchables. It returns the power that needs to be exchanged with the public grid.
+Optional `ramp_up` and `ramp_down` constrain changes in power, measured in W per simulation step.
+A [controller](controllers.md) can set `grid.connected = False` to simulate an outage and `True` to reconnect.
+Omit the grid connection to model an islanded system.
-### DefaultDispatchPolicy
+### Custom dispatchables
-[`DefaultDispatchPolicy`](../api_reference/dispatch_policy.md#vessim.DefaultDispatchPolicy) is used when no `policy` argument is given to `add_microgrid`. It supports three modes:
+Subclass [`vs.Dispatchable`](../api_reference/dispatchable.md#vessim.Dispatchable) to model other resources, such as diesel or gas generators.
+A custom resource can also account for curtailed generation as an absorption of surplus power.
-Grid-connected (default): allocate the delta in priority order, exchange the remainder with the grid.
+This example models a running gas turbine with minimum and maximum output and a ramp limit.
+The illustrative turbine supplies between 1 and 5 MW and can change its output by up to 0.5 MW per simulation step:
```python
-policy = vs.DefaultDispatchPolicy(mode="grid-connected")
-```
+class GasTurbine(vs.Dispatchable):
+ def __init__(self, name, min_output, max_output, ramp_limit):
+ super().__init__(name)
+ self.min_output = min_output
+ self.max_output = max_output
+ self.ramp_limit = ramp_limit
+ self.output = min_output
+ self.current_power = -min_output
+
+ def feasible_range(self, duration):
+ lower = max(self.min_output, self.output - self.ramp_limit)
+ upper = min(self.max_output, self.output + self.ramp_limit)
+ return -upper, -lower # Generation uses negative dispatchable power.
+
+ def step(self, duration):
+ self.output = -self.current_power
+
+ def config(self):
+ return {
+ "min_output": self.min_output,
+ "max_output": self.max_output,
+ "ramp_limit": self.ramp_limit,
+ }
+
+ def state(self):
+ return {"current_power": self.current_power}
-Islanded: fully disconnect the microgrid from the grid. If dispatchables cannot cover a deficit, a `RuntimeError` is raised. Use this to verify a self-sufficient design.
-```python
-policy = vs.DefaultDispatchPolicy(mode="islanded")
+turbine = GasTurbine("gas_turbine", min_output=1e6, max_output=5e6, ramp_limit=0.5e6)
+dispatchables = [turbine, vs.GridConnection()]
```
-Fixed charge power: force storage to charge or discharge at a constant rate, balancing the difference with the grid. Useful for scheduled charging strategies; only works in grid-connected mode.
+`feasible_range()` combines the output and ramp constraints.
+`step()` remembers the dispatched output for the next step.
+`config()` and `state()` expose model parameters and observations for recording.
-```python
-policy = vs.DefaultDispatchPolicy(charge_power=200.0) # charge at 200 W
-policy = vs.DefaultDispatchPolicy(charge_power=-500.0) # discharge at 500 W
-```
+The turbine starts at its minimum output and stays running.
+If demand falls below what it can supply, the grid exports the excess.
+Startup, shutdown, and fuel consumption would require extending the model.
+
+## Dispatch policies
-## Custom dispatchables
+A policy receives the actors' net power, called `p_delta`, and the microgrid's set of dispatchables.
+A positive delta is surplus generation, and a negative delta is unmet demand.
+The policy assigns power to the resources within their operating constraints.
-To model hardware that does not fit a battery model, subclass [`Dispatchable`](../api_reference/dispatchable.md) and implement four methods:
+**After dispatch, the remaining power imbalance must be zero.**
+If a surplus or deficit remains, the dispatch is infeasible.
+Vessim raises an error and records the run as failed.
-| Method | Description |
-|---|---|
-| `feasible_range(duration)` | Return `(min_power, max_power)` achievable for the timestep. |
-| `step(duration)` | Advance internal state after `current_power` has been set. |
-| `config()` | Return static parameters (logged once to `metadata.yaml`). |
-| `state()` | Return dynamic state (logged each step to `timeseries.csv`). |
+### Default policy
-A diesel generator is stateless and only generates (negative power):
+[`vs.DefaultDispatchPolicy`](../api_reference/dispatch_policy.md#vessim.DefaultDispatchPolicy) uses resources in list order while respecting their constraints.
+For example:
```python
-class DieselGenerator(vs.Dispatchable):
- def __init__(self, name: str, max_power_w: float):
- super().__init__(name)
- self.max_power_w = max_power_w
+dispatchables = [battery, grid]
+```
- def feasible_range(self, duration: int) -> tuple[float, float]:
- return (-self.max_power_w, 0.0) # negative = generation
+This uses the battery first and exchanges the remaining power with the grid.
+Reversing the order prioritizes grid exchange.
+Add a generator or curtailment resource to the list when the system needs those options.
- def step(self, duration: int) -> None:
- pass # no internal state
+The default policy is used automatically.
+To request a fixed battery charging power, pass `policy=vs.DefaultDispatchPolicy(charge_power=200)` to `env.add_microgrid()`.
+The policy balances this target using available generation and the other resources.
- def config(self) -> dict:
- return {"max_power_w": self.max_power_w}
+### Custom policies
- def state(self) -> dict:
- return {"current_power": self.current_power}
-```
+Choosing when to charge, generate, import, or export is often the main control problem in an energy system.
+Policies can range from a simple priority order to forecast-based optimization or reinforcement learning.
+Vessim lets you test your own strategies across demand, weather, and price scenarios through [simulation studies](studies.md), including probabilistic inputs.
-Combined with a battery, the default policy will exhaust the battery first and only call on the generator for the remaining deficit:
+For the gas turbine and grid connection above, prioritize the cheaper source.
+This policy compares the current electricity price with an assumed turbine cost per kWh, then lets the default policy allocate power in that order:
```python
-environment.add_microgrid(
- name="datacenter",
- actors=[...],
- dispatchables=[
- vs.SimpleBattery(name="battery", capacity=10000, initial_soc=0.9),
- DieselGenerator(name="generator", max_power_w=5000),
- ],
-)
+class CostBasedPolicy(vs.DefaultDispatchPolicy):
+ def __init__(self, turbine_cost):
+ super().__init__()
+ self.turbine_cost = turbine_cost
+
+ def apply(self, p_delta, duration, dispatchables, grid_signals=None):
+ grid_price = grid_signals["price"]
+ ordered = sorted(
+ dispatchables,
+ key=lambda resource: (
+ grid_price if isinstance(resource, vs.GridConnection)
+ else self.turbine_cost
+ ),
+ )
+ super().apply(p_delta, duration, ordered, grid_signals)
+
+ def state(self):
+ return {"turbine_cost": self.turbine_cost}
```
-## Custom dispatch policies
+Pass `policy=CostBasedPolicy(turbine_cost=0.15)` and `grid_signals={"price": 0.10}` to `env.add_microgrid()`, using EUR/kWh for both.
+The grid is preferred at this price.
+If the price rises above 0.15, the turbine is preferred.
+Use a price trace to vary it during the run.
-For advanced energy management, subclass [`DispatchPolicy`](../api_reference/dispatch_policy.md) and implement `apply`. The microgrid's grid signals are passed in directly, so the policy can be carbon- or price-aware without extra wiring:
+The turbine's minimum output and ramp limits still apply, so the grid supplies any remaining demand or receives excess generation.
+This policy covers a turbine and a grid connection.
+To add other resource types, define their costs or priority in the ordering rule.
-```python
-class GreenChargePolicy(vs.DispatchPolicy):
- """Only charge batteries when grid carbon intensity is low."""
+For a different allocation algorithm, implement [`vs.DispatchPolicy.apply()`](../api_reference/dispatch_policy.md#vessim.DispatchPolicy.apply) and assign resource power through `set_power()`, ensuring the final balance is zero.
- def __init__(self, carbon_threshold: float):
- self.carbon_threshold = carbon_threshold
-
- def apply(self, p_delta, duration, dispatchables, grid_signals=None):
- remaining = p_delta
- carbon = (grid_signals or {}).get("carbon_intensity")
-
- for d in dispatchables:
- lo, hi = d.feasible_range(duration)
- if remaining > 0:
- # Excess: only charge if grid is clean
- if carbon is not None and carbon > self.carbon_threshold:
- d.set_power(0.0, duration)
- continue
- allocated = min(hi, remaining)
- else:
- # Deficit: always discharge
- allocated = max(lo, remaining)
- d.set_power(allocated, duration)
- remaining -= allocated
-
- return remaining # remainder goes to the grid
-
-
-environment.add_microgrid(
- name="datacenter",
- actors=[...],
- dispatchables=[vs.SimpleBattery(name="battery", capacity=5000, initial_soc=0.8)],
- policy=GreenChargePolicy(carbon_threshold=200),
- grid_signals={"carbon_intensity": vs.Trace.from_csv("datasets/watttime_example.csv")},
-)
-```
+The [strategy comparison example](https://github.com/dos-group/vessim/blob/main/examples/strategy_comparison.py) compares self-consumption, peak-shaving, and carbon-aware policies.
diff --git a/docs/concepts/live.md b/docs/concepts/live.md
new file mode 100644
index 00000000..a6bd54fe
--- /dev/null
+++ b/docs/concepts/live.md
@@ -0,0 +1,95 @@
+# Live simulation
+
+Test an application against a simulated energy system before connecting it to real equipment.
+A job scheduler, for example, can observe solar generation and battery charge, start more work when power is available, and scale back when it is scarce.
+
+`vs.Environment.live()` runs alongside real time.
+Your application reads state and sends commands through the REST API while Vessim simulates the response of the energy system.
+
+## Connect an application
+
+Install the live integration:
+
+```console
+python -m pip install 'vessim[live]'
+```
+
+Start with a datacenter drawing 700 W and solar producing 1000 W.
+The surplus charges its battery.
+Save this as `live_example.py` and run `python live_example.py`:
+
+```python
+import vessim as vs
+
+
+def main():
+ with vs.Environment.live(step_size=1) as env:
+ env.add_microgrid(
+ name="site",
+ actors=[
+ vs.Consumer("datacenter", signal=700),
+ vs.Producer("solar", signal=1000),
+ ],
+ dispatchables=[
+ vs.SimpleBattery("battery", capacity=1000, initial_soc=0.5),
+ vs.GridConnection(),
+ ],
+ )
+ env.add_controller(vs.Api(broker_port=8700))
+ env.run()
+
+
+if __name__ == "__main__":
+ main()
+```
+
+In another terminal, read the latest observation:
+
+```console
+curl http://localhost:8700/microgrids/site
+```
+
+The response includes actor power, grid exchange, and battery state of charge.
+Now represent the scheduler starting more work by raising datacenter demand to 1200 W:
+
+```console
+curl -X PUT 'http://localhost:8700/microgrids/site/actors/datacenter/signal?value=1200'
+```
+
+Demand now exceeds solar generation, so the battery starts discharging.
+Read the state again to observe the change.
+Your application can make the same requests as part of its control loop.
+The running API lists available requests at `http://localhost:8700/docs`.
+
+Use Ctrl+C to end the simulation.
+See [`vs.Api`](../api_reference/controller.md#vessim.Api) for configuration options.
+
+## Use live measurements
+
+You can also combine real equipment with simulated energy infrastructure.
+For example, feed measured server demand into Vessim using [`vs.PrometheusSignal`](../api_reference/signal.md#vessim.PrometheusSignal):
+
+```python
+power = vs.PrometheusSignal(
+ url="http://localhost:9090",
+ query="server_power_watts",
+)
+datacenter = vs.Consumer("datacenter", signal=power)
+```
+
+Use this consumer in your live environment and replace the query with a power metric from your monitoring setup.
+For other data sources, implement a [custom live signal](signals.md#live-signals).
+
+The [server-monitoring example](https://github.com/dos-group/vessim/blob/main/examples/live_server_monitoring.py) includes a Prometheus setup and optional Grafana dashboards.
+Use `Api(export_prometheus=True)` to expose simulation measurements for monitoring.
+
+## Build a digital twin
+
+Live measurements can keep a digital twin aligned with an operating energy system.
+Your application can update demand, battery charge, and other model parameters as conditions change.
+
+To explore possible futures, create separate [simulation studies](studies.md) initialized from the observed state.
+For example, start a study from the current battery charge and compare how long it could support demand during a grid outage.
+Your application supplies the observed state and forecast inputs to those simulations.
+
+See [controllers](controllers.md) for model updates and [results and metrics](results.md) for recording live experiments.
diff --git a/docs/concepts/optimization.md b/docs/concepts/optimization.md
new file mode 100644
index 00000000..38c63bbe
--- /dev/null
+++ b/docs/concepts/optimization.md
@@ -0,0 +1,79 @@
+# Optimization
+
+Simulation studies can span more configurations than you can reasonably test.
+Often, you want to find designs that are cost-effective, low-carbon, or resilient, and understand the trade-offs between those goals.
+
+Vessim lets you evaluate candidate designs with your own demand profiles, energy infrastructure, and control strategies.
+Connect those simulations to an optimizer such as [Optuna](https://optuna.org/) to search for promising designs using the [metrics](results.md) that matter to your study.
+
+## Size a battery
+
+Suppose your datacenter has solar generation and you are choosing a battery.
+More storage can reduce grid imports by saving surplus solar power for later, but it also costs more.
+Rather than choosing a single weighting between these goals, we can search for designs that offer different trade-offs.
+
+This example uses a synthetic solar profile and searches battery capacities between 0.5 and 5 kWh.
+It minimizes both battery purchase cost and grid imports over the six-hour scenario.
+Install Optuna with `pip install optuna`.
+
+```python
+import optuna
+import pandas as pd
+import vessim as vs
+
+solar = vs.Trace(pd.Series(
+ [0, 1200, 2400, 1200, 0, 0, 0],
+ index=[hour * 3600 for hour in range(7)],
+))
+
+
+def objective(trial):
+ capacity = trial.suggest_float("battery_capacity_wh", 500, 5000)
+ with vs.Environment("2026-01-01", step_size=1800) as env:
+ env.add_microgrid(
+ name="site",
+ actors=[vs.Consumer("load", 700), vs.Producer("solar", solar)],
+ dispatchables=[vs.SimpleBattery("battery", capacity), vs.GridConnection()],
+ )
+ recorder = env.add_controller(vs.Recorder(timeseries=False))
+ env.run(6 * 3600)
+
+ battery_cost = capacity / 1000 * 300 # Illustrative purchase cost in EUR/kWh
+ grid_import = recorder.metrics["microgrids"]["site"]["grid_import"]
+ return battery_cost, grid_import
+
+
+study = optuna.create_study(
+ directions=["minimize", "minimize"],
+ sampler=optuna.samplers.NSGAIISampler(seed=42),
+)
+study.optimize(objective, n_trials=20)
+
+for trial in study.best_trials:
+ print(trial.params, trial.values)
+```
+
+Optuna proposes a capacity, Vessim simulates it, and the objective function returns its cost and grid imports.
+`study.best_trials` contains the Pareto front among the evaluated designs.
+None of these designs improves one objective without worsening the other.
+You can choose a candidate based on your budget or import target.
+
+## Explore your design choices
+
+The same approach can search renewable capacity, generator sizing, or dispatch policy parameters.
+Use cost and emissions metrics as objectives, or assess resilience by simulating outages.
+To account for uncertain demand or weather, evaluate each candidate across several scenarios and compare its average or worst-case performance.
+
+The [complete infrastructure example](https://github.com/dos-group/vessim/blob/main/examples/infrastructure_study.py) searches wind, solar, and battery sizes over a full year and records each candidate's results.
+Its `--financial` mode compares annual cost with grid-import emissions using illustrative inputs.
+After following the [example setup instructions](https://github.com/dos-group/vessim/blob/main/examples/README.md), run:
+
+```console
+python examples/infrastructure_study.py --financial --trials 100 --output results/infrastructure-search
+python -m vessim view results/infrastructure-search
+```
+
+Explore the [annual optimization results](../viewer/index.html?study=optuna-search#results) from 100 equipment configurations in the viewer.
+
+Use [Optuna Dashboard](https://optuna-dashboard.readthedocs.io/en/stable/) to explore the search and its trade-offs, then inspect saved runs in the [Vessim viewer](../vessim_viewer.md) to understand why a design performs well.
+See Optuna's [multi-objective tutorial](https://optuna.readthedocs.io/en/stable/tutorial/20_recipes/002_multi_objective.html) for more on configuring the search.
diff --git a/docs/concepts/results.md b/docs/concepts/results.md
new file mode 100644
index 00000000..71d78f42
--- /dev/null
+++ b/docs/concepts/results.md
@@ -0,0 +1,107 @@
+# Results and metrics
+
+How much electricity did your datacenter import, and when did the battery help?
+Record a simulation to inspect its time series and calculate metrics you can compare across runs.
+
+## Recorder
+
+Add a [`vs.Recorder`](../api_reference/recorder.md#vessim.Recorder) to your environment before running it:
+
+```python
+recorder = env.add_controller(vs.Recorder("results/experiment"))
+env.run(3600)
+```
+
+This saves the run's configuration and metrics in `experiment.json`, alongside its time series in `timeseries.csv`.
+Give each run its own output directory.
+Omit the directory to keep results in memory instead.
+
+After the run, use pandas to explore how your system behaved:
+
+```python
+frame = recorder.to_df()
+site = frame.xs("site", level="microgrid")
+print(site.head())
+```
+
+The columns include actor power, battery state of charge, and grid exchange.
+You can plot these in a notebook or use the CSV in your own analysis tools.
+To explore saved runs in the viewer:
+
+```sh
+python -m vessim view results/experiment
+```
+
+Try the [example results](../viewer/index.html?study=getting-started#results) and [time-series view](../viewer/index.html?study=getting-started#timeseries), or see [Vessim Viewer](../vessim_viewer.md) to open and share your own recordings.
+
+## ExperimentResult
+
+[`vs.ExperimentResult`](../api_reference/recorder.md#vessim.ExperimentResult) keeps a run's configuration, parameters, and metrics together.
+Access it through `recorder.result`, or load a saved run to continue your analysis:
+
+```python
+result = vs.ExperimentResult.load("results/experiment")
+print(result.metrics["microgrids"]["site"]["grid_import"])
+```
+
+You can also attach information calculated outside the simulation, such as an equipment cost, and save it with the result:
+
+```python
+result.add_metric("capex", 12000, "EUR")
+result.save("results/experiment/experiment.json")
+```
+
+## Metrics
+
+The recorder calculates energy consumption, generation, grid imports and exports in Wh, plus peak grid import and export in W.
+Read them from `recorder.metrics` or a saved result as shown above.
+See the [metric definitions](../api_reference/recorder.md#standard-metrics) for the full list.
+
+For larger studies, `Recorder(timeseries=False)` calculates metrics without storing time series.
+[Simulation studies](studies.md) shows how to compare configurations across many runs.
+
+### Costs and emissions
+
+To evaluate electricity costs or emissions, supply the corresponding price and carbon-intensity signals and add metric calculators to your recorder:
+
+```python
+env.add_microgrid(
+ name="site",
+ actors=[vs.Consumer("load", signal=700)],
+ dispatchables=[vs.GridConnection()],
+ grid_signals={
+ "price": 0.30, # EUR/kWh
+ "carbon": 0.25, # kgCO2e/kWh
+ },
+)
+recorder = env.add_controller(vs.Recorder(custom_metrics={
+ "cost": vs.ElectricityImportCost("site", "price", currency="EUR"),
+ "emissions": vs.GridImportEmissions("site", "carbon"),
+}))
+```
+
+These calculations combine imported electricity with your signals, which can also be [traces](signals.md#trace-signals) that vary over time.
+After the run, read `cost` and `emissions` alongside the standard metrics for `site`.
+[`vs.ElectricityExportRevenue`](../api_reference/recorder.md#vessim.ElectricityExportRevenue) can account for electricity sales using a separate export-price signal.
+
+### Custom metrics
+
+Subclass [`vs.Metric`](../api_reference/recorder.md#vessim.Metric) to calculate something specific to your study.
+For example, count the hours when grid import exceeds 1 kW:
+
+```python
+class HighImportHours(vs.Metric):
+ def __init__(self):
+ super().__init__("h", microgrid="site")
+
+ def step(self, now, states, duration):
+ imported_power = -states["site"]["p_grid"]
+ if imported_power > 1000:
+ self.value += duration / 3600
+```
+
+Vessim calls `step()` for each recorded interval, with its duration in seconds.
+Register the calculation as `custom_metrics={"high_import_hours": HighImportHours()}` on your recorder.
+Metrics can describe the whole run, a microgrid, or an individual actor.
+
+The [custom-metrics example](https://github.com/dos-group/vessim/blob/main/examples/custom_metrics.py) combines electricity costs, emissions, and equipment costs into study objectives.
diff --git a/docs/concepts/signals.md b/docs/concepts/signals.md
index d7df6196..4ee5bb48 100644
--- a/docs/concepts/signals.md
+++ b/docs/concepts/signals.md
@@ -1,101 +1,92 @@
-# Signals and Datasets
+# Actors and signals
-A [`Signal`](../api_reference/signal.md) provides a time-varying value to the simulation: the power consumed by a server, the power produced by a solar panel, the carbon intensity of the public grid. Every [`Actor`](../api_reference/actor.md) and every entry in a microgrid's `grid_signals` is backed by a `Signal`.
+[`vs.Consumer`](../api_reference/actor.md#vessim.Consumer) represents electrical loads, such as computing equipment or cooling.
+[`vs.Producer`](../api_reference/actor.md#vessim.Producer) represents generation, such as solar panels or wind turbines.
+Both are subclasses of [`vs.Actor`](../api_reference/actor.md#vessim.Actor), which also supports prosumers that consume and produce power.
-Vessim ships three signal types: [`StaticSignal`](../api_reference/signal.md#vessim.StaticSignal) for constant values, [`Trace`](../api_reference/signal.md#vessim.Trace) for replaying historical data, and [`SilSignal`](../api_reference/signal.md#vessim.SilSignal) for live data sources.
-You can easily write your own, only take care that `signal.at(elapsed)` returns the value at the elapsed time the simulation has accumulated since `sim_start`.
+Add these components to your microgrid's `actors` list.
+Their inputs can be static, follow a recorded trace, or come from live measurements.
-## StaticSignal
+## Static signals
-[`StaticSignal`](../api_reference/signal.md#vessim.StaticSignal) always returns the same value. It is useful for baseline loads, fixed production, or as a placeholder during testing:
+Actors with static inputs are the simplest form of actor.
+Pass a number to model fixed demand or generation:
```python
import vessim as vs
-signal = vs.StaticSignal(value=42)
+load = vs.Consumer("load", signal=700)
+solar = vs.Producer("solar", signal=1000)
```
-You can update a `StaticSignal` at runtime via `set_value()`. This allows a [`Controller`](../api_reference/controller.md) to toggle loads or follow a schedule.
+A [controller](controllers.md#demand-response) can change demand with `load.signal = 500`, which takes effect at the next actor update.
-## Trace
+## Trace signals
-[`Trace`](../api_reference/signal.md#vessim.Trace) replays a time series row-by-row. Its index is an *offset* in seconds since the trace's start (row 0 at `0`); as the simulation advances, `Trace` returns the value at the matching offset.
-
-### From in-memory data
-
-`Trace` accepts a pandas `Series` or `DataFrame` with either a numeric index (interpreted as seconds) or a `TimedeltaIndex`:
+Use [`vs.Trace`](../api_reference/signal.md#vessim.Trace) to describe demand or generation over time.
+Create a trace from a pandas Series or DataFrame:
```python
import pandas as pd
-import vessim as vs
-
-trace = vs.Trace(pd.Series([50, 200, 50], index=[0, 3600, 7200]))
-trace = vs.Trace(pd.Series(
- [50, 200, 50],
- index=pd.to_timedelta(["0s", "1h", "2h"]),
-))
+profile = pd.Series([0, 1000, 0], index=[0, 3600, 7200])
+solar = vs.Producer("solar", signal=vs.Trace(profile))
```
-Sampling does not need to be uniform. The index must start at 0.
+
-### Loading from a CSV
+You can also load a trace from a CSV file.
+For example, save this hourly profile as `input.csv`, with elapsed seconds in the first column:
-[`Trace.from_csv`](../api_reference/signal.md#vessim.Trace.from_csv) reads a CSV whose first column is the offset in seconds and remaining columns hold values:
-
-```text
-time,trace_a,trace_b
-0,0.10,0.35
-3600,0.22,0.47
-7200,0.38,0.41
+```csv
+seconds,load,solar
+0,700,0
+3600,700,1200
+7200,700,2400
+10800,700,1200
+14400,700,0
+18000,700,0
+21600,700,0
```
+Select the column for each actor:
+
```python
-load = vs.Trace.from_csv(
- "datasets/load_example.csv",
- column="trace_a", # required if the CSV has multiple value columns
-)
+inputs = vs.Trace.from_csv("input.csv")
+load = vs.Consumer("load", signal=inputs.select("load"))
+solar = vs.Producer("solar", signal=inputs.select("solar"))
```
-`scale=` multiplies all values (handy for normalized data), and missing cells (per column) are dropped.
-
-### Datetime data and `anchor`
+The trace holds each value until the next timestamp.
+Use a profile covering your simulation period.
+See the [Trace reference](../api_reference/signal.md#vessim.Trace) for calendar timestamps, scaling, and data requirements.
-Calendar-stamped data is also supported. Pass an `anchor` to mark which row should sit at offset=0; earlier rows are dropped. This works both for in-memory data and for CSVs with a datetime first column:
+The [CSV example](https://github.com/dos-group/vessim/blob/main/examples/replay_csv.py) runs this profile:
-```text
-datetime,Berlin,Stockholm,Mumbai
-2022-06-08 00:05:00,0.0,0.0,0.0
-2022-06-08 00:10:00,0.0,0.0,0.0
-2022-06-08 00:15:00,0.0,0.0,0.0
+```console
+python examples/replay_csv.py input.csv --hours 6
```
-```python
-solar = vs.Trace.from_csv(
- "datasets/solcast.csv",
- anchor="2022-06-09 00:00:00",
- column="Berlin",
- scale=5000,
-)
-```
-
-## Public data sources
+## Live signals
-Vessim's CSV schema is intentionally minimal so you can produce it from any source. Common providers for simulation data include:
+Live signals read measurements from a running system.
+For example, [`vs.PrometheusSignal`](../api_reference/signal.md#vessim.PrometheusSignal) can supply measured server power from Prometheus.
+See [live measurements](live.md#use-live-measurements) for setup and an example.
-- Carbon intensity: [Electricity Maps](https://www.electricitymaps.com/) or [WattTime](https://watttime.org/).
-- Weather and solar: [Open-Meteo](https://open-meteo.com/) (free historical archive), [Solcast](https://solcast.com/) (commercial irradiance and PV forecasts), or [NREL NSRDB](https://nsrdb.nrel.gov/) (high-resolution US data). For an example for a public live station feed, see [HTW Berlin](https://wetter.htw-berlin.de/).
-- Electricity prices: [ENTSO-E Transparency Platform](https://transparency.entsoe.eu/) (European day-ahead and balancing markets), [EIA Open Data](https://www.eia.gov/opendata/) (US wholesale and retail), [Nord Pool](https://www.nordpoolgroup.com/) (Nordic and Baltic spot markets), or [Electricity Maps](https://www.electricitymaps.com/) (price data alongside carbon intensity).
+## Custom signals
-For real-time data instead of historical traces, see the [Software-in-the-Loop signals](#software-in-the-loop-signals) below.
+To calculate an input yourself, implement [`vs.Signal.at()`](../api_reference/signal.md#vessim.Signal.at) as a function of elapsed simulation time in seconds:
-## Software-in-the-Loop signals
-
-For real-time simulations, [Software-in-the-Loop (SiL)](sil.md) signals poll data from external sources in the background:
+```python
+class DemandProfile(vs.Signal):
+ def at(self, elapsed):
+ return 700 if elapsed < 3600 else 1200
-- [`PrometheusSignal`](../api_reference/signal.md#vessim.PrometheusSignal): pull live metrics (e.g. server power) from a Prometheus server.
-- [`WatttimeSignal`](../api_reference/signal.md#vessim.WatttimeSignal): fetch live grid carbon intensity from the WattTime API.
+load = vs.Consumer("load", signal=DemandProfile())
+```
-SiL signals are time-independent: they ignore the `elapsed` argument and always return their most recent cached value. Use them with [`Environment.live`](../api_reference/environment.md#vessim.Environment.live) rather than `Environment(...)`. See [Software-in-the-Loop](sil.md).
+Signals can also supply electricity prices or carbon intensity to policies, controllers, and metrics.
+Add them through the microgrid's `grid_signals`, for example `grid_signals={"price": price_trace}`.
+See [costs and emissions](results.md#costs-and-emissions) for a complete use case.
-You can subclass [`SilSignal`](../api_reference/signal.md#vessim.SilSignal) to wrap any other live data source.
+For a component with its own behavior, such as a datacenter with a job queue, implement a [custom actor](../api_reference/actor.md#vessim.Actor.sample).
diff --git a/docs/concepts/sil.md b/docs/concepts/sil.md
deleted file mode 100644
index 56dc4bc6..00000000
--- a/docs/concepts/sil.md
+++ /dev/null
@@ -1,180 +0,0 @@
-# Software-in-the-Loop
-
-Software-in-the-Loop (SiL) simulation lets Vessim interact with real systems while the simulation is running. Use it to:
-
-- Test real applications — validate energy-aware schedulers, autoscalers, or routing logic against simulated energy scenarios.
-- Use real data sources — pull live metrics from sensors, monitoring systems, or APIs into the simulation.
-
-SiL extras are not installed by default. Enable them with:
-
-```console
-pip install vessim[sil]
-```
-
-## Architecture
-
-Vessim's SiL has two communication directions:
-
-1. Real-time data → Vessim: [`SilSignal`](../api_reference/signal.md#vessim.SilSignal) (and subclasses like [`PrometheusSignal`](../api_reference/signal.md#vessim.PrometheusSignal) or [`WatttimeSignal`](../api_reference/signal.md#vessim.WatttimeSignal)) fetches data from external sources in a background thread and feeds it into the simulation.
-2. Vessim → external world: the [`Api`](../api_reference/controller.md#vessim.Api) controller exposes the simulation state via a REST API. External programs can read it and send back control commands. Feel free to implement other types of APIs.
-
-## Real-time signals
-
-A [`SilSignal`](../api_reference/signal.md#vessim.SilSignal) polls its data source on a background thread, so the simulation never blocks on a network request.
-
-### PrometheusSignal
-
-[`PrometheusSignal`](../api_reference/signal.md#vessim.PrometheusSignal) pulls a value from a [Prometheus](https://prometheus.io/) query. This is typical for using real server metrics as the load of a simulated actor:
-
-```python
-import vessim as vs
-
-power_signal = vs.PrometheusSignal(
- url="http://localhost:9090",
- query='(1 - avg(rate(node_cpu_seconds_total{mode="idle"}[1m]))) * 1000',
-)
-
-server = vs.Actor(name="server", signal=power_signal, consumer=True)
-```
-
-### WatttimeSignal
-
-[`WatttimeSignal`](../api_reference/signal.md#vessim.WatttimeSignal) fetches live grid carbon intensity from the [WattTime API](https://watttime.org/):
-
-```python
-carbon = vs.WatttimeSignal(
- username="my_user",
- password="my_password",
- region="CAISO_NORTH",
- update_interval=300,
-)
-```
-
-### Custom SiL signals
-
-Subclass [`SilSignal`](../api_reference/signal.md#vessim.SilSignal) and implement `_fetch_current_value`:
-
-```python
-class MySensorSignal(vs.SilSignal):
- def _fetch_current_value(self) -> float:
- # response = requests.get("http://192.168.1.50/power")
- # return float(response.text)
- return 42.0
-```
-
-## The Api controller
-
-[`Api`](../api_reference/controller.md#vessim.Api) starts a small FastAPI server that bridges the simulation and the outside world:
-
-```python
-environment.add_controller(vs.Api(export_prometheus=True, broker_port=8700))
-```
-
-Once the simulation is running, the following endpoints are available at `http://localhost:8700`:
-
-| Method & path | Purpose |
-|---|---|
-| `GET /` | List of microgrids and their actors. |
-| `GET /` | Current state of a microgrid (power flows, battery SoC, …). |
-| `PUT /` | Send a control command (e.g. mutate a dispatchable property). |
-| `GET /metrics` | Prometheus-format metrics (when `export_prometheus=True`). |
-
-Read the state:
-
-```bash
-curl http://localhost:8700/datacenter
-```
-
-```json
-{
- "microgrid": "datacenter",
- "time": "2023-01-01T00:00:00",
- "p_delta": -500.0,
- "p_grid": -500.0,
- "dispatch": { "battery": { "soc": 0.8 } },
- "actors": { "server": { "power": -500.0 } }
-}
-```
-
-Send a control command:
-
-```bash
-curl -X PUT http://localhost:8700/datacenter \
- -H "Content-Type: application/json" \
- -d '{"dispatchable": {"name": "battery", "property": "min_soc", "value": 0.5}}'
-```
-
-When `export_prometheus=True`, the `/metrics` endpoint can be scraped by Prometheus and visualized in Grafana. See [`examples/sil/`](https://github.com/dos-group/vessim/tree/main/examples/sil) for a ready-made compose file.
-
-## Walkthrough: real CPU load to simulated microgrid
-
-This example wires up a real Prometheus + node_exporter stack so that the actual CPU usage of the host drives the simulated server's power draw. The full source is at [`examples/sil_example.py`](https://github.com/dos-group/vessim/blob/main/examples/sil_example.py).
-
-```python
-import vessim as vs
-
-
-def main():
- # Live mode: sim_start is captured when run() is called, simulation
- # advances at 1× wall-clock, and Trace data replays starting from "now".
- environment = vs.Environment.live(step_size=1)
-
- # Server load is driven by real host CPU usage scraped via Prometheus
- server = vs.Actor(
- name="server",
- consumer=True,
- signal=vs.PrometheusSignal(
- url="http://localhost:9090",
- query='(1 - avg(rate(node_cpu_seconds_total{mode="idle"}[1m]))) * 1000',
- ),
- )
-
- solar = vs.Actor(
- name="solar",
- signal=vs.Trace.from_csv(
- "datasets/solar_example.csv",
- column="Berlin",
- scale=2000,
- ),
- )
-
- battery = vs.SimpleBattery(name="battery", capacity=20, initial_soc=0.5)
-
- environment.add_microgrid(
- name="your_computer",
- actors=[server, solar],
- dispatchables=[battery],
- )
-
- environment.add_controller(vs.Api(export_prometheus=True))
- environment.run()
-
-
-if __name__ == "__main__":
- main()
-```
-
-### Running it
-
-1. Install the SiL extras:
- ```console
- pip install 'vessim[sil]'
- ```
-2. Start Prometheus + node_exporter:
- ```console
- docker compose -f examples/sil/docker-compose.yml up -d
- ```
- Wait ~15 s for the first scrapes to land.
-3. Run the simulation:
- ```console
- python examples/sil_example.py
- ```
-4. Hit the API in another terminal:
- ```console
- curl http://localhost:8700/your_computer
- ```
- Drive your CPU up (e.g. `yes > /dev/null`) and watch the server power follow.
-5. Optionally start Grafana to visualize the live `/metrics` endpoint:
- ```console
- docker compose -f examples/sil/docker-compose.grafana.yml up -d
- ```
diff --git a/docs/concepts/studies.md b/docs/concepts/studies.md
new file mode 100644
index 00000000..f140c038
--- /dev/null
+++ b/docs/concepts/studies.md
@@ -0,0 +1,60 @@
+# Simulation studies
+
+Define scenarios in Python and compare their [recorded metrics](results.md) and time series in the [viewer](../vessim_viewer.md).
+These three examples use synthetic inputs. Follow the [setup instructions](https://github.com/dos-group/vessim/blob/main/examples/README.md) to run them locally.
+
+## Exploring microgrid configurations
+
+Adding wind, solar, and battery storage can reduce a datacenter's grid imports, but manufacturing the equipment also causes emissions.
+We compare eight setups over a full year to see how equipment choices affect grid dependence and emissions.
+
+The setups include grid-only supply, solar, wind, a wind–solar mix, and four combinations with storage, all facing the same demand and weather.
+Compare their energy and emissions metrics, then overlay time series to see when batteries charge and grid supply is needed.
+
+[Explore the results](../viewer/index.html?study=infrastructure-year#results) or run the [infrastructure example](https://github.com/dos-group/vessim/blob/main/examples/infrastructure_study.py):
+
+```console
+python examples/infrastructure_study.py
+python -m vessim view results/infrastructure-study
+```
+
+## Optimization with Optuna
+
+Design spaces can be very large, while we often only need configurations that perform well against specific metrics.
+Optuna uses results from previous simulations to guide the search toward promising designs without evaluating every possible combination.
+
+Here, we optimize wind, solar, and battery sizing to minimize annual cost and grid-import emissions.
+Optuna selects 100 candidates, Vessim simulates each for a year, and the viewer shows their cost–emissions trade-offs and time series.
+
+
+
+ 
+
+ Annual cost and export revenue for the evaluated configurations, shown in the Vessim viewer.
+
+
+[Explore the results](../viewer/index.html?study=optuna-search#results) or run:
+
+```console
+python examples/infrastructure_study.py --financial --trials 100 --output results/infrastructure-search
+python -m vessim view results/infrastructure-search
+```
+
+See [Optimization](optimization.md) for defining an objective function and using Optuna Dashboard to inspect the search.
+
+## Probabilistic studies
+
+Demand, weather, and electricity prices are uncertain.
+Monte Carlo sampling lets us evaluate configurations across possible realizations of these inputs and examine how their outcomes vary.
+
+This example samples demand and compares three battery strategies: self-consumption, peak shaving, and carbon-aware charging.
+Each faces the same five daily demand samples. Compare individual runs or aggregate them to see the variation in costs, emissions, and peak grid imports.
+
+[Explore the results](../viewer/index.html?study=dispatch-comparison#results) or run the [dispatch example](https://github.com/dos-group/vessim/blob/main/examples/strategy_comparison.py):
+
+```console
+python examples/strategy_comparison.py
+python -m vessim view results/strategy-comparison
+```
+
+We are working on better functionality for planning, executing, and visualizing probabilistic studies.
diff --git a/docs/getting_started.md b/docs/getting_started.md
index 1008b861..29c9ed8d 100644
--- a/docs/getting_started.md
+++ b/docs/getting_started.md
@@ -1,162 +1,127 @@
-# Getting Started
+# Getting started
-This walkthrough builds a complete Vessim simulation step by step.
+## Install
-The final code matches [`examples/basic_example.py`](https://github.com/dos-group/vessim/blob/main/examples/basic_example.py) in the repository, so you can follow along by running that file directly.
+Install Vessim with Python 3.9 to 3.15:
-## 1. The Environment
-
-The [`Environment`](api_reference/environment.md) manages simulation time and the synchronization between all components. We start by creating one with a step size of 5 minutes (300 seconds):
-
-```python
-import vessim as vs
-
-environment = vs.Environment(sim_start="2026-05-03", step_size=300)
-```
-
-Vessim has two modes:
-
-- *simulated* (used here) uses only simulated actors and runs as fast as possible (see [Discrete-event simulation](https://en.wikipedia.org/wiki/Discrete-event_simulation) on Wikipedia). The `sim_start` parameter labels the output (e.g. CSV timestamps).
-- *live* (via [`Environment.live`](api_reference/environment.md#vessim.Environment.live)) advances at wall-clock speed and can connect to real systems, see [Software-in-the-Loop](concepts/sil.md). Here, `sim_start` is optional and defaults to `datetime.now()`.
-
-## 2. Actors
-
-Actors are the energy consumers and producers in your microgrid.
-Each actor wraps a [`Signal`](concepts/signals.md) that provides its power value at any point in time.
-
-We start with a server that constantly draws 700 W.
-By Vessim convention, production yields positives and consumption negative values.
-Passing `consumer=True` flips the sign of the singal:
-
-```python
-server = vs.Actor(name="server", signal=vs.StaticSignal(value=700), consumer=True)
+```console
+python -m pip install vessim
```
-Next, we add a solar panel. We point it at a CSV from the repository's `datasets/` directory, pick the Berlin column, and scale the normalized data to a 5 kW peak:
+Optional integrations:
-```python
-solar = vs.Actor(
- name="solar_panel",
- signal=vs.Trace.from_csv(
- "datasets/solar_example.csv",
- column="Berlin",
- scale=5000,
- ),
-)
-```
+| Install | Use |
+| --- | --- |
+| `pip install 'vessim[live]'` | [Live measurements and REST APIs](concepts/live.md) |
+| `pip install 'vessim[mosaik]'` | [Connect external simulators](concepts/coupling.md) |
-A [`Trace`](api_reference/signal.md#vessim.Trace) replays a CSV row-by-row, indexed by an offset in seconds since the trace's start. See [Signals and Datasets](concepts/signals.md) for all available signal types, the CSV schema, and how to fetch more data.
+## How Vessim works
-At every step, Vessim sums the actors' powers into the microgrid's *power delta*. Again, positive means surplus, negative means deficit.
+Vessim models local energy systems, called *microgrids*.
+Each combines electrical loads, such as computing equipment and cooling, with generation, storage, and grid connections.
+An [environment](#run-your-first-simulation) advances one or more microgrids on a shared clock, for example to model geographically distributed datacenters.
-## 3. Dispatchables (optional)
+
-Dispatchables are controllable resources whose power is decided at each step by a [`DispatchPolicy`](api_reference/dispatch_policy.md). The most common dispatchable is a battery. Vessim's [`SimpleBattery`](api_reference/dispatchable.md#vessim.SimpleBattery) is an ideal capacity-based model:
+- **[Actors](concepts/signals.md)** (red) provide demand and generation.
+ [`vs.Consumer`](api_reference/actor.md#vessim.Consumer) and [`vs.Producer`](api_reference/actor.md#vessim.Producer) use static inputs or a [signal](concepts/signals.md), such as a load trace or live measurement.
+ Their combined power gives the surplus or deficit to balance at each step.
+- **[Dispatchables](concepts/dispatchables.md)** (gray) supply or absorb power within their operating limits.
+ A [dispatch policy](concepts/dispatchables.md#dispatch-policies) allocates the surplus or deficit across batteries, generators, and grid connections.
+ Policies can use electricity prices or carbon intensity to guide those decisions.
+- **[Controllers](concepts/controllers.md)** (yellow) observe all microgrids after dispatch.
+ They can change demand or policy parameters for subsequent steps, [record results](concepts/results.md), or let external applications monitor and control the system through a [REST API](concepts/live.md#connect-an-application).
-```python
-battery = vs.SimpleBattery(name="battery", capacity=1500, initial_soc=0.8, min_soc=0.3)
-```
+## Run your first simulation
-Without a custom policy, Vessim uses the [`DefaultDispatchPolicy`](api_reference/dispatch_policy.md#vessim.DefaultDispatchPolicy) which charges on surplus, discharges on deficit, and exchanges any leftover power with the public grid. See [Dispatchables and Dispatch Policies](concepts/dispatchables.md) for islanded mode, custom policies, and how to model anything beyond batteries.
+The following steps build a three-day experiment with a datacenter load, solar generation, and battery storage.
+The complete script is available as [basic_example.py](https://github.com/dos-group/vessim/blob/main/examples/basic_example.py).
-## 4. Grid Signals (optional)
+### 1. Choose how to run
-A microgrid can also carry *grid signals*: time-varying contextual data such as local electricity price or carbon intensity.
-They don't consume or produce power, but custom dispatch policies and controllers can use them to make decisions.
+| Create an environment with | To |
+| --- | --- |
+| `vs.Environment(...)` | Run as fast as computation allows for [simulation studies](concepts/studies.md) |
+| `vs.Environment.live(...)` | Run alongside wall-clock time for [live applications and measurements](concepts/live.md) |
-We add carbon intensity from a WattTime CAISO-North trace.
-This CSV is datetime-indexed, so we pass an `anchor=` to mark which row maps to our simulation start with offset=0:
+Both modes use the same component models.
+Here we use simulation mode and update the system every five minutes:
```python
-grid_signals = {
- "carbon_intensity": vs.Trace.from_csv(
- "datasets/watttime_example.csv",
- anchor="2023-06-08 00:00:00",
- ),
-}
-```
-
-## 5. Putting the microgrid together
-
-[`add_microgrid`](api_reference/environment.md#vessim.Environment.add_microgrid) combines actors, dispatchables, and grid signals into one simulated system.
-You can call it multiple times to simulate several geo-distributed microgrids in parallel.
+import vessim as vs
-```python
-environment.add_microgrid(
- name="datacenter",
- actors=[server, solar],
- dispatchables=[battery],
- grid_signals=grid_signals,
-)
+env = vs.Environment(sim_start="2026-01-01", step_size=5 * 60)
```
-## 6. Logging and running
+### 2. Assemble the energy system
-The last ingredient is a [`Controller`](api_reference/controller.md).
-Controllers observe the simulation at every step and can react to it.
-We use the built-in [`CsvLogger`](api_reference/controller.md#vessim.CsvLogger), which writes the experiment configuration and full timeseries to a directory:
+Add demand, generation, and the resources that supply or store power to a microgrid.
+Download [solar_example.csv](https://raw.githubusercontent.com/dos-group/vessim/main/datasets/solar_example.csv) into your working directory.
+Its first column contains elapsed seconds and the remaining columns contain normalized solar profiles for different cities.
+Here we select Berlin and scale its profile to a 2.4 kW installation, alongside a constant 700 W load, a 1 kWh battery, and the public grid:
```python
-environment.add_controller(vs.CsvLogger("results/basic_example"))
-
-environment.run(until=24 * 3600) # 24 hours
-```
+# Scale the normalized solar profile to a 2.4 kW installation.
+solar = vs.Trace.from_csv("solar_example.csv", column="Berlin", scale=2400)
-After the run, `results/basic_example/` contains:
-
-```
-results/basic_example/
- metadata.yaml # static experiment configuration
- timeseries.csv # power flows, battery state, grid signals at every step
+env.add_microgrid(
+ name="site",
+ actors=[
+ vs.Consumer("load", signal=700),
+ vs.Producer("solar", signal=solar),
+ ],
+ dispatchables=[vs.SimpleBattery("battery", capacity=1000), vs.GridConnection()],
+)
```
-Custom controllers can do much more than logging: they can mutate the dispatch policy at runtime or expose the simulation as a REST API. See [Controllers](concepts/controllers.md) and [Software-in-the-Loop](concepts/sil.md).
+The default policy uses the battery before exchanging power with the grid.
+Choose different [inputs](concepts/signals.md) or [resources and policies](concepts/dispatchables.md) to represent your system.
+Call `add_microgrid()` again to model another site.
-## 7. Exploring the results
+### 3. Add recording or control
-Vessim ships with a browser-based experiment viewer. Launch it on your results directory:
+Register a `Recorder` to save results:
-```console
-vessim view results/basic_example
+```python
+recorder = env.add_controller(vs.Recorder("results/basic_example"))
```
-This opens an interactive dashboard with charts for power flows, battery state of charge, and grid signals.
+Add [custom controllers](concepts/controllers.md) to adjust demand or resource operation as the simulation progresses.
+Controllers can observe all sites in the environment.
-You can also [open the live demo of this exact run](viewer/index.html){target="_blank"}, served as a static page from the documentation site.
+### 4. Run and inspect
-## The complete example
+Run the model for three simulated days:
```python
-import vessim as vs
+run = env.run(until=3 * 24 * 3600)
+metrics = recorder.metrics["microgrids"]["site"]
+print(f"Grid import: {metrics['grid_import'] / 1000:.2f} kWh")
+print(f"Grid export: {metrics['grid_export'] / 1000:.2f} kWh")
+```
-environment = vs.Environment(sim_start="2022-06-09", step_size=300)
+```text
+Grid import: 26.71 kWh
+Grid export: 11.08 kWh
+```
-environment.add_microgrid(
- name="datacenter",
- actors=[
- vs.Actor(name="server", signal=vs.StaticSignal(value=700), consumer=True),
- vs.Actor(name="solar_panel", signal=vs.Trace.from_csv(
- "datasets/solar_example.csv", column="Berlin", scale=5000
- )),
- ],
- dispatchables=[
- vs.SimpleBattery(name="battery", capacity=1500, initial_soc=0.8, min_soc=0.3)
- ],
- grid_signals={
- "carbon_intensity": vs.Trace.from_csv(
- "datasets/watttime_example.csv", anchor="2023-06-08 00:00:00"
- ),
- },
-)
+Open the saved results:
-environment.add_controller(vs.CsvLogger("results/basic_example"))
-environment.run(until=24 * 3600)
+```console
+python -m vessim view results/basic_example
```
+[Explore this example in the viewer](viewer/index.html?study=getting-started#results), or open [Time series](viewer/index.html?study=getting-started#timeseries) and select the basic example to inspect its power flows.
-## Next steps
+## Where next
-- [Signals and Datasets](concepts/signals.md) — replay historical data, build constant or custom signals.
-- [Dispatchables and Dispatch Policies](concepts/dispatchables.md) — battery models, custom dispatchables, custom policies.
-- [Controllers](concepts/controllers.md) — observe and modify a running simulation.
-- [Software-in-the-Loop](concepts/sil.md) — connect Vessim to live data sources and real applications.
+| To… | Read |
+| --- | --- |
+| Use your own demand and generation data | [Load a CSV](concepts/signals.md#load-a-csv) |
+| Model resource limits and outages | [Dispatchables and policies](concepts/dispatchables.md#grid-connections) |
+| Implement control strategies or demand response | [Controllers](concepts/controllers.md) |
+| Calculate costs, emissions, or custom metrics | [Results and metrics](concepts/results.md#costs-and-emissions) |
+| Compare configurations and uncertain scenarios | [Simulation studies](concepts/studies.md) and [optimization](concepts/optimization.md) |
+| Test an application against a simulated energy system | [Live simulation](concepts/live.md#connect-an-application) |
+| Model an existing energy system using live measurements | [Digital-twin workflows](concepts/live.md#build-a-digital-twin) |
+| Connect another simulator | [Connect external simulators](concepts/coupling.md) |
diff --git a/docs/index.md b/docs/index.md
index a129da49..b19ef88f 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,67 +1,92 @@
+---
+hide:
+ - toc
+---
+
#
-Vessim is a **co-simulation testbed for microgrids**\*
+Vessim is a Python library for simulating datacenter energy systems.
+
+Vessim models the interaction between datacenter demand and energy infrastructure, helping you understand how design and control decisions affect cost, carbon emissions, and resilience.
+
+
+
+
+
+### Simulation studies
+
+Evaluate configurations and control strategies across variations in operating conditions.
+Use parameter sweeps, what-if scenarios, and probabilistic inputs to explore design choices and assess their sensitivity to uncertainty.
+
+[Explore example results](viewer/index.html?study=optuna-search&view=%7B%22v%22%3A1%2C%22pareto%22%3Atrue%7D#results){ .md-button }
+
+
+
+
+### Live modeling
-Vessim lets you model how loads, (renewable) generation, energy storage, and the public grid interact, and test control and dispatch strategies against them, in pure simulation or with **real software and hardware** in the loop.
+Connect real applications to Vessim as they would connect to a physical energy system.
+Through REST APIs and software-in-the-loop simulation, applications can observe and control simulated components in real time.
-*\*By "microgrid" we mean any local energy system that combines loads, generation, and storage behind a grid connection. Grids can scale from small battery-constrained systems to a GW-scale datacenter campus.*
+
+
+
+### Digital twins
-## What can I do with Vessim?
+Model your energy system alongside the real installation, using live measurements and updating parameters as conditions change.
+Explore future scenarios such as prolonged low renewable generation or a complete grid outage.
-Vessim helps you understand and optimize how loads, energy sources, and storage interact.
-It was originally designed for energy-aware and carbon-aware data centers, but has also been used for other use cases such as battery price arbitrage and demand response for cooling warehouses.
+
+
+
-- **Control and dispatch strategies**: test battery charging/discharging, load-shifting, and price- or carbon-driven dispatch.
-- **Demand response and power outages**: simulate demand response signals or power outages to understand your system's flexibility and test mitigation strategies.
-- **Microgrid composition**: experiment with adding solar panels, wind turbines, or batteries to see how they would affect your energy costs and carbon emissions.
-- **Energy- and carbon-aware computing**: develop applications that adapt their energy consumption to the carbon intensity and price of electricity.
+
-Vessim can simulate multiple distributed microgrids in parallel and easily integrates historical datasets and new simulators.
-Vessim's [software-in-the-loop](concepts/sil.md) capabilities let you run real systems against simulated microgrids.
+## News
+
-## How Vessim Works
+
-Vessim simulates local energy systems, called *microgrids*, that combine loads (such as computing equipment, cooling, or charging) with energy sources, dispatchable resources like batteries, and a connection to the public grid.
+Vessim 0.16.0 is out!
+See the [release notes](https://github.com/dos-group/vessim/releases) for what’s new.
-
+
-Vessim is built on [Mosaik](https://mosaik.offis.de), a general-purpose co-simulation framework.
-Each hexagon in the diagram is a Mosaik component that can be simulated or run as real software (software-in-the-loop).
-A microgrid is composed of three kinds of building blocks:
+
-- [Actors](concepts/signals.md) (red): exogenous consumers and producers whose power is determined by a [`Signal`](api_reference/signal.md). Their sum at each step is the microgrid's *power delta*.
-- [Dispatchables](concepts/dispatchables.md) (gray): controllable resources like batteries or generators. A [`DispatchPolicy`](api_reference/dispatch_policy.md) decides how to distribute the power delta across them; any remainder is exchanged with the public grid.
-- [Controllers](concepts/controllers.md) (yellow): observe the simulation each step. Built-in loggers record the state, and custom controllers can adjust dispatch parameters or expose the simulation via APIs.
+
+Philipp Wiesner gave a keynote on [“Co-Simulating Computing and Energy Systems”](https://tdis.gitlab.io/tdis26/) at the TDIS workshop at [EuroSys'26](https://2026.eurosys.org/) in Edinburgh.
-## Try it now
+
-Head to [Getting Started](getting_started.md) for a step-by-step walkthrough that builds a complete microgrid in a few lines of code.
+
-You can also explore a finished simulation in your browser: [open the Experiment Viewer beta](viewer/index.html){target="_blank"} to see the interactive dashboard for the Getting Started run.
+
+Together with [Exalsius](https://www.exalsius.ai/) and [Deep Science Ventures](https://www.deepscienceventures.com/), we presented a prototype for [distributed LLM pretraining during renewable curtailment windows](https://arxiv.org/abs/2602.22760).
+Vessim was used to model geographically distributed energy systems.
-## Installation
+
-Install the [latest release](https://pypi.org/project/vessim/) via [pip](https://pip.pypa.io/en/stable/getting-started/):
+
-```console
-pip install vessim
-```
+
-For [software-in-the-loop](concepts/sil.md) capabilities, install the `sil` extra:
+Vessim is developed at the [University of Cambridge](https://www.cam.ac.uk/).
-```console
-pip install vessim[sil]
-```
+
+ Choose{' '}
+ {aggregate
+ ? 'up to eight configurations and 100 recorded runs'
+ : study.singleRun
+ ? 'up to eight runs'
+ : 'up to 50 runs'}{' '}
+ for readable overlays.
+
+ ) : (
+ <>
+ {aggregate && (
+
+ {human(s.timeStat)} at each matching timestamp
+ {s.timeSpread === 'none'
+ ? ''
+ : s.timeSpread === 'iqr'
+ ? ' with the middle 50% of recorded values'
+ : ' with the observed min–max range'}
+ . Summaries use all available runs in each selected configuration.
+ Bands are not confidence intervals.
+