Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
d6df69b
Expose export_res
rajeee Jan 16, 2026
5b1c9ea
Add timezone
rajeee Jan 20, 2026
d0d245a
Narrow python version requirement
rajeee Jan 21, 2026
b35ee4e
Add EV and PV
rajeee Jan 27, 2026
32df0db
ResStock output mode
rajeee Jan 27, 2026
25eb856
output control
rajeee Feb 10, 2026
7a2ab4b
Simple ruff fixes
rajeee Feb 10, 2026
1fa56a2
Simple ruff fixes
rajeee Feb 10, 2026
4979a0b
Improve output control architecture with add_output() helper
rajeee Feb 10, 2026
3792440
Fix output control: cache enabled_outputs, clean up junk files
rajeee Feb 10, 2026
908e39d
Reduce CI matrix to one Python version per OS
rajeee Feb 10, 2026
29aeefb
Fix ruff formatting in Dwelling, Envelope, and Water
rajeee Feb 10, 2026
5b7c650
Tighten simulation speed check to 5 seconds
rajeee Feb 10, 2026
7d494a9
Fix output registry to include Test Equipment, restore original test …
rajeee Feb 10, 2026
e5579fe
Update changelog and docs for ResStock output support
rajeee Mar 4, 2026
5366477
Fix changelog: accurate HPXML change description
rajeee Mar 4, 2026
5289d67
Simplify annual totals: sum already-converted timeseries columns
rajeee Mar 5, 2026
3a98ec3
Encapsulate ResStock output logic in ResStockOutput class
rajeee Mar 5, 2026
03bf7af
Add golden test suite for 69 ResStock buildings
rajeee Mar 10, 2026
520ee40
Add seed parameter and EV schedule names
rajeee Mar 10, 2026
16c6345
Add pytest-xdist dependency and golden test marker
rajeee Mar 10, 2026
b075331
Add ResStock output format unit tests
rajeee Mar 10, 2026
c49cd93
Add golden test data for 69 ResStock buildings
rajeee Mar 10, 2026
2ddbea0
Fix ruff formatting in resstock.py
rajeee Mar 10, 2026
230a414
Enable parallel test execution in CI
rajeee Mar 10, 2026
853f167
Pin ruff version and fix formatting
rajeee Mar 10, 2026
4ec20f7
Add EnergyPlus cross-validation to golden tests
rajeee Mar 10, 2026
8fc3408
Replace per-test EPlus warnings with consolidated summary tables
rajeee Mar 10, 2026
ea67305
Skip golden tests on non-macOS CI runners
rajeee Mar 10, 2026
8d125e4
Fix ruff formatting in conftest and golden test
rajeee Mar 10, 2026
8fea378
Move EPlus comparison to standalone script with CSV output
rajeee Mar 10, 2026
8b2ea5c
Fix CRLF line endings in comparison CSVs
rajeee Mar 10, 2026
fbb9e93
Fix CRLF line endings in comparison CSVs
rajeee Mar 10, 2026
12de790
Fix CI comparison CSV push by fetching latest before push
rajeee Mar 10, 2026
62cdf13
Update golden test README with EPlus comparison docs
rajeee Mar 10, 2026
0c3e1f3
Fix CI commit step and add update_resstock_golden_reference.py
rajeee Mar 10, 2026
b4e6042
Move to_underscore_case to ochre/utils/resstock.py as canonical location
rajeee Mar 10, 2026
8d8e588
Fix CI: fetch PR branch before checkout in comparison CSV step
rajeee Mar 10, 2026
9de5b21
Fix F541: remove unnecessary f-string prefix
rajeee Mar 10, 2026
739dea0
Update EPlus comparison CSVs
github-actions[bot] Mar 10, 2026
2925ffa
Polish PR: deduplicate test utils, fix stale CSVs, cleanup
rajeee Mar 10, 2026
bed7910
Simplify golden test scripts: remove guards, derive column names
rajeee Mar 10, 2026
4d0b04f
Fix to_underscore_case port and centralize test constants
rajeee Mar 10, 2026
b0bcd6a
Add 130 missing ResStock outputs to crosswalk CSV
rajeee Mar 10, 2026
fd8b452
Rename _parse_unit to parse_unit for public API consistency
rajeee Mar 10, 2026
3d96710
Reorganize golden test directory structure and overhaul README
rajeee Mar 13, 2026
2ef476d
Remove buildstock-query from project dependencies
rajeee Mar 13, 2026
649cb57
Run generate_ochre_result.py before compare step in CI
rajeee Mar 13, 2026
2bce4dd
Fix ruff formatting in copy_eplus_result.py and generate_minimal_buil…
rajeee Mar 13, 2026
56551b0
Fix CI: tolerate expected simulation failures in golden tests
rajeee Mar 13, 2026
f27282c
Fix verbosity=0 breaking resstock result saving
rajeee Mar 13, 2026
5d51c43
Run generate_ochre_result.py before pytest in CI
rajeee Mar 13, 2026
3da5e45
Fix golden tests: blank non-OCHRE energy columns in reference CSV
rajeee Mar 14, 2026
3010e72
Update EPlus comparison CSVs
github-actions[bot] Mar 14, 2026
24acc28
Address PR #205 review comments and improve golden test infrastructure
rajeee Mar 20, 2026
e828539
Show FAILED instead of NA for crashed buildings in comparison CSVs
rajeee Mar 20, 2026
bc17c8b
Fix ruff formatting
rajeee Mar 20, 2026
e785355
Regenerate comparison CSVs with FAILED labels and per-metric columns
rajeee Mar 20, 2026
5f5e78a
Update ochre_annual_result.csv reference with OCHRE-only output columns
rajeee Mar 20, 2026
9e67cb5
Rewrite golden test README and gitignore ochre_annual_result_new.csv
rajeee Mar 20, 2026
7c94803
Update readme
rajeee Mar 20, 2026
a2a0cdd
Remove unused --time_zone CLI option from this PR
rajeee Mar 20, 2026
31f6a48
Enumerate all unit conversions explicitly and raise on unknown pairs
rajeee Mar 20, 2026
723285c
Note that ResStock OCHRE support requires sdr_2025_ochre_support branch
rajeee Mar 20, 2026
928d23b
Remove redundant TestPintConversions; TestConvertUnits covers all cases
rajeee Mar 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 30 additions & 4 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,21 @@ on:
# Run on all pull requests
types: [opened, synchronize, reopened]

