Simulation execution¶
idfkit.simulation wraps the EnergyPlus executable as a subprocess. You pass an IDFDocument and a weather file; it writes the model to a run directory, optionally runs ExpandObjects and ground-heat preprocessors, invokes EnergyPlus, and returns a SimulationResult pointing at the outputs.
When to use¶
- You want to run a single simulation against a weather file.
- You're running parameter sweeps (use
simulate_batch/async_simulate_batch). - You need progress events streaming back as EnergyPlus runs.
- You want results cached by content hash to skip re-running unchanged inputs.
Quick start¶
from idfkit import load_idf
from idfkit.simulation import simulate
doc = load_idf("building.idf")
result = simulate(doc, "weather.epw", design_day=True)
print(result.errors.summary())
Core API¶
from idfkit.simulation import (
simulate, # sync, single model
async_simulate, # async, single model
simulate_batch, # sync, multiple jobs
find_energyplus, # discover the EnergyPlus install
EnergyPlusConfig, # pre-configured install handle
SimulationCache, # content-addressed cache
SimulationJob, # one job in a batch
BatchResult, # results from simulate_batch
SimulationResult, # result container — see result-parsing.md
SimulationProgress, # progress event payload
)
simulate is the workhorse. The most useful arguments:
| Argument | Default | Purpose |
|---|---|---|
model |
required | IDFDocument to simulate. Not mutated. |
weather |
required | Path to an .epw file. |
output_dir |
temp dir | Where outputs are written. |
energyplus |
auto-discover | Pre-built EnergyPlusConfig (skip discovery). |
expand_objects |
True |
Run ExpandObjects + ground-heat preprocessors as needed. |
annual |
False |
-a flag (annual sim, skip design days). |
design_day |
False |
-D flag (design-day only, skip annual). |
output_suffix |
"C" |
Combined-table output. "L" for legacy, "D" for timestamped. |
readvars |
False |
Run ReadVarsESO to produce eplusout.csv. |
timeout |
3600 |
Wall-clock seconds for the main EnergyPlus process. |
cache |
None |
SimulationCache for content-hash skip. |
auto_migrate |
False |
Forward-migrate the model to match the installed EP version. |
on_progress |
None |
Callback or "tqdm" for progress events. |
fs |
None |
FileSystem backend (e.g. S3) for storing outputs remotely. |
Discovering EnergyPlus¶
from idfkit.simulation import find_energyplus
config = find_energyplus() # latest installed
config = find_energyplus(version=(24, 1, 0))
print(config.executable, config.version)
Discovery checks $ENERGYPLUS_DIR, $PATH, and standard install locations (/usr/local/EnergyPlus-*, /Applications/EnergyPlus-*, C:\EnergyPlusV*). Pass the result to simulate(..., energyplus=config) to skip rediscovery on every call.
If EnergyPlus is missing, EnergyPlusNotFoundError is raised.
Version handling¶
simulate refuses to run if model.version doesn't match the installed EnergyPlus version. Two ways to handle:
# Option 1 — explicit migration before simulate (recommended for production)
from idfkit import migrate
doc = load_idf("v22_model.idf")
doc = migrate(doc, target_version=config.version).migrated_model
# Option 2 — let simulate forward-migrate transparently
result = simulate(doc, "weather.epw", auto_migrate=True)
print(result.migration_report.summary()) # what was changed
Backward migration (installed EP older than the model) is never attempted — it raises VersionMismatchError.
Design day vs. annual¶
# Quick smoke-test with the heating/cooling design days only — seconds
result = simulate(doc, "weather.epw", design_day=True)
# Full annual run — minutes
result = simulate(doc, "weather.epw", annual=True)
# Default (both flags False) — EnergyPlus's default, governed by SimulationControl in the IDF
result = simulate(doc, "weather.epw")
Async¶
For UI loops or asyncio-based batch tooling:
import asyncio
from idfkit.simulation import async_simulate
async def main():
result = await async_simulate(doc, "weather.epw")
print(result.errors.summary())
asyncio.run(main())
Batch parameter sweeps¶
from idfkit.simulation import simulate_batch, SimulationJob
jobs = []
for wwr in (0.2, 0.3, 0.4, 0.5):
variant = doc.copy()
from idfkit import set_wwr
set_wwr(variant, wwr=wwr)
jobs.append(SimulationJob(model=variant, weather="weather.epw", label=f"wwr_{int(wwr * 100)}"))
batch = simulate_batch(jobs, max_workers=4)
for job, result in zip(jobs, batch.results):
print(job.label, result.errors.summary())
async_simulate_batch and async_simulate_batch_stream give you async/streaming variants. simulate_batch runs jobs in parallel processes (use max_workers to cap parallelism).
Caching¶
If you re-simulate the same model + weather + flags often (e.g. iterating on output processing), wrap in a SimulationCache:
from idfkit.simulation import SimulationCache
cache = SimulationCache(cache_dir=".sim_cache")
result = simulate(doc, "weather.epw", cache=cache) # first call runs
result = simulate(doc, "weather.epw", cache=cache) # second call cache-hits, milliseconds
The cache key is the SHA-256 of the (model bytes + weather bytes + flags) tuple. Any change invalidates the entry.
Progress events¶
from idfkit.simulation import SimulationProgress
def on_progress(event: SimulationProgress) -> None:
print(event.phase, event.percent, event.message)
result = simulate(doc, "weather.epw", on_progress=on_progress)
# Or for an interactive shell:
result = simulate(doc, "weather.epw", on_progress="tqdm") # requires idfkit[progress]
Events carry phase (warmup/run/postprocessing), percent (where known), the EnergyPlus message, and the timestamp.
Remote storage (S3)¶
from idfkit.simulation import simulate, S3FileSystem
fs = S3FileSystem(bucket="my-sim-outputs")
result = simulate(
doc,
"weather.epw",
output_dir="runs/2026-05-23/baseline",
fs=fs,
)
# EnergyPlus runs in a local temp dir; outputs are uploaded to s3://my-sim-outputs/runs/2026-05-23/baseline/
The weather file must be local — remote weather is not auto-downloaded. Pre-stage with WeatherDownloader (see weather-data.md).
Common mistakes¶
running without checking the version
use auto_migrate=True or migrate explicitly
assuming result.sql always exists
let the runner ensure SQLite output is on
re-running the same simulation across iterations
cache
silently swallowing simulation errors
gate on errors
Related¶
- result-parsing.md — extracting data from
SimulationResult. - weather-data.md — getting an
.epwfile. - hvac-templates.md — why
expand_objects=Truematters. - version-migration.md — when
auto_migrateruns. - API docs: py.idfkit.com/simulation/