A MIDAS module for integrating synthetic mobility demand from OMOD (OpenStreetMap Mobility Demand Generator) into a mosaik-based co-simulation, enabling electric vehicles (EVs) to be moved temporally within a power distribution grid simulation.
midas-omod reads OMOD-generated daily activity plans (JSON format)
and creates mosaik EVAgent entities that simulate the movement and
charging of electric vehicles. Each agent follows its OMOD daily plan,
consuming battery energy while driving and charging while parked at
locations with charging infrastructure (HOME, WORK).
OMOD (Java) → JSON file → midas-omod (parser) → EVAgent entities
↓
mosaik co-simulation
↓
midas-powergrid (pandapower)
↓
midas-store (persistence)
- OMOD generates a JSON file with daily activity plans per agent (Activities and Trips with coordinates, times, distances, and transport modes).
- midas-omod parses the JSON, filters for agents with at least
one
CAR_DRIVERtrip, and createsEVAgentmosaik entities. - Each
EVAgentfollows its daily plan: driving trips consume SoC, parking at HOME/WORK charges the battery (ifgrid_connectedisTrue). - The charging power output is connected to a midas-powergrid
load node's
p_mwattribute, coupling mobility to the grid. - Results are persisted via midas-store.
pip install midas-omodFor development:
git clone <repo-url>
cd midas-omod
pip install -e ".[dev]"- Python >= 3.10
mosaik_apimidas-mosaik >= 2.1.0mosaik(for running scenarios)midas-powergrid(for grid coupling)midas-store(for persistence, optional)
OMOD is a Java-based tool available at
github.com/L-Strobel/omod.
Since v0.2.0, midas-omod can run OMOD automatically from Python —
no manual java command required.
from midas_omod.omod_runner import run_omod
output = run_omod(
jar_path="/path/to/omod.jar",
osm_file="region.osm.pbf",
area_geojson="area.geojson",
n_agents=100,
n_days=1,
out_path="omod_output.json",
)from midas_omod.omod_runner import ensure_omod_jar
jar = ensure_omod_jar(version="latest", target_dir="./jars")python scenario_example.py \
--omod-jar /path/to/omod.jar \
--osm-file region.osm.pbf \
--area-geojson area.geojson \
--n-agents-omod 100 --n-days-omod 1If --json-path is provided, it takes priority and OMOD is not
re-run. Results are cached by parameter hash in .cache/omod/.
You can still run OMOD manually and pass the JSON file:
java -jar omod.jar \
--osm_file area.osm.pbf \
--area_geojson focus.geojson \
--n_agents 100 \
--n_days 1 \
--out output.json \
--return_path_coords yThen use --json-path output.json.
- Java JDK 17+ must be installed and
javaon PATH. - The OMOD JAR can be downloaded from GitHub Releases.
- An OpenStreetMap
.osm.pbffile for your region. - A focus area GeoJSON (e.g. from geojson.io).
See also the OMOD publication: Strobel et al., "OMOD: An open-source tool for creating disaggregated mobility demand based on OpenStreetMap", Computers, Environment and Urban Systems, 2023 (arXiv).
# Using the bundled test data (no OMOD installation needed):
python scenario_example.py \
--json-path midas_omod/tests/data/test_agents.json \
--num-agents 2
# Using real OMOD output:
python scenario_example.py --json-path path/to/omod_output.jsonimport mosaik
SIM_CONFIG = {
"Omod": {"python": "midas_omod.simulator:OmodSimulator"},
}
world = mosaik.World(SIM_CONFIG)
sim = world.start(
"Omod",
time_resolution=60,
json_path="omod_output.json",
battery_capacity_kwh=60,
charging_power_kw=11,
consumption_kwh_per_km=0.18,
)
agents = sim.EVAgent.create(num=10)| Attribute | Type | Direction | Description |
|---|---|---|---|
lat |
float | output | Current latitude |
lon |
float | output | Current longitude |
soc_kwh |
float | output | State of charge in kWh |
charging_power_kw |
float | output | Current charging power in kW (0 if not charging) |
status |
str | output | "parked" or "driving" |
grid_connected |
bool | input | Whether a grid/charging connection is available |
| Parameter | Default | Description |
|---|---|---|
json_path |
— | Path to OMOD JSON file |
battery_capacity_kwh |
60.0 | Max battery capacity (kWh) |
charging_power_kw |
11.0 | Nominal charging power (kW) |
consumption_kwh_per_km |
0.18 | Energy consumption (kWh/km) |
- Driving: SoC decreases by
distanceKilometer × consumption_kwh_per_km(prorated over the trip duration across multiple steps). - Charging: SoC increases by
charging_power_kw × step_hourswhile parked atHOMEorWORKifgrid_connected == True, capped atbattery_capacity_kwh. - Position: Interpolated linearly along
lats/lonspath coordinates during trips (or between start/end activities if no path coordinates are available).
pytest midas_omod/tests/ -vTests cover:
- OMOD JSON parsing and CAR_DRIVER filtering (
test_parser.py) - SoC discharging during trips (
test_simulator_soc.py) - SoC charging at HOME/WORK with grid connection (
test_simulator_soc.py) - Position interpolation (
test_simulator_soc.py) get_dataoutput (test_simulator_soc.py)
midas-omod/
├── midas_omod/
│ ├── __init__.py # Version
│ ├── meta.py # mosaik META definition
│ ├── model.py # EVAgent data model
│ ├── parser.py # OMOD JSON parser
│ ├── simulator.py # OmodSimulator (mosaik API)
│ ├── module.py # MIDAS module registration
│ └── tests/
│ ├── test_parser.py
│ ├── test_simulator_soc.py
│ └── data/
│ └── test_agents.json # OMOD-format test fixture
├── scenario_example.py # Example mosaik scenario
├── pyproject.toml # Package configuration
└── README.md
In a real co-simulation, each EV agent should feed its charging (or
V2G discharge) power to the grid bus at its current location.
However, mosaik connections are static — an entity cannot
reconnect to a different target at runtime. midas-omod solves this
with a LoadRouterSimulator:
- Each
LoadRouteentity has a fixedtarget_bus_id. - It receives
grid_power_mwandbus_idfrom all EV agents. - At each step, it forwards only power from agents whose current
bus_idmatchestarget_bus_id; all others contribute 0. - The output
p_mwis connected to a midas-powergridLoad.p_mw.
This is not a power aggregation workaround — pandapower natively sums multiple loads at the same bus (see docs/aggregation_verification.md). The LoadRouter is needed because mosaik's topology is static while EV positions are dynamic.
The BusMapper class maps geographic coordinates to bus IDs using:
- Haversine distance (when bus coordinates are available).
- Activity-type mapping (
{"HOME": "Bus-0", ...}) as fallback. - Round-robin assignment for test scenarios without geodata.
The bus_id is recomputed at every activity transition in the
OMOD plan, and set to None while driving (no grid connection).
Vehicle-to-grid (V2G) discharge is opt-in via enable_v2g=True.
When enabled and v2g_request_mw > 0, the agent discharges up to
the requested power, bounded by available SoC above min_soc_kwh.
grid_power_mw is negative during V2G discharge.
Contributions are welcome. Please follow these guidelines:
- Coding style: The project uses ruff
for linting and formatting with a line length of 79 characters. Run
ruff check .before submitting a pull request. - Tests: All new features or bug fixes must include pytest tests.
Run
pytest midas_omod/tests/ -vto verify. - Commits: Use atomic commits with descriptive messages
(e.g.
Add CI workflow,Fix SoC calculation edge case). - Pull requests: Open a PR against the
mainbranch. Ensure CI passes (pytest + ruff on Python 3.10, 3.11, 3.12) before requesting review. - License: By contributing, you agree that your contributions are licensed under the LGPL-2.1.
- No live coupling with OMOD: OMOD runs as a separate Java process and must be executed beforehand. The JSON output is read statically at simulation start; there is no runtime interface between OMOD and midas-omod.
- Static 24-hour window: The simulator models a single day (day 0) from the OMOD output. Multi-day plans are not yet supported.
- Single-day plan only: Only the first day's
mobilityDemandentry is used. Agents with multi-day plans will only simulate day 0. - No traffic simulation: Vehicles move according to OMOD's pre-computed schedules. There is no interaction with a traffic simulator (e.g. SUMO).
- Linear position interpolation: During trips, positions are
interpolated linearly along OMOD's
lats/lonspath coordinates or between start/end activities. Realistic routing is not performed.
The BusMapper uses a three-tier fallback strategy to assign EV agents
to power grid buses. Users should be aware of the following limitations:
-
Haversine mapping requires bus coordinates: Many MIDAS reference grids (including
midasmv) do not include geographic coordinates (bus_geodata) for their buses. Without coordinates, the Haversine-based nearest-bus lookup is not available. -
Activity-type mapping is coarse: The fallback
{activity_type: bus_id}mapping assigns all HOME activities to one bus, all WORK activities to another, etc. This does not reflect the spatial distribution of real charging points within a grid area. -
Round-robin fallback is arbitrary: When neither coordinates nor activity-type mapping are available, the
BusMapperfalls back to round-robin assignment acrossfallback_bus_ids. This assignment is not geographically meaningful — it simply cycles through the available bus IDs. AWARNINGlog message is emitted each time this fallback is used. For any realistic scenario, provide eitherbus_coords(with lat/lon per bus) or alocation_bus_mapping. -
No runtime re-mapping: The bus assignment is computed at activity-transition time from the OMOD plan. If the grid topology changes during the simulation (e.g. due to switching), the bus mapping is not updated.
-
Bus ID format depends on the grid: The EID format for grid entities is simulator-specific. For
midas-powergridwith themidasmvgrid, load EIDs follow the pattern0-load-{load_idx}-{bus_idx}and bus EIDs follow0-bus-{bus_idx}. When using a different grid, thelocation_bus_mappingmust be updated accordingly.
LGPL-2.1-only — see LICENSE for details.