Result parsing¶
SimulationResult is what simulate(...) returns. It's a thin container over the EnergyPlus output directory with typed accessors for the SQLite, CSV, ESO/MTR, HTML tabular, ERR, and RDD/MDD files. Use result.sql for almost everything — it's complete, queryable, and consistent across EnergyPlus versions. Reach for result.eso only when SQLite output wasn't produced, or when you want the fastest extraction of a few variables from a large .eso.
When to use¶
- A simulation has completed and you want time series or tabular reports.
- You're aggregating results across a batch (peak loads, energy totals, comfort hours).
- You need to read the
.errfile to surface warnings/errors. - You want available output variables before adding
Output:Variableobjects.
Quick start¶
result = simulate(doc, "weather.epw")
# 1. Always check errors first
if result.errors.has_severe:
print(result.errors.summary())
raise SystemExit
# 2. Time series
ts = result.sql.get_timeseries("Zone Mean Air Temperature", "Office", frequency="Hourly")
print(max(ts.values), ts.units) # 27.3 'C'
# 3. Tabular reports (annual energy, sizing, …)
rows = result.sql.get_tabular_data(report_name="AnnualBuildingUtilityPerformanceSummary")
What's on SimulationResult¶
| Accessor | Returns | Lazy? |
|---|---|---|
result.errors |
ErrorReport (always available) |
Eager (parsed on construction). |
result.sql |
SQLResult | None |
Lazy — opens the SQLite file on first access. |
result.csv |
CSVResult | None |
Lazy. |
result.eso / result.mtr |
ESOResult | None |
Lazy — dictionary parsed on access, variable data on get_column. |
result.html |
HTMLResult | None |
Lazy. |
result.variables |
OutputVariableIndex | None |
Lazy — parses .rdd/.mdd. |
result.sql_path / .err_path / .eso_path / .mtr_path / .csv_path / .html_path / .rdd_path / .mdd_path |
Path | None |
Direct file paths. |
result.migration_report |
MigrationReport | None |
Set if auto_migrate=True. |
All None returns mean "the file doesn't exist" — EnergyPlus may not produce CSV/HTML unless you asked for them in the IDF (Output:Variable, Output:Table:SummaryReports).
Errors (always check first)¶
errs = result.errors # ErrorReport
print(errs.summary()) # human-readable rollup
if errs.has_severe: # property, not method
for msg in errs.severe: # tuple[ErrorMessage, ...]
print(msg.severity, msg.message)
for warn in errs.warnings:
print(warn.message)
severe() includes both Severe and Fatal. Always treat a non-empty severe() as a simulation failure even if EnergyPlus exited zero — many corrupt-output cases leave the file present but unreadable.
Time series from SQLite¶
ts = result.sql.get_timeseries(
variable_name="Zone Mean Air Temperature",
key_value="Office", # zone, surface, system name; "*" for environment vars
frequency="Hourly", # optional filter
environment=None, # None=all, "annual", or "sizing"
)
print(ts.units, len(ts.values)) # 'C' 8760
ts.timestamps # tuple[datetime, ...]
ts.values # tuple[float, ...]
# Pandas (requires idfkit[dataframes])
df = ts.to_dataframe()
get_timeseries is case-insensitive on key value and raises KeyError if the variable isn't in the database (typically because you didn't add an Output:Variable for it in the IDF — see Output variable discovery).
Use frequency to disambiguate when the same variable is reported at multiple frequencies (e.g. "Hourly", "Daily", "Monthly"). Use environment="annual" to skip design-day data when both ran.
Tabular reports¶
# All rows of a single table
rows = result.sql.get_tabular_data(
report_name="AnnualBuildingUtilityPerformanceSummary",
table_name="End Uses",
)
for r in rows:
print(r.row_name, r.column_name, r.value, r.units)
# Single value
total = result.sql.get_tabular_value(
report_name="AnnualBuildingUtilityPerformanceSummary",
table_name="End Uses",
row_name="Total End Uses",
column_name="Electricity",
)
get_tabular_data returns list[TabularRow]; there is no direct tabular-to-DataFrame helper. If you need a DataFrame, build one yourself: pd.DataFrame([dataclasses.asdict(r) for r in rows]). SQLResult.to_dataframe is for time-series variables only (see above).
To enumerate what's available:
for r in result.sql.list_reports():
print(r)
for v in result.sql.list_variables():
print(v.name, v.key_value, v.frequency, v.units)
for e in result.sql.list_environments():
print(e.environment_type, e.name)
Raw SQL¶
For ad-hoc queries SQLResult exposes the underlying connection:
rows = result.sql.query(
"SELECT KeyValue, AVG(Value) FROM ReportData "
"JOIN ReportDataDictionary USING(ReportDataDictionaryIndex) "
"JOIN Time USING(TimeIndex) "
"WHERE Name = ? AND COALESCE(WarmupFlag, 0) = 0 "
"GROUP BY KeyValue",
("Zone Mean Air Temperature",),
)
The EnergyPlus SQLite schema is documented at bigladdersoftware.com/epx/docs/ (the SQL Output sections).
CSV (if readvars=True)¶
# Only available if you called simulate(..., readvars=True)
csv = result.csv
if csv:
csv.timestamps # tuple[str, ...]
for col in csv.columns: # each CSVColumn has parsed metadata
print(col.variable_name, col.key_value, col.units)
col = csv.get_column("Electricity:Facility") # by variable name, optional key_value=
if col:
print(col.units, max(col.values))
Prefer SQL — CSV is one shot per Output:Variable, while SQL is queryable.
HTML tabular¶
The HTML parser is mostly useful for surfacing reports that aren't in SQLite (rare in modern EnergyPlus).
ESO / MTR time series¶
The .eso (Standard Output) and .mtr (Meter) files are EnergyPlus's native time-series format. result.eso and result.mtr return an ESOResult parsed by the same reader. Prefer SQLite when it's available; use ESO when it isn't, or to pull a handful of variables out of a very large file cheaply.
# ESO time series — use when the model has no Output:SQLite, or to pull a few
# variables out of a large .eso fast. The reader parses the dictionary eagerly
# but the data lazily.
eso = result.eso # ESOResult | None
if eso:
# Lazy: a single scan that float-parses ONLY this variable.
col = eso.get_column("Zone Mean Air Temperature", "Office")
if col:
print(col.variable.units, len(col.values)) # 'C' 8760
col.values # tuple[float, ...]
col.timestamps # tuple[datetime, ...]
df = col.to_dataframe() # requires idfkit[dataframes]
# A file has several environments: the design days, then the run period.
# get_column returns the LAST one (the run period) by default. To pick a
# specific design day, map index -> title via .environments:
for env in eso.environments:
print(env.index, env.title) # 0 '... ANN HTG ...' 1 '... ANN CLG ...' 2 'RUN PERIOD 1'
htg = next(e.index for e in eso.environments if "HTG" in e.title)
design_day_col = eso.get_column("Zone Mean Air Temperature", "Office", environment_index=htg)
# And back the other way — a column tells you its environment:
if design_day_col:
env = eso.environments[design_day_col.environment_index]
# Eager full parse:
all_columns = eso.columns # tuple[ESOColumn, ...] — every variable
# Meter files (.mtr) use the same reader:
mtr = result.mtr
if mtr:
meter = mtr.get_column("Electricity:Facility") # meters have no key value
The reader is lazy by design: constructing it parses only the data dictionary, and get_column(name, key) runs a single byte-level scan that float-parses only the requested variable — so reading one variable from a large .eso doesn't pay to parse the whole file. Accessing .columns (or from_file(..., eager=True)) materializes every variable in one pass. ESOColumn exposes .values, .timestamps, and .variable (an ESOVariable with .variable_name/.key_value/.units/.frequency); timestamps use the reference year 2017, like the SQL reader. ESO carries no calendar year, so only the year differs from SQL — values and month/day/hour match exactly.
Selecting an environment. A file holds several environments — the sizing design days, then the weather run period. get_column returns the last one (the run period) by default. To target a specific design day, read result.eso.environments (a tuple of ESOEnvironment) and match environment_index to its title — EnergyPlus encodes no environment type in the ESO format, so the title ("... ANN HTG 99% CONDNS DB", "RUN PERIOD 1", …) is the discriminator. Each ESOColumn.environment_index cross-references back into result.eso.environments.
Output variable discovery¶
If you want to know what variables you could report before adding Output:Variable objects, parse the RDD/MDD files. These list every variable EnergyPlus knows how to emit for the current model:
idx = result.variables
if idx:
for v in idx.variables:
if "Cooling" in v.name:
print(v.name, v.key, v.frequency)
for m in idx.meters:
print(m.name)
To produce RDD/MDD, the IDF needs Output:VariableDictionary, IDF; (or regular). The idfkit.simulation.prep_outputs helper adds it for you:
from idfkit.simulation import prep_outputs
prep_outputs(doc) # adds Output:VariableDictionary (and Output:SQLite)
result = simulate(doc, "weather.epw")
Reconstructing a result from a directory¶
If you've simulated outside Python (or cached the outputs), rebuild a SimulationResult from the run directory:
from idfkit.simulation import SimulationResult
result = SimulationResult.from_directory("/path/to/run", output_prefix="eplus")
Plotting helpers¶
from idfkit.simulation.plotting import (
plot_temperature_profile,
plot_energy_balance,
plot_comfort_hours,
)
if result.sql:
plot_temperature_profile(result.sql, zones=["Office"])
plot_energy_balance(result.sql)
plot_comfort_hours(result.sql, zones=["Office"])
Backends: matplotlib (default, requires idfkit[plot]) and plotly (requires idfkit[plotly]). Pick with get_default_backend(...) or pass backend= explicitly.
Common mistakes¶
accessing result.sql without a None check
check or trust the runner's auto-injection
assuming a variable exists
add the output, or discover what's available
ignoring result.errors.has_severe()
gate on errors
Related¶
- simulation-execution.md — running simulations that produce these results.
- API docs: py.idfkit.com/simulation/parsers/