Skip to content

Commit 3e26700

Browse files
committed
ResStock output mode
1 parent a04a19e commit 3e26700

File tree

5 files changed

+540
-2
lines changed

5 files changed

+540
-2
lines changed

ochre/Dwelling.py

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@
1111
nested_update,
1212
update_equipment_properties,
1313
save_json,
14+
load_crosswalk,
15+
build_resstock_timeseries,
16+
write_resstock_timeseries,
17+
update_resstock_annual,
18+
accumulate_annual_sums,
19+
convert_accumulated_sums_to_annual,
1420
)
1521
from ochre.Models import Envelope
1622
from ochre.Equipment import (
@@ -80,10 +86,34 @@ def __init__(
8086
ochre_schedule_file = os.path.join(self.output_path, self.name + "_schedule" + extn)
8187
else:
8288
ochre_schedule_file = None
89+
90+
# ResStock output format: set up different file paths and load crosswalk
91+
if self.output_format == 'resstock':
92+
self.resstock_timeseries_file = os.path.join(self.output_path, 'results_timeseries.csv')
93+
self.resstock_annual_file = os.path.join(self.output_path, 'results_annual.csv')
94+
self.resstock_crosswalk = load_crosswalk()
95+
self.resstock_units_dict = None # Will be set on first export
96+
self._resstock_annual_sums = {} # Accumulate kWh for annual totals
97+
# Remove existing timeseries file (we replace it completely)
98+
# Note: results_annual.csv is NOT removed - we append/update to it
99+
if os.path.exists(self.resstock_timeseries_file):
100+
self.print("Removing previous results file:", self.resstock_timeseries_file)
101+
os.remove(self.resstock_timeseries_file)
102+
else:
103+
self.resstock_timeseries_file = None
104+
self.resstock_annual_file = None
105+
self.resstock_crosswalk = None
106+
self.resstock_units_dict = None
107+
self._resstock_annual_sums = None
83108
else:
84109
self.metrics_file = None
85110
self.hourly_output_file = None
86111
ochre_schedule_file = None
112+
self.resstock_timeseries_file = None
113+
self.resstock_annual_file = None
114+
self.resstock_crosswalk = None
115+
self.resstock_units_dict = None
116+
self._resstock_annual_sums = None
87117

88118
# Load properties from HPXML file
89119
properties, weather_station = load_hpxml(**house_args)
@@ -347,8 +377,48 @@ def generate_results(self):
347377

348378
return results
349379

380+
def export_results(self):
381+
"""
382+
Export results to file. For ResStock format, converts and writes to
383+
results_timeseries.csv instead of ochre.csv.
384+
"""
385+
if self.output_format != 'resstock':
386+
return super().export_results()
387+
388+
# ResStock format: convert and write to results_timeseries.csv
389+
df = pd.DataFrame(self.results).set_index('Time') if self.results else None
390+
self.results.clear()
391+
392+
if not self.save_results or df is None or self.resstock_timeseries_file is None:
393+
return df
394+
395+
# Convert to ResStock format
396+
resstock_df, units_dict = build_resstock_timeseries(
397+
df, self.resstock_crosswalk, self.time_res
398+
)
399+
400+
# Store units dict for later appends (only set on first export)
401+
if self.resstock_units_dict is None:
402+
self.resstock_units_dict = units_dict
403+
404+
# Accumulate annual sums (in kWh) for later conversion to MBtu
405+
self._resstock_annual_sums = accumulate_annual_sums(
406+
df, self.resstock_crosswalk, self.time_res, self._resstock_annual_sums
407+
)
408+
409+
# Write or append to timeseries file
410+
append = os.path.exists(self.resstock_timeseries_file)
411+
write_resstock_timeseries(resstock_df, self.resstock_units_dict,
412+
self.resstock_timeseries_file, append=append)
413+
414+
return df
415+
350416
def finalize(self, failed=False):
351-
# save final results
417+
# For ResStock format, we need custom finalization
418+
if self.output_format == 'resstock':
419+
return self._finalize_resstock(failed)
420+
421+
# Standard OCHRE format
352422
df = super().finalize(failed)
353423

354424
if df is not None:
@@ -382,6 +452,44 @@ def finalize(self, failed=False):
382452

383453
return df, metrics, df_hourly
384454

455+
def _finalize_resstock(self, failed=False):
456+
"""
457+
Finalize simulation for ResStock output format.
458+
Writes final timeseries data and calculates annual totals.
459+
"""
460+
# Export any remaining results
461+
df = self.export_results()
462+
463+
# Print status and save status file (similar to Simulator.finalize)
464+
status = 'failed' if failed else 'complete'
465+
if self.main_simulator and self.verbosity >= 3:
466+
if self.resstock_timeseries_file and os.path.exists(self.resstock_timeseries_file):
467+
results = f'time series results saved to: {self.resstock_timeseries_file}'
468+
else:
469+
results = 'no results'
470+
self.print(f'Simulation {status}, {results}')
471+
472+
if self.save_status and self.output_path:
473+
status_file = os.path.join(self.output_path, f'{self.name}_{status}')
474+
with open(status_file, 'a'):
475+
pass
476+
477+
# Finalize sub_simulators
478+
for sub in self.sub_simulators:
479+
sub.finalize(failed=failed)
480+
481+
# Calculate and write annual totals
482+
if self._resstock_annual_sums and self.resstock_annual_file:
483+
annual_totals = convert_accumulated_sums_to_annual(
484+
self._resstock_annual_sums, self.resstock_crosswalk
485+
)
486+
update_resstock_annual(annual_totals, self.resstock_annual_file)
487+
self.print("Annual results saved to:", self.resstock_annual_file)
488+
489+
# For ResStock mode, we don't compute OCHRE metrics or hourly aggregation
490+
# The ResStock format is the final output
491+
return df, None, None
492+
385493
def simulate(self, metrics_verbosity=None, **kwargs):
386494
if metrics_verbosity is not None:
387495
self.metrics_verbosity = metrics_verbosity

ochre/Simulator.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ class Simulator:
1717

1818
def __init__(self, start_time, time_res, duration, name=None, main_sim_name=None, seed=None,
1919
verbosity=3, save_results=None, save_status=None, output_path=None, output_to_parquet=False,
20-
initialization_time=None, export_res=None, **kwargs):
20+
initialization_time=None, export_res=None, output_format='ochre', **kwargs):
21+
self.output_format = output_format
2122
if name is not None:
2223
self.name = name
2324
self.main_sim_name = main_sim_name

ochre/cli.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ def create_dwelling(
3939
initialization_time=1,
4040
export_res=None,
4141
time_zone=None,
42+
output_format='ochre',
4243
):
4344
# Update input file paths
4445
if not os.path.isabs(hpxml_file):
@@ -77,6 +78,7 @@ def create_dwelling(
7778
output_path=output_path,
7879
verbosity=verbosity,
7980
time_zone=time_zone,
81+
output_format=output_format,
8082
**weather_args,
8183
)
8284

@@ -245,6 +247,12 @@ def common_options(f):
245247
help="Export interval in days (exports results periodically to reduce memory). "
246248
"Recommended: 30 days for 1-minute resolution, 60 days for 5-minute resolution",
247249
),
250+
click.option(
251+
"--output_format",
252+
default="ochre",
253+
type=click.Choice(["ochre", "resstock"]),
254+
help="Output format: 'ochre' (default) or 'resstock' (ResStock-compatible CSV)",
255+
),
248256
]
249257
return functools.reduce(lambda x, opt: opt(x), options[::-1], f)
250258

ochre/utils/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,12 @@
88

99
from .hpxml import load_hpxml
1010
from .schedule import load_schedule
11+
from .resstock import (
12+
load_crosswalk,
13+
build_resstock_timeseries,
14+
calculate_annual_totals,
15+
write_resstock_timeseries,
16+
update_resstock_annual,
17+
accumulate_annual_sums,
18+
convert_accumulated_sums_to_annual,
19+
)

0 commit comments

Comments
 (0)