Skip to content

Commit

Permalink
User Improvements (#86)
Browse files Browse the repository at this point in the history
Addresses #85.

- Added error for very short duration simulations
- Added OCHREException to handle errors
- Updated docs and added trademark logo

- [x] Reference the issue your PR is fixing
- [x] Assign at least 1 reviewer for your PR
- [x] Test with run_dwelling.py or other script
- [x] Update documentation as appropriate
- [x] Update changelog as appropriate
  • Loading branch information
jmaguire1 committed Nov 6, 2023
2 parents e7f04b6 + b524333 commit 688985c
Show file tree
Hide file tree
Showing 23 changed files with 153 additions and 136 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
![OCHRE](docs\source\images\OCHRE-Logo-Horiz-2Color.png)

# OCHRE: The Object-oriented Controllable High-resolution Residential Energy Model

A high-fidelity, high-resolution residential building model with behind-the-meter DERs and flexible load models
that integrates with controllers and distribution models in building-to-grid co-simulation platforms.
OCHRE™ is a Python-based building energy modeling (BEM) tool designed to model flexible loads in residential buildings. OCHRE includes detailed models and controls for flexible devices including HVAC equipment, water heaters, electric vehicles, solar PV, and batteries. It is designed to run in co-simulation with custom controllers, aggregators, and grid models.

The full documentation for OCHRE can be found at https://ochre-docs-final.readthedocs.io/en/latest/

Expand Down
4 changes: 4 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## OCHRE Changelog

### Changes from PRs

- Added OCHREException class to handle errors

### OCHRE v0.8.4-beta

- Fixed bug with air infiltration inputs (works with ResStock 3.0 and 3.1, and OS-HPXML 1.6.0)
Expand Down
25 changes: 13 additions & 12 deletions docs/source/Introduction.rst
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
Getting Started
===============

.. image:: images/OCHRE-Logo-Stacked-2Color.png
.. image:: images/OCHRE-Logo-Horiz-2Color.png
:width: 500
:alt: OCHRE logo

OCHRE Overview
--------------

OCHRE is a Python-based building energy modeling (BEM) tool designed
specifically for modeling flexible loads in residential buildings. OCHRE
includes detailed models and controls for flexible devices including
HVAC equipment, water heaters, electric vehicles, solar PV, and
batteries. It is designed to run in co-simulation with custom
controllers, aggregators, and grid models. OCHRE is integrated with
`OS-HPXML <https://openstudio-hpxml.readthedocs.io/en/latest/index.html>`__,
and any OS-HPXML integrated workflow can be used to generate OCHRE input
files.

More information about OCHRE is available on `NREL’s
OCHRE\ |tm| is a Python-based building energy modeling (BEM) tool designed to
model flexible loads in residential buildings. OCHRE includes detailed models
and controls for flexible devices including HVAC equipment, water heaters,
electric vehicles, solar PV, and batteries. It is designed to run in
co-simulation with custom controllers, aggregators, and grid models. OCHRE
integrates with `OS-HPXML
<https://openstudio-hpxml.readthedocs.io/en/latest/index.html>`__, and any
OS-HPXML integrated workflow can be used to generate OCHRE input files.

.. |tm| unicode:: U+2122

More information about OCHRE is available on `NREL's
website <https://www.nrel.gov/grid/ochre.html>`__ and on
`Github <https://github.com/NREL/OCHRE>`__.

Expand Down
10 changes: 5 additions & 5 deletions ochre/Analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import psychrolib
from numpy.polynomial.polynomial import Polynomial

from ochre.utils import convert, load_csv, ZONES
from ochre.utils import OCHREException, convert, load_csv, ZONES
from ochre.Equipment import ALL_END_USES

psychrolib.SetUnitSystem(psychrolib.SI)
Expand Down Expand Up @@ -111,10 +111,10 @@ def load_eplus_file(file_name, eplus_format='ResStock', variable_names_file='Var
elif eplus_format == 'Eplus Detailed':
df = pd.read_csv(file_name)
else:
raise Exception(f'Unknown EnergyPlus output file format: {eplus_format}')
raise OCHREException(f'Unknown EnergyPlus output file format: {eplus_format}')

if len(df) != 8760:
raise Exception(f'Comparison file should have 8760 rows: {file_name}')
raise OCHREException(f'Comparison file should have 8760 rows: {file_name}')

# Load variable names and units file
df_names = load_csv(variable_names_file)
Expand Down Expand Up @@ -560,12 +560,12 @@ def find_files_from_ending(path, ending, priority_list=None, **kwargs):
# select file match in priority list. If not found, throw an error
matches = [f for f in priority_list if f in matches]
if len(matches) != 1:
raise Exception(f'{len(matches)} files found matching {ending} in {root}: {matches}')
raise OCHREException(f'{len(matches)} files found matching {ending} in {root}: {matches}')

file_path = os.path.join(root, matches[0])
run_name = get_parent_folders(file_path, **kwargs)
if run_name in all_files:
raise Exception(f'Multiple files found with same run name ({run_name}).'
raise OCHREException(f'Multiple files found with same run name ({run_name}).'
'Try increasing dirs_to_include. Error from:', file_path)

all_files[run_name] = file_path
Expand Down
10 changes: 5 additions & 5 deletions ochre/Dwelling.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import numpy as np

from ochre import Simulator, Analysis
from ochre.utils import load_hpxml, load_schedule, nested_update, update_equipment_properties, save_json
from ochre.utils import OCHREException, load_hpxml, load_schedule, nested_update, update_equipment_properties, save_json
from ochre.Models import Envelope
from ochre.Equipment import *

Expand Down Expand Up @@ -136,7 +136,7 @@ def __init__(self, metrics_verbosity=6, save_schedule_columns=None, save_args_to
# check if there is more than 1 equipment per end use. Raise error for HVAC/WH, else print a warning
if len(eq) > 1:
if end_use in ['HVAC Heating', 'HVAC Cooling', 'Water Heating']:
raise Exception(f'More than 1 equipment defined for {end_use}: {eq}')
raise OCHREException(f'More than 1 equipment defined for {end_use}: {eq}')
elif end_use not in ['Lighting', 'Other']:
self.warn(f'More than 1 equipment defined for {end_use}: {eq}')

Expand All @@ -145,7 +145,7 @@ def __init__(self, metrics_verbosity=6, save_schedule_columns=None, save_args_to
ideal = eq.use_ideal_capacity if isinstance(eq, (HVAC, WaterHeater)) else True
if not ideal:
if self.time_res >= dt.timedelta(minutes=15):
raise Exception(f'Cannot use non-ideal equipment {name} with large time step of'
raise OCHREException(f'Cannot use non-ideal equipment {name} with large time step of'
f' {self.time_res}')
if self.time_res >= dt.timedelta(minutes=5):
self.warn(f'Using non-ideal equipment {name} with large time step of {self.time_res}')
Expand Down Expand Up @@ -190,7 +190,7 @@ def get_equipment_by_end_use(self, end_use):
elif end_use in self.equipment:
return self.equipment[end_use]
else:
raise Exception(f'Unknown end use: {end_use}')
raise OCHREException(f'Unknown end use: {end_use}')

def update_inputs(self, schedule_inputs=None):
if schedule_inputs is None:
Expand All @@ -199,7 +199,7 @@ def update_inputs(self, schedule_inputs=None):
# check voltage from external model
self.voltage = schedule_inputs.get('Voltage (-)', 1)
if np.isnan(self.voltage) or self.voltage < 0:
raise Exception(f'Error reading voltage for house {self.name}: {self.voltage}')
raise OCHREException(f'Error reading voltage for house {self.name}: {self.voltage}')
if self.voltage == 0:
# Enter resilience mode when voltage is 0. Assumes home generator maintains voltage at 1 p.u.
schedule_inputs['Voltage (-)'] = 1
Expand Down
5 changes: 3 additions & 2 deletions ochre/Equipment/Battery.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from scipy.interpolate import interp1d
import rainflow

from ochre.utils import OCHREException
from ochre.utils.units import convert, degC_to_K
from ochre.Models import OneNodeRCModel
from ochre.Equipment import Generator
Expand Down Expand Up @@ -94,7 +95,7 @@ def __init__(self, enable_degradation=True, efficiency_type='advanced', enable_t
elif self.zone:
self.thermal_model.states[0] = self.zone.temperature
else:
raise Exception('Must specify "Initial Battery Temperature (C)"')
raise OCHREException('Must specify "Initial Battery Temperature (C)"')
else:
self.thermal_model = None

Expand Down Expand Up @@ -353,7 +354,7 @@ def calculate_degradation(self):

# raise warning/error if degradation is too high
if sum(self.degradation_states) >= 1:
raise Exception('{} degraded beyond useful life.'.format(self.name))
raise OCHREException('{} degraded beyond useful life.'.format(self.name))
elif sum(self.degradation_states) >= 0.7:
self.warn('Degraded beyond useful life.')

Expand Down
10 changes: 5 additions & 5 deletions ochre/Equipment/EV.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import datetime as dt
import pandas as pd

from ochre.utils import load_csv
from ochre.utils import OCHREException, load_csv
from ochre.Equipment import EventBasedLoad, ScheduledLoad

# For EVI-Pro assumptions, see Section 1.2 and 1.3:
Expand Down Expand Up @@ -33,7 +33,7 @@ class ElectricVehicle(EventBasedLoad):
def __init__(self, vehicle_type, charging_level, capacity=None, mileage=None, enable_part_load=None, **kwargs):
# get EV battery capacity and mileage
if capacity is None and mileage is None:
raise Exception('Must specify capacity or mileage for {}'.format(self.name))
raise OCHREException('Must specify capacity or mileage for {}'.format(self.name))
elif capacity is not None:
self.capacity = capacity # in kWh
mileage = self.capacity * EV_FUEL_ECONOMY # in mi
Expand All @@ -44,7 +44,7 @@ def __init__(self, vehicle_type, charging_level, capacity=None, mileage=None, en
# get charging level and set option for part load setpoints
charging_level = charging_level.replace(' ', '')
if str(charging_level) not in ['Level0', 'Level1', 'Level2']:
raise Exception('Unknown vehicle type for {}: {}'.format(self.name, charging_level))
raise OCHREException('Unknown vehicle type for {}: {}'.format(self.name, charging_level))
self.charging_level = str(charging_level)
if enable_part_load is None:
enable_part_load = self.charging_level == 'Level2'
Expand All @@ -56,7 +56,7 @@ def __init__(self, vehicle_type, charging_level, capacity=None, mileage=None, en
elif vehicle_type == 'BEV':
vehicle_num = 3 if mileage < 175 else 4
else:
raise Exception('Unknown vehicle type for {}: {}'.format(self.name, vehicle_type))
raise OCHREException('Unknown vehicle type for {}: {}'.format(self.name, vehicle_type))
self.vehicle_type = vehicle_type

# charging model
Expand Down Expand Up @@ -99,7 +99,7 @@ def generate_all_events(self, probabilities, event_data, eq_schedule, ambient_ev
if eq_schedule is not None:
# get average daily ambient temperature for generating events and round to nearest 5 C
if 'Ambient Dry Bulb (C)' not in eq_schedule:
raise Exception('EV model requires ambient dry bulb temperature in schedule.')
raise OCHREException('EV model requires ambient dry bulb temperature in schedule.')
temps_by_day = eq_schedule['Ambient Dry Bulb (C)']
temps_by_day = temps_by_day.groupby(temps_by_day.index.date).mean() # in C
temps_by_day = ((temps_by_day / 5).round() * 5).astype(int).clip(lower=-20, upper=40)
Expand Down
10 changes: 5 additions & 5 deletions ochre/Equipment/Equipment.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import numpy as np

from ochre import Simulator
from ochre.utils import load_csv
from ochre.utils import OCHREException, load_csv


class Equipment(Simulator):
Expand Down Expand Up @@ -100,7 +100,7 @@ def update_duty_cycles(self, *duty_cycles):
if len(duty_cycles) == len(self.modes) - 1:
duty_cycles.append(1 - sum(duty_cycles))
if len(duty_cycles) != len(self.modes):
raise Exception('Error parsing duty cycles. Expected a list of length equal or 1 less than ' +
raise OCHREException('Error parsing duty cycles. Expected a list of length equal or 1 less than ' +
'the number of modes ({}): {}'.format(len(self.modes), duty_cycles))

self.duty_cycle_by_mode = dict(zip(self.modes, duty_cycles))
Expand All @@ -116,7 +116,7 @@ def calculate_mode_priority(self, *duty_cycles):
:return: list of mode names in order of priority
"""
if self.ext_time_res is None:
raise Exception('External control time resolution is not defined for {}.'.format(self.name))
raise OCHREException('External control time resolution is not defined for {}.'.format(self.name))
if duty_cycles:
self.update_duty_cycles(*duty_cycles)

Expand All @@ -142,7 +142,7 @@ def calculate_mode_priority(self, *duty_cycles):

def update_external_control(self, control_signal):
# Overwrite if external control might exist
raise Exception('Must define external control algorithm for {}'.format(self.name))
raise OCHREException('Must define external control algorithm for {}'.format(self.name))

def update_internal_control(self):
# Returns the equipment mode; can return None if the mode doesn't change
Expand Down Expand Up @@ -199,7 +199,7 @@ def update_model(self, control_signal=None):
self.time_in_mode += self.time_res
else:
if mode not in self.modes:
raise Exception(
raise OCHREException(
"Can't set {} mode to {}. Valid modes are: {}".format(self.name, mode, self.modes))
self.mode = mode
self.time_in_mode = self.time_res
Expand Down
10 changes: 5 additions & 5 deletions ochre/Equipment/EventBasedLoad.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import datetime as dt
import numpy as np

from ochre.utils import load_csv
from ochre.utils import OCHREException, load_csv
from ochre.Equipment import Equipment


Expand Down Expand Up @@ -53,7 +53,7 @@ def import_probabilities(self, equipment_pdf_file=None, equipment_event_file=Non
elif equipment_event_file is not None:
raise NotImplementedError
else:
raise Exception('Must specify a PDF or Event file for {}.'.format(self.name))
raise OCHREException('Must specify a PDF or Event file for {}.'.format(self.name))

def initialize_schedule(self, **kwargs):
schedule = super().initialize_schedule(**kwargs)
Expand All @@ -75,13 +75,13 @@ def initialize_schedule(self, **kwargs):
negative_times = self.event_schedule['end_time'] - self.event_schedule['start_time'] < dt.timedelta(0)
if negative_times.any():
bad_event = self.event_schedule.loc[negative_times.idxmax()]
raise Exception('{} has event with end time before start time. '
raise OCHREException('{} has event with end time before start time. '
'Event details: \n{}'.format(self.name, bad_event))
overlap = (self.event_schedule['start_time'] - self.event_schedule['end_time'].shift()) < dt.timedelta(0)
if overlap.any():
bad_index = overlap.idxmax()
bad_events = self.event_schedule.loc[bad_index - 1: bad_index + 1]
raise Exception('{} event overlap. Event details: \n{}'.format(self.name, bad_events))
raise OCHREException('{} event overlap. Event details: \n{}'.format(self.name, bad_events))

return schedule

Expand Down Expand Up @@ -138,7 +138,7 @@ def update_external_control(self, control_signal):
if isinstance(delay, (int, bool)):
delay = self.time_res * delay
if not isinstance(delay, dt.timedelta):
raise Exception(f'Unknown delay for {self.name}: {delay}')
raise OCHREException(f'Unknown delay for {self.name}: {delay}')

if delay:
if self.delay_event_end:
Expand Down
5 changes: 3 additions & 2 deletions ochre/Equipment/Generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import datetime as dt
from scipy.interpolate import interp1d

from ochre.utils import OCHREException
from ochre.utils.units import kwh_to_therms
from ochre.Equipment import Equipment

Expand Down Expand Up @@ -50,7 +51,7 @@ def __init__(self, control_type='Off', parameter_file='default_parameters.csv',

# Control parameters
if control_type not in CONTROL_TYPES:
raise Exception('Unknown {} control type: {}'.format(self.name, control_type))
raise OCHREException('Unknown {} control type: {}'.format(self.name, control_type))
self.control_type = control_type

def update_external_control(self, control_signal):
Expand Down Expand Up @@ -161,7 +162,7 @@ def calculate_efficiency(self, electric_kw=None, is_output_power=True):
eff = self.efficiency_rated * (-0.5 * capacity_ratio ** 2 + 1.5 * capacity_ratio)
return min(eff, 0.001) # must be positive
else:
raise Exception('Unknown efficiency type for {}: {}'.format(self.name, self.efficiency_type))
raise OCHREException('Unknown efficiency type for {}: {}'.format(self.name, self.efficiency_type))

def calculate_power_and_heat(self):
if self.mode == 'Off':
Expand Down

0 comments on commit 688985c

Please sign in to comment.