|
11 | 11 | nested_update, |
12 | 12 | update_equipment_properties, |
13 | 13 | 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, |
14 | 20 | ) |
15 | 21 | from ochre.Models import Envelope |
16 | 22 | from ochre.Equipment import ( |
@@ -80,10 +86,34 @@ def __init__( |
80 | 86 | ochre_schedule_file = os.path.join(self.output_path, self.name + "_schedule" + extn) |
81 | 87 | else: |
82 | 88 | 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 |
83 | 108 | else: |
84 | 109 | self.metrics_file = None |
85 | 110 | self.hourly_output_file = None |
86 | 111 | 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 |
87 | 117 |
|
88 | 118 | # Load properties from HPXML file |
89 | 119 | properties, weather_station = load_hpxml(**house_args) |
@@ -347,8 +377,48 @@ def generate_results(self): |
347 | 377 |
|
348 | 378 | return results |
349 | 379 |
|
| 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 | + |
350 | 416 | 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 |
352 | 422 | df = super().finalize(failed) |
353 | 423 |
|
354 | 424 | if df is not None: |
@@ -382,6 +452,44 @@ def finalize(self, failed=False): |
382 | 452 |
|
383 | 453 | return df, metrics, df_hourly |
384 | 454 |
|
| 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 | + |
385 | 493 | def simulate(self, metrics_verbosity=None, **kwargs): |
386 | 494 | if metrics_verbosity is not None: |
387 | 495 | self.metrics_verbosity = metrics_verbosity |
|
0 commit comments