# Add concurrency to cancel in-progress jobs when a new commit is pushed to the PR
# This avoids CI jobs "stacking up" for the same PR
concurrency:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent!

# Cancel in-progress runs when a new workflow with the same concurrency group is queued
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.10", "3.11", "3.12"]
python-version: ["3.11"]

steps:
- name: Checkout repository
Expand All @@ -29,11 +36,30 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install -e .
pip install pytest pytest-cov
pip install pytest pytest-cov pytest-xdist

- name: Generate OCHRE golden results
if: matrix.os == 'macos-latest'
run: python test/resstock_golden/generate_ochre_result.py

- name: Run tests
run: |
pytest test/ -v --tb=short --cov=ochre --cov-report=xml --cov-report=term-missing
pytest test/ -v --tb=short -n auto --cov=ochre --cov-report=xml --cov-report=term-missing ${{ matrix.os != 'macos-latest' && '-m "not golden"' || '' }}

- name: Compare OCHRE vs EnergyPlus
if: matrix.os == 'macos-latest'
run: python test/resstock_golden/compare_ochre_and_eplus.py

- name: Commit comparison CSVs
if: matrix.os == 'macos-latest' && github.event_name == 'pull_request'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git fetch origin ${{ github.head_ref }}
git checkout -B ${{ github.head_ref }} FETCH_HEAD
git add test/resstock_golden/comparison/
git diff --cached --quiet || git commit -m "Update EPlus comparison CSVs"
git push origin ${{ github.head_ref }}
Comment on lines +53 to +62

- name: Upload coverage to Codecov
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.11'
Expand All @@ -59,7 +85,7 @@ jobs:
- name: Install linting tools
run: |
python -m pip install --upgrade pip
pip install ruff
pip install -e ".[dev]"

- name: Run ruff linter
run: ruff check . --output-format=github
Expand Down
9 changes: 7 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
*.pyc
*.log
*.rej
.DS_Store

.idea/
Expand All @@ -12,6 +13,10 @@ dist/
docs/_build
*.egg-info/
*.code-workspace

.history
ochre/defaults/Input Files/OCHRE*
.history
test/outputs/
test/resstock_golden/ochre_result/bldg*/
test/resstock_golden/ochre_result/ochre_annual_result_new.csv
test_run/
uv.lock
8 changes: 8 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
## OCHRE Changelog

### OCHRE v0.9.4
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we'll want to do a new release after we merge this in. Makes sense with how substantial this is, just flagging that we'll be doing this soon.

- Added ResStock output mode producing `results_timeseries.csv` and `results_annual.csv`
[#167](https://github.com/NREL/OCHRE/issues/167)
- Added output registry and verbosity-based output control system
- Added CLI options for `--output_format` and `--export_res`
- Added EV and PV parsing from dedicated HPXML sections (Vehicles, Photovoltaics)
- Narrowed Python version requirement

### OCHRE v0.9.3
- Allow 120V (aka low power) heat pump water heaters
- Applied ruff code formatting to entire codebase
Expand Down
2 changes: 2 additions & 0 deletions docs/source/InputsAndArguments.rst
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ The table below lists the optional arguments for creating a ``Dwelling`` model.
+---------------------------+------------------------+-------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------+
| ``export_res`` | ``datetime.timedelta`` | None (saves files at end of simulation only) | Saves intermediate time series results to files at the given simulation interval |
+---------------------------+------------------------+-------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------+
| ``output_format`` | string | ``ochre`` | Output format: ``ochre`` (default) or ``resstock``. See :ref:`resstock-output` for details. |
+---------------------------+------------------------+-------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------+
| ``save_results`` | boolean | True if ``verbosity > 0`` | Save results files, including time series files, metrics file, OCHRE schedule file, and status file |
+---------------------------+------------------------+-------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------+
| ``save_args_to_json`` | boolean | False | Save all input arguments to json file, including user defined arguments |
Expand Down
69 changes: 69 additions & 0 deletions docs/source/Outputs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,75 @@ The ``verbosity`` will also impact the print statements provided during the
simulation. Setting ``verbosity >= 3`` will allow most print statements to be
written.

.. _resstock-output:

ResStock Output Format
----------------------

.. note::

ResStock OCHRE support is currently available on the
``sdr_2025_ochre_support`` branch of ResStock.

When ``output_format`` is set to ``resstock``, OCHRE produces output files
compatible with the ResStock analysis workflow instead of the default OCHRE
output files. This mode generates two files:

- ``results_timeseries.csv``: Time series results with ResStock-compatible
column names and units. Uses a two-header-row format (column names and units)
and supports incremental append via ``export_res``. This format is also
compatible with `DView <https://github.com/NREL/wex/wiki/DView>`_, NREL's
time-series data viewer.

- ``results_annual.csv``: Annual energy totals by end use in MBtu.

OCHRE output columns are mapped to ResStock column names using a crosswalk CSV
(``ochre/defaults/resstock_ochre_crosswalk.csv``). Unit conversions are applied
automatically (e.g., kW to kWh, Celsius to Fahrenheit, W to kBtu).

Known Limitations
^^^^^^^^^^^^^^^^^

OCHRE models a subset of what ResStock/EnergyPlus covers. The crosswalk CSV
indicates which ResStock metrics have an OCHRE equivalent (rows with a blank
OCHRE column are not populated). Key limitations are summarized below.

**Unsupported end uses.** The following equipment types are not modeled in OCHRE
and their ResStock columns will be blank:

- Dehumidifier
- Whole house fan
- Solar thermal pump
- Mechanical ventilation precooling/preheating

**Partial equipment support.** Some equipment is modeled but with constraints:

- **PV**: Only one PV system is parsed from HPXML. Homes with panels on
multiple roof orientations will only use the first system
(`#223 <https://github.com/NatLabRockies/OCHRE/issues/223>`_).
- **Electric vehicle**: Only one vehicle is parsed. Additional vehicles in the
HPXML Vehicles section are ignored.
- **Home battery**: OCHRE has a Battery equipment model, but battery inputs are
not yet parsed from HPXML
(`#224 <https://github.com/NatLabRockies/OCHRE/issues/224>`_).

**Unmapped ResStock metric categories.** The following categories of ResStock
output columns are not currently populated by OCHRE:

- **Emissions**: CO2e emissions calculations are not implemented.
- **Weather**: Only drybulb temperature is mapped. Wetbulb, relative humidity,
wind speed, and solar radiation are not yet populated from the weather file
(`#222 <https://github.com/NatLabRockies/OCHRE/issues/222>`_).
- **Peak electricity**: Winter, summer, and annual peak metrics.
- **Alternative fuels**: End uses for propane, fuel oil, coal, wood cord, and
wood pellets.
- **HVAC design**: Design loads, capacities, and design temperatures.
- **Electrical panel**: Breaker space counts and panel load metrics.
- **Hot water volumes**: Clothes washer, dishwasher, fixtures, and distribution
waste volumes.

See the crosswalk CSV for the complete column-by-column mapping.

.. _all-metrics:

All Metrics
Expand Down
86 changes: 54 additions & 32 deletions ochre/Dwelling.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
nested_update,
update_equipment_properties,
save_json,
ResStockOutput,
)
from ochre.Models import Envelope
from ochre.Equipment import (
Expand Down Expand Up @@ -72,10 +73,17 @@ def __init__(self, metrics_verbosity=3, save_schedule_columns=None, save_args_to
ochre_schedule_file = os.path.join(self.output_path, self.name + "_schedule" + extn)
else:
ochre_schedule_file = None

# ResStock output format: encapsulated in ResStockOutput
if self.output_format == "resstock":
self._resstock_output = ResStockOutput(self.output_path, self.time_res)
else:
self._resstock_output = None
else:
self.metrics_file = None
self.hourly_output_file = None
ochre_schedule_file = None
self._resstock_output = None

# Load properties from HPXML file
properties, weather_station = load_hpxml(**house_args)
Expand Down Expand Up @@ -289,42 +297,56 @@ def generate_results(self):
# See docs for list of results and verbosity levels
results = super().generate_results()

if self.verbosity >= 0:
results.update(
{
"Total Electric Power (kW)": self.total_p_kw,
"Total Reactive Power (kVAR)": self.total_q_kvar,
"Total Gas Power (therms/hour)": self.total_gas_therms_per_hour,
}
)

if self.verbosity >= 6:
hours_per_step = self.time_res / dt.timedelta(hours=1)
results.update(
{
"Total Electric Energy (kWh)": self.total_p_kw * hours_per_step,
"Total Reactive Energy (kVARh)": self.total_q_kvar * hours_per_step,
"Total Gas Energy (therms)": self.total_gas_therms_per_hour * hours_per_step,
}
)

if self.verbosity >= 2:
for end_use, equipment in self.equipment_by_end_use.items():
if equipment and any([e.is_electric for e in equipment]):
results[end_use + " Electric Power (kW)"] = sum([e.electric_kw for e in equipment])
for end_use, equipment in self.equipment_by_end_use.items():
if equipment and any([e.is_gas for e in equipment]):
results[end_use + " Gas Power (therms/hour)"] = sum([e.gas_therms_per_hour for e in equipment])
if self.verbosity >= 8:
for end_use, equipment in self.equipment_by_end_use.items():
if equipment and any([e.is_electric for e in equipment]):
results[end_use + " Reactive Power (kVAR)"] = sum([e.reactive_kvar for e in equipment])
results["Grid Voltage (-)"] = self.voltage
self.add_output(results, "Total Electric Power (kW)", self.total_p_kw)
self.add_output(results, "Total Reactive Power (kVAR)", self.total_q_kvar)
self.add_output(results, "Total Gas Power (therms/hour)", self.total_gas_therms_per_hour)

hours_per_step = self.time_res / dt.timedelta(hours=1)
self.add_output(results, "Total Electric Energy (kWh)", self.total_p_kw * hours_per_step)
self.add_output(results, "Total Reactive Energy (kVARh)", self.total_q_kvar * hours_per_step)
self.add_output(results, "Total Gas Energy (therms)", self.total_gas_therms_per_hour * hours_per_step)

# End-use level power aggregation
for end_use, equipment in self.equipment_by_end_use.items():
if equipment and any([e.is_electric for e in equipment]):
self.add_output(results, end_use + " Electric Power (kW)", sum([e.electric_kw for e in equipment]))
for end_use, equipment in self.equipment_by_end_use.items():
if equipment and any([e.is_gas for e in equipment]):
self.add_output(
results, end_use + " Gas Power (therms/hour)", sum([e.gas_therms_per_hour for e in equipment])
)
for end_use, equipment in self.equipment_by_end_use.items():
if equipment and any([e.is_electric for e in equipment]):
self.add_output(results, end_use + " Reactive Power (kVAR)", sum([e.reactive_kvar for e in equipment]))
self.add_output(results, "Grid Voltage (-)", self.voltage)

return results

def export_results(self):
"""
Export results to file. For ResStock format, converts and writes to
results_timeseries.csv instead of ochre.csv.
"""
if self.output_format != "resstock":
return super().export_results()

df = pd.DataFrame(self.results).set_index("Time") if self.results else None
self.results.clear()

if not self.save_results or df is None or self._resstock_output is None:
return df

self._resstock_output.export_chunk(df)
return df

def finalize(self, failed=False):
# save final results
# For ResStock format, we need custom finalization
if self._resstock_output:
df = pd.DataFrame(self.results).set_index("Time") if self.results else None
self.results.clear()
return self._resstock_output.finalize(df=df, failed=failed)

# Standard OCHRE format
df = super().finalize(failed)

if df is not None:
Expand Down
25 changes: 13 additions & 12 deletions ochre/Equipment/Battery.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ class BatteryThermalModel(OneNodeRCModel):

def generate_results(self):
results = super().generate_results()
if self.verbosity >= 7:
results[f"{self.name} (C)"] = self.states[0]
self.add_output(results, f"{self.name} (C)", self.states[0])

return results

Expand Down Expand Up @@ -438,16 +437,18 @@ def calculate_degradation(self):

def generate_results(self):
results = super().generate_results()
if self.verbosity >= 3:
results[f"{self.end_use} SOC (-)"] = self.soc
if self.verbosity >= 7:
results[f"{self.end_use} Energy to Discharge (kWh)"] = self.get_kwh_remaining()
if self.degradation_states is not None:
results[f"{self.end_use} Nominal Capacity (kWh)"] = self.capacity_kwh_nominal
results[f"{self.end_use} Actual Capacity (kWh)"] = self.capacity_kwh
results[f"{self.end_use} Degradation State Q1"] = self.degradation_states[0]
results[f"{self.end_use} Degradation State Q2"] = self.degradation_states[1]
results[f"{self.end_use} Degradation State Q3"] = self.degradation_states[2]

self.add_output(results, f"{self.end_use} SOC (-)", self.soc)

self.add_output(results, f"{self.end_use} Energy to Discharge (kWh)", self.get_kwh_remaining())

if self.degradation_states is not None:
self.add_output(results, f"{self.end_use} Nominal Capacity (kWh)", self.capacity_kwh_nominal)
self.add_output(results, f"{self.end_use} Actual Capacity (kWh)", self.capacity_kwh)
self.add_output(results, f"{self.end_use} Degradation State Q1", self.degradation_states[0])
self.add_output(results, f"{self.end_use} Degradation State Q2", self.degradation_states[1])
self.add_output(results, f"{self.end_use} Degradation State Q3", self.degradation_states[2])

if self.save_ebm_results:
results.update(self.make_equivalent_battery_model())

Expand Down
27 changes: 15 additions & 12 deletions ochre/Equipment/EV.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,18 +334,21 @@ def make_equivalent_battery_model(self):
def generate_results(self):
results = super().generate_results()

if self.verbosity >= 3:
results[f"{self.end_use} SOC (-)"] = self.soc
results[f"{self.end_use} Unmet Load (kWh)"] = self.unmet_load
if self.verbosity >= 4:
results[f"{self.end_use} Parked"] = self.in_event
if self.verbosity >= 7:
# results[f'{self.end_use} Setpoint Power (kW)'] = self.setpoint_power or 0
results[f"{self.end_use} Start Time"] = self.event_start
results[f"{self.end_use} End Time"] = self.event_end
if self.verbosity >= 7:
remaining_charge_minutes = (1 - self.soc) * self.capacity / (self.max_power_ctrl * EV_EFFICIENCY) * 60
results[f"{self.end_use} Remaining Charge Time (min)"] = remaining_charge_minutes
self.add_output(results, f"{self.end_use} SOC (-)", self.soc)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is more a future comment if/when we address speed: this slightly bumps up our calculation time if we always calculate these with no intent to output them. I think speed is not very high on our list of priorities at the moment vs. functionality/calibration though.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.add_output does support deferred calculation for expensive calculation (just pass a lambda) - and only performs it if the verbosity level requires that. We could use that for the more involved calculation.


self.add_output(results, f"{self.end_use} Unmet Load (kWh)", self.unmet_load)

self.add_output(results, f"{self.end_use} Parked", self.in_event)

self.add_output(results, f"{self.end_use} Start Time", self.event_start)

self.add_output(results, f"{self.end_use} End Time", self.event_end)

self.add_output(
results,
f"{self.end_use} Remaining Charge Time (min)",
lambda: (1 - self.soc) * self.capacity / (self.max_power_ctrl * EV_EFFICIENCY) * 60,
)

if self.save_ebm_results:
results.update(self.make_equivalent_battery_model())
Expand Down
Loading
Loading