Source code for mswh.system.models

import logging

import numpy as np
import pandas as pd

from mswh.system.components import Converter, Storage, Distribution
from mswh.tools.unit_converters import UnitConv
from mswh.comm.label_map import SwhLabels

log = logging.getLogger(__name__)


[docs]class System(object): """Project level system models: * Assembles system configurations * Performs timestep simulation * Returns annual and timestep project and household level results, such as gas and electricity use, heat delivered, unmet demand and solar fraction. Parameters: sys_params: pd df Main system component performance parameters per project Default: None (default model parameters will get used) backup_params: pd df Backup system performance parameters per project. It should contain a household ID column, otherwise columns identical to params. sys_sizes: pd df Main system component sizes Default: 1. (see individual components for specifics) backup_sizes: pd df Backup system component sizes, contains household id column Default: 1. (see individual components for specifics) weather: pd df Weather data timeseries. Number of rows equals the number of timesteps. Can be generated using the Source.irradiation_and_water_main method Example: >>> sourceASource(read_from_input_dataframes = inputs) Oakland climate zone in CEC weather data is '03': >>> self.weather = source.irradiation_and_water_main('03', method='isotropic diffuse') loads: pd df A dataframe with loads for all individual household served by the project level system. It should contain 3 columns: household id, occupancy and a column with a load array in m3 for each household. Example: >>> loads_com = pd.DataFrame(data = [[1, occ_indiv - 1., 0.8 * load_array],[2, occ_indiv, 1. * load_array],[3, occ_indiv, 1.2 * load_array],[4, occ_indiv + 1., 1.4 * load_array]], columns = [self.c['id'], self.c['occ'], self.c['load_m3']]) timestep: float, h Duration of a single timestep, in hours Default: 1. h log_level: None or python logger logging level, Default: logging.DEBUG This applies for a subset of the class functionality, mostly used to deprecate logger messages for certain calculations. For Example: log_level = logging.ERROR will only throw error messages and ignore INFO, DEBUG and WARNING. Examples: See :func:`mswh.system.tests.test_components <mswh.system.tests.test_components>` module and :func:`scripts/MSWH System Tool.ipynb <scripts/MSWH System Tool.ipynb>` for examples on how to use the methods as stand alone and in a system model simulation. """ def __init__(self, sys_params=None, backup_params=None, \ weather=None, sys_sizes=1., backup_sizes=1., \ loads=None, timestep=1., log_level=logging.DEBUG): # this is to defend from using the models wihtout # setting up the inputs as recommended if (sys_params is None) and (backup_params is None) and \ (weather is None) and (loads is None): msg = "Please consult example setup either in the "\ "test suite or in the example notebooks to "\ "appropriatelly set up a System instance." log.error(msg) raise ValueError # log level (e.g. only partial functionality of the class # is being used and one does not desire to see all infos) self.log_level = log_level logging.getLogger().setLevel(log_level) self.s = SwhLabels().set_prod_labels() self.c = SwhLabels().set_hous_labels() self.r = SwhLabels().set_res_labels() # assume individual system unless the loads contain multiple # individual household loads self.community = False # main system performance parameters self.sys_params = sys_params # backup system performance parameters self.backup_params = backup_params self.weather = weather # prepare loads if loads is None: self.load = 0.01 # m3 msg = 'The system did not receive any load requirement. '\ 'Setting hot water draw to 0.01 m3/h.' log.info(msg) # initiate household level result df assuming 4 # occupants self.cons_total = pd.DataFrame( data=[[1, 4]], columns=[self.c['id'], self.c['occ']]) # for the distribution losses self.load_max = self.load * 1. else: # initiate household level result df self.cons_total = pd.DataFrame( columns=[self.c['id'], self.c['occ'], self.r['gas_use'], self.r['gas_use_s'], self.r['gas_use_w'], self.r['el_use'], self.r['el_use_s'], self.r['el_use_w'], self.r['sol_fra'], self.r['q_del_bckp'], self.r['q_unmet']]) self.cons_total[self.c['id']] = loads[self.c['id']] self.cons_total[self.c['occ']] = loads[self.c['occ']] self.indiv_loads = pd.DataFrame(loads) if loads.shape[0] > 1: self.community = True # set community load by summing individual loads sum_loads = pd.DataFrame([(loads * 1.).sum()]) self.load = sum_loads[self.c['load_m3']].values[0] * 1. self.loads = loads * 1. # annual fractions for each household ann_cons_fract = loads[self.c['load_m3']].apply( lambda x: x.sum()) / self.load.sum() # this is only to avoid division zero sum_loads.loc[0, self.c['load_m3']][ sum_loads.loc[0, self.c['load_m3']] == 0] = np.inf # timeseries household load fractions in the # community load profile self.indiv_load_ratios = loads * 1. self.indiv_load_ratios[self.c['load_m3']] = ( loads[self.c['load_m3']] / pd.concat( [sum_loads[self.c['load_m3']]] * loads.shape[0], ignore_index=True)) self.indiv_load_ratios[self.c['id']] = loads[self.c['id']] self.indiv_load_ratios[self.c['occ']] = loads[self.c['occ']] # prepare for usage after simulation (+1 timestep) self.indiv_load_ratios[self.c['load_m3']] = \ self.indiv_load_ratios[self.c['load_m3']].apply( lambda x: np.nan_to_num(np.append(0, x))) # household load fractions in the total load self.indiv_load_ratios['ann_cons_fract'] = ann_cons_fract # for the distribution system self.load_max = self.load.max() # main system component sizes self.sys_sizes = sys_sizes # backup system component sizes self.backup_sizes = backup_sizes self.timestep = timestep self.num_timesteps = len(self.load) @property def weather(self): return self.__weather @weather.setter def weather(self, value): """Re-extracts weather timeseries if a new weather dataset is assigned to an instance """ self.__weather = value if isinstance(value, pd.DataFrame): self.t_amb = UnitConv( self.weather[self.c['t_amb_C']].values).degC_K(unit_in='degC') self.t_wet_bulb = UnitConv( self.weather[self.c['t_wet_bulb_C']].values).degC_K( unit_in='degC') self.inc_rad = self.weather[self.c['irrad_on_tilt']].values self.t_main = UnitConv( self.weather[self.c['t_main_C']].values).degC_K( unit_in='degC') self.month = np.append(1, self.weather[self.c['month']].values) self.season = np.append( 'winter', self.weather[self.c['season']].values) self.it_is_summer = self.season == self.c['summer'] self.it_is_winter = self.season == self.c['winter'] self.day = np.append(1, self.weather[self.c['day']].values) self.hour = np.append(1, self.weather[self.c['hour']].values) msg = 'Assigned weather data timeseries.' log.info(msg) elif not value: self.t_amb = 293.15 # K self.t_wet_bulb = 283.15 # K self.inc_rad = 800 # W self.t_main = 291.15 # K self.month = None self.day = None self.season = None msg = 'No weather data got passed to converters. '\ 'Setting default scalar values for ambient temperature, '\ '{}, and solar irradiation, {}.' log.info(msg.format(self.t_amb, self.inc_rad))
[docs] def solar_thermal(self, backup='gas'): """Connects the components of the solar thermal system and simulates it in discrete timesteps. Parameters: backup: string retrofit - pulls from the basecase for each household gas, electric - instantaneous WHs (new installations) Returns: self.cons_total: pd df Consumer level energy use [W], heat rates [W], average temperatures [K], and solar fraction for the analysis period. proj_total: pd series Project level energy use [W], heat rates [W], average temperatures [K], and solar fraction for the analysis period. sol_fra: dict Solar fraction. Keys: 'annual', 'monthly' pump_el_use: dict Electricity use broken into end uses 'dist_pump' - distribution pump, if present 'sol_pump' - solar pump ts_res: pd df Timestep project level results for all energy uses [W], heat rates [W], temperatures [K], and the load. backup_ts_cons: dict of dicts Timestep household level results for energy uses [W], and heat rates [W]. rel_err: float Balancing error due to limitations of finite timestep averaging. """ # initiate a dataframe with hourly results ts_res = pd.DataFrame(index=range(0, self.num_timesteps + 1)) # components converters = Converter( params=self.sys_params, weather=self.weather, sizes=self.sys_sizes, log_level=self.log_level) indirect_tank = Storage( params=self.sys_params, type='sol_tank', size=self.sys_sizes, timestep=self.timestep, log_level=self.log_level) # Initiate arrays to hold the results during calculation # (improved performance with arrays, assigning to df post-calc) T_coil_out = np.empty(self.num_timesteps + 1) Q_sol_col = np.zeros(self.num_timesteps + 1) # demand heat rate Q_dem = np.zeros(self.num_timesteps + 1) Q_dem_with_dist_loss = np.zeros(self.num_timesteps + 1) q_dem_balance = np.zeros(self.num_timesteps + 1) # input solar tank gain and loss heat rates Q_coil_sol_tank = np.zeros(self.num_timesteps + 1) Q_loss_lower_sol_tank = np.zeros(self.num_timesteps + 1) Q_loss_upper_sol_tank = np.zeros(self.num_timesteps + 1) # output solar tank heat rates Q_del_sol_tank = np.zeros(self.num_timesteps + 1) Q_unmet_sol_tank = np.zeros(self.num_timesteps + 1) Q_dump_sol_tank = np.zeros(self.num_timesteps + 1) Q_ovrcool_sol_tank = np.zeros(self.num_timesteps + 1) # tank temperature states T_upper_sol_tank = np.empty(self.num_timesteps + 1) T_lower_sol_tank = np.empty(self.num_timesteps + 1) T_set = np.empty(self.num_timesteps + 1) dT_dist_loss = np.empty(self.num_timesteps + 1) Q_dist_loss = np.empty(self.num_timesteps + 1) # instantaneous gas wh states Q_del_bckp = np.zeros(self.num_timesteps + 1) Q_gas_use_bckp = np.zeros(self.num_timesteps + 1) Q_unmet = np.zeros(self.num_timesteps + 1) # pump on time timestep fractions Q_pump_on_fraction = np.empty(self.num_timesteps + 1) # set values for the initial timestep T_coil_out[0] = self.t_main[0] T_upper_sol_tank[0] = self.t_main[0] T_lower_sol_tank[0] = self.t_main[0] # simulate main system for ts in range(self.num_timesteps): sol_col_res = converters.solar_collector( T_coil_out[ts], t_amb=self.t_amb[ts], inc_rad=self.inc_rad[ts]) # gross collector gain that gets fed into the tank coil Q_sol_col[ts] = sol_col_res['Q_gain'] sto_res = indirect_tank.thermal_tank( pre_T_amb=self.t_amb[ts], pre_T_feed=self.t_main[ts], pre_T_upper=T_upper_sol_tank[ts], pre_T_lower=T_lower_sol_tank[ts], pre_V_tap=self.load[ts], pre_Q_in=Q_sol_col[ts], max_V_tap=self.load_max) # solar collector return temperature (from tank) T_coil_out[ts + 1] = sto_res[self.r['t_coil_out']] # demand heat rate Q_dem[ts + 1] = sto_res[self.r['q_dem']] Q_dem_with_dist_loss[ts + 1] = sto_res[self.r['q_dem_tot']] q_dem_balance[ts + 1] = sto_res[self.r['q_dem_balance']] # solar tank heat sources Q_coil_sol_tank[ts + 1] = sto_res[self.r['q_del_sol']] Q_ovrcool_sol_tank[ts + 1] = sto_res[self.r['q_ovrcool_tank']] # solar tank heat sinks Q_loss_lower_sol_tank[ts + 1] = sto_res[self.r['q_loss_low']] Q_loss_upper_sol_tank[ts + 1] = sto_res[self.r['q_loss_up']] Q_del_sol_tank[ts + 1] = sto_res[self.r['q_del_tank']] Q_unmet_sol_tank[ts + 1] = sto_res[self.r['q_unmet_tank']] Q_dump_sol_tank[ts + 1] = sto_res[self.r['q_dump']] # tank temperatures and set temperature T_upper_sol_tank[ts + 1] = sto_res[self.r['t_tank_up']] T_lower_sol_tank[ts + 1] = sto_res[self.r['t_tank_low']] T_set[ts + 1] = sto_res[self.r['t_set']] # distribution system dT_dist_loss[ts + 1] = sto_res[self.r['dt_dist']] Q_dist_loss[ts + 1] = sto_res[self.r['q_dist_loss']] Q_pump_on_fraction[ts + 1] = sto_res[self.r['flow_on_frac']] # simulate backup heater and assign household level gas # consumption if backup == 'gas': backup_proj_total, backup_ts_proj, backup_ts_cons = \ self._gas_instantaneous_backup( Q_unmet_sol_tank, Q_dist_loss) elif backup == 'retrofit': backup_proj_total, backup_ts_proj, backup_ts_cons = \ self._gas_tank_backup( (np.append(0., T_upper_sol_tank[:self.num_timesteps]) - np.append(0., dT_dist_loss[:self.num_timesteps])), np.append(0., T_upper_sol_tank[:self.num_timesteps])) else: msg = 'Backup types supported are gas or retrofit. '\ 'Please provide one of the supported types.' log.error(msg) raise ValueError # Please be aware that outputs in step (ts + 1) are results # for the inputs in step ts ts_res[self.r['q_dem']] = Q_dem ts_res[self.r['q_dem_tot']] = Q_dem_with_dist_loss ts_res[self.r['q_dem_balance']] = q_dem_balance ts_res[self.r['q_del_sol']] = Q_coil_sol_tank ts_res[self.r['q_loss_low']] = Q_loss_lower_sol_tank ts_res[self.r['q_loss_up']] = Q_loss_upper_sol_tank ts_res[self.r['q_del_tank']] = Q_del_sol_tank ts_res[self.r['q_unmet_tank']] = Q_unmet_sol_tank ts_res[self.r['q_dump']] = Q_dump_sol_tank ts_res[self.r['q_ovrcool_tank']] = Q_ovrcool_sol_tank ts_res[self.r['t_tank_up']] = T_upper_sol_tank ts_res[self.r['t_tank_low']] = T_lower_sol_tank ts_res[self.r['t_coil_out']] = T_coil_out ts_res[self.r['t_set']] = T_set ts_res[self.r['dt_dist']] = dT_dist_loss ts_res[self.r['q_dist_loss']] = Q_dist_loss ts_res[self.r['q_dist_loss_at_bckp']] = np.minimum( Q_dist_loss, Q_unmet_sol_tank) * (Q_unmet_sol_tank > 0.) ts_res[self.r['q_dist_loss_at_bckp_sum']] = np.minimum( Q_dist_loss, Q_unmet_sol_tank) * (Q_unmet_sol_tank > 0.) * self.it_is_summer ts_res[self.r['q_dist_loss_at_bckp_win']] = np.minimum( Q_dist_loss, Q_unmet_sol_tank) * (Q_unmet_sol_tank > 0.) * self.it_is_winter ts_res[self.r['q_del_bckp']] = backup_ts_proj[self.r['q_del_bckp']] ts_res[self.r['gas_use']] = backup_ts_proj[self.r['gas_use']] ts_res[self.r['gas_use_s']] = backup_ts_proj[self.r['gas_use_s']] ts_res[self.r['gas_use_w']] = backup_ts_proj[self.r['gas_use_w']] ts_res[self.r['gas_use_no_dist']] = backup_ts_proj[ self.r['gas_use_no_dist']] ts_res[self.r['gas_use_s_no_dist']] = backup_ts_proj[ self.r['gas_use_s_no_dist']] ts_res[self.r['gas_use_w_no_dist']] = backup_ts_proj[ self.r['gas_use_w_no_dist']] ts_res[self.r['q_unmet']] = backup_ts_proj[self.r['q_unmet']] ts_res[self.r['q_del']] = (Q_del_sol_tank + backup_ts_proj[self.r['q_del_bckp']]) # store timestep weather and water main inputs ts_res[self.c['t_amb']] = np.append(0., self.t_amb) ts_res[self.c['t_main']] = np.append(0., self.t_main) # store load ts_res[self.r['proj_load']] = np.append(0., self.load) # get average temperatures and total heat rates t_columns = [col for col in ts_res.columns if 'Temperature' in col] # all other columns contain timestep heat rate q_columns = [x for x in (set(ts_res.columns) - set(t_columns))] # project results proj_total = ts_res.sum() proj_total[t_columns] = ts_res[t_columns].mean() proj_total[self.r['proj_load']] = ts_res[self.r['proj_load']].mean() if not round(proj_total[self.r['gas_use']], 4) == \ round(backup_proj_total[self.r['gas_use']], 4): msg = 'Check project output of the backup - timestep '\ 'sum preformed in this method and the total are '\ 'not equal.' log.error(msg) raise Exception # household results (annual quantities split according # to individual loads) self.cons_total[self.r['q_dem']] = self.indiv_load_ratios[ self.c['load_m3']].apply( lambda x: (x * Q_dem).sum()) self.cons_total[self.r['q_del']] = (self.timestep * self.indiv_load_ratios[self.c['load_m3']].apply( lambda x: (x * Q_del_sol_tank).sum()) + self.cons_total[self.r['q_del_bckp']]) self.cons_total[self.r['q_unmet_tank']] = (self.timestep * self.indiv_load_ratios[self.c['load_m3']].apply( lambda x: (x * Q_unmet_sol_tank).sum())) self.cons_total[self.r['q_del_tank']] = (self.timestep * self.indiv_load_ratios[self.c['load_m3']].apply( lambda x: (x * Q_del_sol_tank).sum())) self.cons_total[self.r['q_dist_loss']] = (self.timestep * self.indiv_load_ratios[self.c['load_m3']].apply( lambda x: (x * Q_dist_loss).sum())) self.cons_total[self.r['q_dist_loss_at_bckp']] = (self.timestep *\ self.indiv_load_ratios[self.c['load_m3']].apply( lambda x: (x * ts_res[ self.r['q_dist_loss_at_bckp']].values).sum())) self.cons_total[self.r['q_dist_loss_at_bckp_win']] = (self.timestep * self.indiv_load_ratios[self.c['load_m3']].apply( lambda x: (x * ts_res[ self.r['q_dist_loss_at_bckp_win']].values).sum())) self.cons_total[self.r['q_dist_loss_at_bckp_sum']] = (self.timestep * self.indiv_load_ratios[self.c['load_m3']].apply( lambda x: (x * ts_res[ self.r['q_dist_loss_at_bckp_sum']].values).sum())) # Calculate pump electricity consumption if self.community: dist_pump_on_timestep_frac = Q_pump_on_fraction * 1. else: # single households do not require # a distribution pump due to the # water main pressure dist_pump_on_timestep_frac = None pump_ts_el_use, pump_el_use, pump_op_hour = \ self._get_pump_energy_use( dist_pump_on_timestep_frac=dist_pump_on_timestep_frac, gain_from_collector=Q_coil_sol_tank) # Store project level electricity consumption proj_total[self.r['el_use']] = pump_el_use['total'] proj_total[self.r['el_use_s']] = ( pump_ts_el_use['total'] * self.it_is_summer).sum() proj_total[self.r['el_use_w']] = ( pump_ts_el_use['total'] * self.it_is_winter).sum() # Assign household level electricity consumption self.cons_total = self._split_utility( self.cons_total, value=proj_total[self.r['el_use']], var=self.r['el_use'], on=self.c['occ'], drop=False) self.cons_total = self._split_utility( self.cons_total, value=proj_total[self.r['el_use_s']], var=self.r['el_use_s'], on=self.c['occ'], drop=False) self.cons_total = self._split_utility( self.cons_total, value=proj_total[self.r['el_use_w']], var=self.r['el_use_w'], on=self.c['occ'], drop=False) # project level solar fraction sol_fra = dict() sol_fra['annual'] = (proj_total[self.r['q_dem']] - proj_total[self.r['q_del_bckp']]) / proj_total[self.r['q_dem']] proj_total[self.r['sol_fra']] = sol_fra['annual'] # get annual and monthly project level solar fractions if self.month is not None: sol_fra_hourly = pd.DataFrame() sol_fra_hourly[self.c['month']] = self.month sol_fra_hourly[self.c['season']] = self.season sol_fra_hourly['hourly'] = (ts_res[self.r['q_dem']] - ts_res[self.r['q_del_bckp']]) / ts_res[self.r['q_dem']] sol_fra['monthly'] = sol_fra_hourly.groupby( self.c['month']).mean().rename( columns={'hourly': self.r['month_sol_fra']}) sol_fra['seasonal'] = sol_fra_hourly.groupby( self.c['season']).mean().rename( columns={'hourly': self.r['season_sol_fra']}) sol_fra['seasonal'] = sol_fra['seasonal'].drop( columns=self.c['month']) # household level solar fraction self.cons_total[self.r['sol_fra']] = \ self.indiv_load_ratios.apply( lambda row: (row[self.c['load_m3']] * ( ts_res[self.r['q_dem']] - ts_res[self.r['q_del_bckp']])).sum() / (row['ann_cons_fract'] * proj_total[self.r['q_dem']]), axis=1) # check annual tank balance _in = proj_total[self.r['q_del_sol']] _outs = (proj_total[self.r['q_del_tank']] + proj_total[self.r['q_dump']] + proj_total[self.r['q_loss_up']] + proj_total[self.r['q_loss_low']] - proj_total[self.r['q_ovrcool_tank']]) rel_err = abs(_in - _outs) / _in if rel_err > .01: msg = 'Solar tank balance error is {}.' log.warning(msg.format(rel_err)) # time indices for timestep results only ts_res[self.c['month']] = self.month ts_res[self.c['season']] = self.season ts_res[self.c['day']] = self.day ts_res[self.c['hour']] = self.hour proj_total[self.c['max_load']] = UnitConv(self.load_max).m3_gal( unit_in='m3') return self.cons_total.round(2), proj_total.round(2), [ proj_total.round(2).to_dict(), sol_fra, pump_el_use, pump_op_hour, ts_res, backup_ts_cons, rel_err]
[docs] def solar_electric(self, backup='electric'): """Connects the components of the solar electric system and enables simulation. Parameters: backup: string electric - instantaneous WHs (new installations) Returns: sys_en_use: dict System level energy use for the analysis period: 'electricity', Wh sol_fra: dict Solar fraction. Keys: 'annual', 'monthly' ts_res: pd df Colums populated with state variable timeseries, such as average timstep heat rates and temperatures res: dict Summarizes ts_res. Any heat rates are summed, while the temperatures are averaged for the analysis period (usually one year) el_use: dict Electricity use broken into end uses 'dist_pump' - distribution pump, if present rel_err: float Balancing error due to limitations of finite timestep averaging. More precisely, due to selecting minimum tank temperature as the lower between the water main and the ambient. """ # initiate a dataframe with hourly results ts_res = pd.DataFrame(index=range(0, self.num_timesteps + 1)) # components converters = Converter( params=self.sys_params, weather=self.weather, sizes=self.sys_sizes) hp_tank = Storage( params=self.sys_params, size=self.sys_sizes, timestep=self.timestep, type='hp_tank') # Initiate arrays to hold the results during calculation # (improved performance with arrays, assigning to df post-calc) # Heat pump electricity usage P_hp = np.zeros(self.num_timesteps + 1) # Photovoltaic gain (before and after inverter) P_pv_ac = np.zeros(self.num_timesteps + 1) P_pv_dc = np.zeros(self.num_timesteps + 1) # Electric power generated by PV, usages P_pv_to_hp = np.zeros(self.num_timesteps + 1) P_pv_after_hp = np.zeros(self.num_timesteps + 1) # Electric power available for feeding back to grid or # powering e.g. household appliances P_surplus = np.zeros(self.num_timesteps + 1) # demand heat rate Q_dem = np.zeros(self.num_timesteps + 1) Q_dem_with_dist_loss = np.zeros(self.num_timesteps + 1) q_dem_balance = np.zeros(self.num_timesteps + 1) # input balance on the heat pump tank Q_hp = np.zeros(self.num_timesteps + 1) Q_loss_lower_hp_tank = np.zeros(self.num_timesteps + 1) Q_loss_upper_hp_tank = np.zeros(self.num_timesteps + 1) # output balance on the heat pump tank Q_del_hp_tank = np.zeros(self.num_timesteps + 1) Q_unmet_hp_tank = np.zeros(self.num_timesteps + 1) Q_dump_hp_tank = np.zeros(self.num_timesteps + 1) Q_ovrcool_hp_tank = np.zeros(self.num_timesteps + 1) # tank temperature states T_upper_hp_tank = np.zeros(self.num_timesteps + 1) T_lower_hp_tank = np.zeros(self.num_timesteps + 1) # set values for the initial timestep T_upper_hp_tank[0] = self.t_main[0] T_lower_hp_tank[0] = self.t_main[0] # ambient air and main water temperatures T_main = np.zeros(self.num_timesteps + 1) T_wet_bulb = np.zeros(self.num_timesteps + 1) T_amb = np.zeros(self.num_timesteps + 1) # set initial values T_main[0] = self.t_main[0] T_wet_bulb[0] = self.t_wet_bulb[0] T_amb[0] = self.t_amb[0] # intial HP status hp_status = True dt_hist = 5. T_limit = hp_tank.T_max for ts in range(self.num_timesteps): # Use upper tank temperature, since this will be tapped and has # to meet T_draw_set T_tank = T_upper_hp_tank[ts] # hp uses temperature hysteresis as on/off control if (((hp_status) and (T_tank < T_limit)) or ((not hp_status) and (T_tank < (T_limit - dt_hist)))): hp_res = converters.heat_pump( T_wet_bulb=self.t_wet_bulb[ts], T_tank=T_tank) # enable heat pump (or keep it on) hp_status = True else: # Set heat pump results to zero hp_res = {'heat_cap': 0., 'el_use': 0., 'cop': 0.} # disable heat pump (or leave it off) hp_status = False # hp heat gain to the tank (updated in # each timestep, recorded later as # output of the hp tank) Q_hp_cap = hp_res['heat_cap'] # electricity use of the heat pump P_hp[ts] = hp_res['el_use'] pv_res = converters.photovoltaic( use_p_peak=False, inc_rad=self.inc_rad[ts]) # Electric power produced by photovoltaic module P_pv_ac[ts] = pv_res['ac'] P_pv_dc[ts] = pv_res['dc'] # Calculate electric power generated by PV used for supplying # the HP P_pv_to_hp[ts] = min(P_hp[ts], P_pv_ac[ts]) # Calculate amount of electricity available for other # applications P_pv_after_hp[ts] = max((P_pv_ac[ts] - P_hp[ts]), 0.) sto_res = hp_tank.thermal_tank( pre_T_amb=self.t_amb[ts], pre_T_feed=self.t_main[ts], pre_T_upper=T_upper_hp_tank[ts], pre_T_lower=T_lower_hp_tank[ts], pre_V_tap=self.load[ts], pre_Q_in=Q_hp_cap, max_V_tap=self.load_max) # demand heat rate Q_dem[ts + 1] = sto_res[self.r['q_dem']] Q_dem_with_dist_loss[ts + 1] = sto_res[self.r['q_dem_tot']] q_dem_balance[ts + 1] = sto_res[self.r['q_dem_balance']] # heat pump tank heat sources Q_hp[ts + 1] = sto_res[self.r['q_del_hp']] Q_ovrcool_hp_tank[ts + 1] = sto_res[self.r['q_ovrcool_tank']] # heat pump tank heat sinks Q_loss_lower_hp_tank[ts + 1] = sto_res[self.r['q_loss_low']] Q_loss_upper_hp_tank[ts + 1] = sto_res[self.r['q_loss_up']] Q_del_hp_tank[ts + 1] = sto_res[self.r['q_del_tank']] Q_unmet_hp_tank[ts + 1] = sto_res[self.r['q_unmet_tank']] Q_dump_hp_tank[ts + 1] = sto_res[self.r['q_dump']] # resulting temperatures in the heat pump tank T_upper_hp_tank[ts + 1] = sto_res[self.r['t_tank_up']] T_lower_hp_tank[ts + 1] = sto_res[self.r['t_tank_low']] # set ambient air and main water temperatures T_main[ts + 1] = self.t_main[ts] T_wet_bulb[ts + 1] = self.t_wet_bulb[ts] T_amb[ts + 1] = self.t_amb[ts] # simulate backup heater and assign household level # electricity consumption if backup == 'electric': backup_proj_total, backup_ts_proj, \ backup_ts_cons, P_pv_after_bckp = \ self._electric_instantaneous_backup( Q_unmet_hp_tank, P_pv_after_hp) else: msg = 'Backup types supported are: \'electric\'. '\ 'Please provide one of the supported types.' log.error(msg) raise ValueError # store timestep results in a dataframe ts_res[self.r['q_dem']] = Q_dem ts_res[self.r['q_del_hp']] = Q_hp ts_res[self.r['q_del_bckp']] = backup_ts_proj[self.r['q_del_bckp']] ts_res[self.r['q_loss_low']] = Q_loss_lower_hp_tank ts_res[self.r['q_loss_up']] = Q_loss_upper_hp_tank ts_res[self.r['q_del_tank']] = Q_del_hp_tank ts_res[self.r['q_unmet_tank']] = Q_unmet_hp_tank ts_res[self.r['q_dump']] = Q_dump_hp_tank ts_res[self.r['q_ovrcool_tank']] = Q_ovrcool_hp_tank ts_res[self.r['t_tank_up']] = T_upper_hp_tank ts_res[self.r['t_tank_low']] = T_lower_hp_tank # PV generated electricity (AC) ts_res[self.r['p_pv_ac']] = P_pv_ac # PV generated electricity (DC) ts_res[self.r['p_pv_dc']] = P_pv_dc # total heat gain (heat pump and electric resistance) ts_res[self.r['q_del']] = Q_hp + backup_ts_proj[self.r['q_del_bckp']] ts_res[self.r['q_unmet']] = backup_ts_proj[self.r['q_unmet']] # Usage of electric power generated by PV ts_res[self.r['p_pv_to_hp']] = P_pv_to_hp ts_res[self.r['p_pv_to_el_res']] = P_pv_after_bckp['used_for_bckp'] # Electricity usage (from grid and PV) # Heat Pump ts_res[self.r['p_hp_el_use']] = P_hp # Electric Resistance ts_res[self.r['p_el_res_use']] = backup_ts_proj[self.r['grs_el_use']] # also include the water and air temperatures in the results ts_res[self.r['t_main']] = T_main ts_res[self.r['t_amb']] = T_amb ts_res[self.r['t_wet_bulb']] = T_wet_bulb # get average temperatures and total heat rates t_columns = [col for col in ts_res.columns if 'Temperature' in col] # all other columns contain timestep heat rate q_columns = [x for x in (set(ts_res.columns) - set(t_columns))] # project results proj_total = ts_res.sum() proj_total[t_columns] = ts_res[t_columns].mean() # household results (annual quantities split according # to individual loads) self.cons_total[self.r['q_dem']] = self.indiv_load_ratios[ self.c['load_m3']].apply( lambda x: (x * Q_dem).sum()) self.cons_total[self.r['q_del']] = (self.indiv_load_ratios[ self.c['load_m3']].apply( lambda x: (x * Q_del_hp_tank).sum()) + self.cons_total[self.r['q_del_bckp']]) self.cons_total[self.r['q_unmet_tank']] = self.indiv_load_ratios[ self.c['load_m3']].apply( lambda x: (x * Q_unmet_hp_tank).sum()) self.cons_total[self.r['q_del_tank']] = self.indiv_load_ratios[ self.c['load_m3']].apply( lambda x: (x * Q_del_hp_tank).sum()) # Calculate pump electricity use if self.community: dist_pump_on_timestep_frac = Q_del_hp_tank + 0. else: # single households do not require # a distribution pump due to the # water main pressure dist_pump_on_timestep_frac = None pump_ts_el_use, pump_el_use, pump_op_hour = \ self._get_pump_energy_use( dist_pump_on_timestep_frac=dist_pump_on_timestep_frac) # if there is any PV power left, use it for the pumps pump_ts_el_use_after_pv = (pump_ts_el_use['total'] - P_pv_after_bckp['rem_after']) pump_ts_el_use_after_pv[pump_ts_el_use_after_pv < 0.] = 0. pump_el_use['after_pv'] = pump_ts_el_use_after_pv.sum() # Distribute pump el. use onto households and add to # total electricity use self.cons_total = self._split_utility( self.cons_total, value=pump_el_use['after_pv'], var='pump_el_use', on=self.c['occ'], drop=False) self.cons_total[self.r['el_use']] += self.cons_total['pump_el_use'] self.cons_total = self.cons_total.drop( columns=['pump_el_use']) # Total electricity use ts_res[self.r['grs_el_use']] = (ts_res[self.r['p_hp_el_use']] + ts_res[self.r['p_el_res_use']] + pump_el_use['total']) # Electricity available after hp, backup heater and pumps P_surplus = P_pv_after_bckp['rem_after'] - pump_ts_el_use['total'] P_surplus[P_surplus < 0.] = 0. ts_res[self.r['p_surplus']] = P_surplus # project level solar fraction (excludes pumping energy) sol_fra = dict() # Calculate the fraction of HP el. use that came from the PV # annual fraction_pv_to_hp = (proj_total[self.r['p_pv_to_hp']] / proj_total[self.r['p_hp_el_use']]) # annual fraction_pv_to_el_res = (proj_total[self.r['p_pv_to_el_res']] / proj_total[self.r['p_el_res_use']]) # for each timestep # Calculate solar fraction between generated PV power and # total electricity use sol_fra['annual'] = ((proj_total[self.r['q_del_hp']] * fraction_pv_to_hp + proj_total[self.r['q_del_bckp']] * fraction_pv_to_el_res) / proj_total[self.r['q_dem']]) # check annual tank balance _in = proj_total[self.r['q_del_hp']] _outs = (proj_total[self.r['q_del_tank']] + proj_total[self.r['q_dump']] + proj_total[self.r['q_loss_up']] + proj_total[self.r['q_loss_low']] - proj_total[self.r['q_ovrcool_tank']]) rel_err = abs(_in - _outs) / _in if rel_err > .01: msg = 'Heat pump tank balance error is {}.' log.warning(msg.format(rel_err)) return self.cons_total, proj_total.round(2), [ proj_total.round(2).to_dict(), sol_fra, pump_el_use, pump_op_hour, ts_res, rel_err]
[docs] def conventional_gas_tank(self): """Basecase conventional gas tank water heater. Make sure to size the tank according to the recommended sizing rules, since the WHAM model does not apply to tanks that are not appropriately sized. Returns: ts_proj: dict of arrays, W Heat: * self.r['q_del']: delivered * self.r['gas_use']: gas consumed """ # components heater = Storage( params=self.sys_params, size=self.sys_sizes, timestep=self.timestep, type='wham_tank', log_level=self.log_level) # indoor air temperature is 291.48 K res = heater.gas_tank_wh( self.load, self.t_main, T_amb=291.48) # household/project level timestep results ts_proj = {self.r['q_del']: res[self.r['q_del']], self.r['gas_use']: res[self.r['gas_use']], self.r['gas_use_no_dist']: res[self.r['gas_use']], self.r['q_dem']: res[self.r['q_dem']]} ts_proj[self.c['season']] = self.season # household/project level analysis period results self.cons_total.at[0, self.r['el_use']] = 0. self.cons_total.at[0, self.r['el_use_s']] = 0. self.cons_total.at[0, self.r['el_use_w']] = 0. self.cons_total.at[0, self.r['gas_use']] = \ res[self.r['gas_use']].sum() * self.timestep self.cons_total.at[0, self.r['gas_use_w']] = ( res[self.r['gas_use']] * self.it_is_winter[1:]).sum() * self.timestep self.cons_total.at[0, self.r['gas_use_s']] = ( res[self.r['gas_use']] * self.it_is_summer[1:]).sum() * self.timestep # these are just placeholders for consistency # the model considers only additional distribution piping # in case of a solar thermal system self.cons_total.at[0, self.r['gas_use_no_dist']] =\ self.cons_total.at[0, self.r['gas_use']] self.cons_total.at[0, self.r['gas_use_s_no_dist']] =\ self.cons_total.at[0, self.r['gas_use_s']] self.cons_total.at[0, self.r['gas_use_w_no_dist']] =\ self.cons_total.at[0, self.r['gas_use_w']] self.cons_total.at[0, self.r['q_del']] =\ res[self.r['q_del']].sum() * self.timestep self.cons_total.at[0, self.r['q_dem']] =\ res[self.r['q_dem']].sum() * self.timestep # zero by WHAM model definition self.cons_total.at[0, self.r['q_unmet']] = 0. self.cons_total.at[0, self.r['q_dump']] = 0. self.cons_total.at[0, self.r['q_dist_loss']] = 0. self.cons_total.at[0, self.r['q_dist_loss_at_bckp']] = 0. self.cons_total.at[0, self.r['q_dist_loss_at_bckp_sum']] = 0. self.cons_total.at[0, self.r['q_dist_loss_at_bckp_win']] = 0. self.cons_total.at[0, self.r['sol_fra']] = 0. self.cons_total.at[0, self.c['max_load']] = UnitConv( self.load_max).m3_gal(unit_in='m3') return self.cons_total, self.cons_total.iloc[0,:], ts_proj
[docs] def simulate(self, type='gas_tank_wh'): """Runs a 8760. hourly simulation of the provided system type. Parameters: type: string * 'gas_tank_wh' * 'solar_thermal_retrofit' (gas tank backup at each household) * 'solar_thermal_new' (gas tankless backup at each household) * 'solar_electric' Returns: en_use: dict Total energy use for the analysis period: * 'gas', Wh * 'electricity', Wh sys_res: list List containing detailed system level output. See dedicated methods for details """ # idealized instantaneous backup (type = see db for system names) if type == 'solar_thermal_new': cons_total, proj_total, sys_res = \ self.solar_thermal(backup='gas') elif type == 'solar_electric': cons_total, proj_total, sys_res = \ self.solar_electric() elif type == 'solar_thermal_retrofit': cons_total, proj_total, sys_res = \ self.solar_thermal(backup='retrofit') elif type == 'gas_tank_wh': cons_total, sys_res = \ self.conventional_gas_tank() proj_total = 0. sol_fra = 0. return cons_total, proj_total, sys_res
def _gas_instantaneous_backup(self, Q_unmet_sol_tank, Q_dist_loss): """Simulates the performance of each individual household backup based on their hot water end-use load profile and the load unmet by the community solar system. Parameters: Q_unmet_sol_tank: array Unmet demand after the solar tank (set load for the backup) Q_dist_loss: array Distribution losses Returns: total_proj: pd df Project level backup energy use and heat rates. ts_proj: dict Timestep project level results for energy use (gas consumed) and heat rates (delivered, unmet) ts_cons: dict of dicts Timestep household level results for energy uses [W], and heat rates [W]. Updates `self.cons_total` with the backup simulation energy use and heat rates results. """ # project level backup delivered, gas use and unmet ts_proj = {self.r['q_del_bckp']: np.zeros(self.num_timesteps + 1), self.r['gas_use']: np.zeros(self.num_timesteps + 1), self.r['gas_use_no_dist']: np.zeros( self.num_timesteps + 1), self.r['q_unmet']: np.zeros(self.num_timesteps + 1)} ts_cons = dict() # Distribution losses when the backup is on. The # actual loss covered by the backup is distributed over # individual households in the loop below Q_dist_loss_when_backup_on = (Q_dist_loss * (Q_unmet_sol_tank > 0.)) Q_unmet_without_dist_loss = (Q_unmet_sol_tank - Q_dist_loss_when_backup_on) # distribution loss may be larger than # the backup demand (this occurs when a part of # the loss got covered by the solar source), # so impose zero as lower limit Q_unmet_without_dist_loss[ Q_unmet_without_dist_loss < 0.] = 0. for cons_id in self.cons_total[self.c['id']].unique(): # get the fraction of the remaining load # pertaining to a single household, which should # be met by that household's backup Q_unmet_sol_tank_ind_cons = (Q_unmet_sol_tank * self.indiv_load_ratios.loc[ self.indiv_load_ratios[ self.c['id']] == cons_id, self.c['load_m3']].reset_index(drop=True)[0]) Q_unmet_sol_tank_ind_cons_no_dist_loss = ( Q_unmet_without_dist_loss * self.indiv_load_ratios.loc[ self.indiv_load_ratios[self.c['id']] == cons_id, self.c['load_m3']].reset_index(drop=True)[0]) size = self.backup_sizes.loc[ self.backup_sizes[self.c['id']] == cons_id, [self.s['comp'], self.s['cap']]] ind_backup = Converter( params=self.backup_params, weather=self.weather, sizes=size, log_level=self.log_level) ind_res = ind_backup.gas_burner( Q_unmet_sol_tank_ind_cons) ind_res_no_dist_loss = ind_backup.gas_burner( Q_unmet_sol_tank_ind_cons_no_dist_loss) for key in ind_res: ts_proj[key] += ind_res[key] ts_proj[self.r['gas_use_no_dist']] += ( ind_res_no_dist_loss[self.r['gas_use']]) ts_cons[cons_id] = { self.r['q_del_bckp']: ind_res[self.r['q_del_bckp']], self.r['gas_use']: ind_res[self.r['gas_use']], self.r['gas_use_s']: (ind_res[self.r['gas_use']] * self.it_is_summer), self.r['gas_use_w']: (ind_res[self.r['gas_use']] * self.it_is_winter), self.r['gas_use_no_dist']: ind_res_no_dist_loss[ self.r['gas_use']], self.r['gas_use_s_no_dist']: ind_res_no_dist_loss[ self.r['gas_use']] * self.it_is_summer, self.r['gas_use_w_no_dist']: ind_res_no_dist_loss[ self.r['gas_use']] * self.it_is_winter, self.r['q_unmet']: ind_res[self.r['q_unmet']]} ts_cons[cons_id][self.c['season']] = self.season self.cons_total.at[ self.cons_total[self.c['id']] == cons_id, self.r['gas_use']] = ind_res[ self.r['gas_use']].sum() * self.timestep self.cons_total.at[ self.cons_total[self.c['id']] == cons_id, self.r['gas_use_s']] = ( ind_res[self.r['gas_use']] * self.it_is_summer).sum() * self.timestep self.cons_total.at[ self.cons_total[self.c['id']] == cons_id, self.r['gas_use_w']] = ( ind_res[self.r['gas_use']] * self.it_is_winter).sum() * self.timestep self.cons_total.at[ self.cons_total[self.c['id']] == cons_id, self.r['gas_use_no_dist']] = ind_res_no_dist_loss[ self.r['gas_use']].sum() * self.timestep self.cons_total.at[ self.cons_total[self.c['id']] == cons_id, self.r['gas_use_s_no_dist']] = ( ind_res_no_dist_loss[self.r['gas_use']] * self.it_is_summer).sum() * self.timestep self.cons_total.at[ self.cons_total[self.c['id']] == cons_id, self.r['gas_use_w_no_dist']] = ( ind_res_no_dist_loss[self.r['gas_use']] * self.it_is_winter).sum() * self.timestep self.cons_total.at[ self.cons_total[self.c['id']] == cons_id, self.r['q_del_bckp']] = ind_res[ self.r['q_del_bckp']].sum() * self.timestep self.cons_total.at[\ self.cons_total[self.c['id']] == cons_id, \ self.r['q_unmet']] = \ ind_res[self.r['q_unmet']].sum() * self.timestep ts_proj[self.r['gas_use_s']] = ( ts_proj[self.r['gas_use']] * self.it_is_summer) ts_proj[self.r['gas_use_w']] = ( ts_proj[self.r['gas_use']] * self.it_is_winter) ts_proj[self.r['gas_use_s_no_dist']] = ( ts_proj[self.r['gas_use_no_dist']] * self.it_is_summer) ts_proj[self.r['gas_use_w_no_dist']] = ( ts_proj[self.r['gas_use_no_dist']] * self.it_is_winter) total_proj = pd.DataFrame.from_dict(ts_proj).sum().to_dict() ts_proj[self.c['season']] = self.season return total_proj, ts_proj, ts_cons def _electric_instantaneous_backup(self, Q_unmet_tank, P_pv): """Simulates the performance of each individual household backup based on their hot water end-use load profile and the load unmet by the community solar system. Parameters: Q_unmet_tank: array Unmet demand after the solar tank (set load for the backup) P_pv: array PV power available for the backup application Set to None if there is no PV in the system or if PV generated power should not be used to power the electric heater backup total_proj: pd df Project level backup energy use and heat rates. ts_proj: dict Timestep project level results for energy use [W], and delivered heat rate [W]. ts_cons: dict of dicts Timestep household level results for energy use [W], and delivered heat rate [W]. Updates self.cons_total with the backup simulation energy use and heat rates results. """ if P_pv is None: P_pv = np.zeros(self.num_timesteps + 1) # project level backup delivered, gas use and unmet ts_proj = {self.r['q_del_bckp']: np.zeros(self.num_timesteps + 1), self.r['el_use']: np.zeros(self.num_timesteps + 1), self.r['grs_el_use']: np.zeros(self.num_timesteps + 1), self.r['q_unmet']: np.zeros(self.num_timesteps + 1)} ts_cons = dict() for cons_id in self.cons_total[self.c['id']].unique(): # get the fraction of the remaining load # pertaining to a single household, which should # be met by that household's backup Q_unmet_tank_ind_cons = (Q_unmet_tank * self.indiv_load_ratios.loc[ self.indiv_load_ratios[self.c['id']] == cons_id, self.c['load_m3']].reset_index(drop=True)[0]) # fraction of the PV generated power that can be allocated # to consumer cons_id P_pv_ind_cons = (P_pv * self.indiv_load_ratios.loc[ self.indiv_load_ratios[self.c['id']] == cons_id, self.c['load_m3']].reset_index(drop=True)[0]) size = self.backup_sizes.loc[ self.backup_sizes[self.c['id']] == cons_id, [self.s['comp'], self.s['cap']]] ind_backup = Converter( params=self.backup_params, weather=self.weather, sizes=size) ind_res = ind_backup.electric_resistance( Q_unmet_tank_ind_cons) ind_res[self.r['grs_el_use']] = ( ind_res[self.r['el_use']] * 1.) # power from PV if possible ind_res[self.r['el_use']] = ( ind_res[self.r['grs_el_use']] - P_pv_ind_cons) ind_res[self.r['el_use']][ ind_res[self.r['el_use']] < 0.] = 0. for key in ind_res: ts_proj[key] += ind_res[key] ts_cons[cons_id] = { self.r['q_del_bckp']: ind_res[self.r['q_del_bckp']], self.r['el_use']: ind_res[self.r['el_use']], self.r['q_unmet']: ind_res[self.r['q_unmet']]} self.cons_total.at[ self.cons_total[self.c['id']] == cons_id, self.r['el_use']] = ind_res[ self.r['el_use']].sum() * self.timestep self.cons_total.at[ self.cons_total[self.c['id']] == cons_id, self.r['q_del_bckp']] = ind_res[ self.r['q_del_bckp']].sum() * self.timestep self.cons_total.at[ self.cons_total[self.c['id']] == cons_id, self.r['q_unmet']] = ind_res[ self.r['q_unmet']].sum() * self.timestep # PV power accounting P_pv_bckp = {} P_pv_bckp['rem_after'] = P_pv - ts_proj[self.r['grs_el_use']] P_pv_bckp['rem_after'][P_pv_bckp['rem_after'] < 0.] = 0. P_pv_bckp['used_for_bckp'] = P_pv - P_pv_bckp['rem_after'] total_proj = pd.DataFrame.from_dict(ts_proj).sum().to_dict() return total_proj, ts_proj, ts_cons, P_pv_bckp def _gas_tank_backup(self, T_feed, T_feed_no_loss): """Backup gas tank water heater. Retrofit systems retain the original one. Currently we assume that the outdoor air temperature rarely drives the tank temperature below the water main. If the model is to be applied for colder climates, and the tanks are to be installed outdoors, a valve which switches the backup feed to main in case the feed from the tank is of a lower temperature should be implemented. Parameters: T_feed: array like, K Backup water heater inlet temperature T_feed_no_loss: array like, K Backup water heater inlet temperature assuming no temperature drop in the distribution system (adiabatic pipe) Returns: total_proj: pd df Project level backup energy use and heat rates. ts_proj: dict Timestep project level results for energy use [W], and delivered heat rate [W]. ts_cons: dict of dicts Timestep household level results for energy use [W], and delivered heat rate [W]. Updates `self.cons_total` with the backup simulation energy use and heat rates results. """ ts_proj = {self.r['q_del_bckp']: np.zeros(self.num_timesteps + 1), self.r['gas_use']: np.zeros(self.num_timesteps + 1), self.r['gas_use_no_dist']: np.zeros( self.num_timesteps + 1), self.r['q_unmet']: np.zeros(self.num_timesteps + 1)} ts_proj[self.c['season']] = self.season ts_cons = dict() for cons_id in self.cons_total[self.c['id']].unique(): # extract backup size for the household size = self.backup_sizes.loc[ self.backup_sizes[self.c['id']] == cons_id, self.s['cap']].values[0] backup_heater = Storage( params=self.backup_params, size=size, timestep=self.timestep, type='wham_tank', log_level=self.log_level) # assuming the tank is indoors and using default # tank surrounding temperature ind_load = np.append(0, self.loads.loc[self.loads[self.c['id']] == cons_id, self.c['load_m3']].values[0]) ind_res = backup_heater.gas_tank_wh( ind_load, T_feed, T_amb=291.48) ind_res_no_dist_loss = backup_heater.gas_tank_wh( ind_load, T_feed_no_loss, T_amb=291.48) # rename key ind_res[self.r['q_del_bckp']] = ind_res.pop(self.r['q_del']) # placeholder unmet load, not implemented in the comp. model ind_res[self.r['q_unmet']] = np.zeros(self.num_timesteps + 1) for key in ([self.r['gas_use'], self.r['q_del_bckp'], self.r['q_unmet']]): ts_proj[key] += ind_res[key] ts_proj[self.r['gas_use_no_dist']] += ( ind_res_no_dist_loss[self.r['gas_use']]) ts_cons[cons_id] = { self.r['q_del_bckp']: ind_res[self.r['q_del_bckp']], self.r['gas_use']: ind_res[self.r['gas_use']], self.r['gas_use_s']: ind_res[ self.r['gas_use']] * self.it_is_summer, self.r['gas_use_w']: ind_res[ self.r['gas_use']] * self.it_is_winter, self.r['gas_use_no_dist']: ind_res_no_dist_loss[ self.r['gas_use']], self.r['gas_use_s_no_dist']: ind_res_no_dist_loss[ self.r['gas_use']] * self.it_is_summer, self.r['gas_use_w_no_dist']: ind_res_no_dist_loss[ self.r['gas_use']] * self.it_is_winter} ts_cons[cons_id][self.c['season']] = self.season self.cons_total.at[ self.cons_total[self.c['id']] == cons_id, self.r['gas_use']] = ind_res[self.r['gas_use']].sum() self.cons_total.at[ self.cons_total[self.c['id']] == cons_id, self.r['gas_use_s']] = ( ind_res[self.r['gas_use']] * self.it_is_summer).sum() * self.timestep self.cons_total.at[ self.cons_total[self.c['id']] == cons_id, self.r['gas_use_w']] = ( ind_res[self.r['gas_use']] * self.it_is_winter).sum() * self.timestep self.cons_total.at[ self.cons_total[self.c['id']] == cons_id, self.r['gas_use_no_dist']] = ind_res_no_dist_loss[ self.r['gas_use']].sum() * self.timestep self.cons_total.at[ self.cons_total[self.c['id']] == cons_id, self.r['gas_use_s_no_dist']] = ( ind_res_no_dist_loss[self.r['gas_use']] * self.it_is_summer).sum() * self.timestep self.cons_total.at[ self.cons_total[self.c['id']] == cons_id, self.r['gas_use_w_no_dist']] = ( ind_res_no_dist_loss[self.r['gas_use']] * self.it_is_winter).sum() * self.timestep self.cons_total.at[ self.cons_total[self.c['id']] == cons_id, self.r['q_del_bckp']] = ind_res[ self.r['q_del_bckp']].sum() self.cons_total.at[ self.cons_total[self.c['id']] == cons_id, self.r['q_unmet']] = ind_res[ self.r['q_unmet']].sum() * self.timestep total_proj = pd.DataFrame.from_dict(ts_proj).sum().to_dict() ts_proj[self.r['gas_use_s']] = ( ts_proj[self.r['gas_use']] * self.it_is_summer) ts_proj[self.r['gas_use_w']] = ( ts_proj[self.r['gas_use']] * self.it_is_winter) ts_proj[self.r['gas_use_s_no_dist']] = ( ts_proj[self.r['gas_use_no_dist']] * self.it_is_summer) ts_proj[self.r['gas_use_w_no_dist']] = ( ts_proj[self.r['gas_use_no_dist']] * self.it_is_winter) return total_proj, ts_proj, ts_cons @staticmethod def _split_utility(df, value=1., var=None, on=None, drop=True): """Splits community level costs or energy use between the households based on a size defining variable, such as occupancy. Parameters: df: pd df Contains household ID and split on column value: float Value to split among households var: str Label to store splitted values on: str Label for the column with weights (e.g. occupancy) drop: boolean Drops column used for calculating weights ('on' kwarg) """ df[var] = df[on] * value / df[on].sum() if drop: df = df.drop(on, axis=1) # return with a value distributed onto households return df def _get_pump_energy_use(self, dist_pump_on_timestep_frac=None, gain_from_collector=None): """Calculates pump energy use based on the load on the pump. We assume fixed_speed pumps and full load at each timestep when there is flow demand. Parameters: dist_pump_on_timestep_frac: array, W Array with the fraction of distribution pump on time for each timestep gain_from_collector: array, W Array with load delivered from collector. Determines the operating hours for the solar pump. Assumption is that for each timestep in which the load exists, the pump is on for the full duration of the timestep Returns: el_use: dict, Wh Electricity consumption for the analyzed period by: * 'dist_pump' - distribution pump * 'sol_pump' - solar pump * 'total' - total (all pumps in the system) op_hour: dict, h Operating hours for the analyzed period by: * 'dist_pump' - distribution pump * 'sol_pump' - solar pump """ ts_el_use = dict() ts_el_use['total'] = np.zeros(self.num_timesteps + 1) el_use = dict() el_use['total'] = 0. op_hour = dict() pump = Distribution( params=self.sys_params, sizes=self.sys_sizes, log_level=self.log_level) if dist_pump_on_timestep_frac is not None: # get annual on hours op_hour['dist_pump'] = (dist_pump_on_timestep_frac.sum() * self.timestep) ts_el_use['dist_pump'], el_use['dist_pump'] = pump.pump( on_array=dist_pump_on_timestep_frac, role='distribution') ts_el_use['total'] += ts_el_use['dist_pump'] el_use['total'] += el_use['dist_pump'] if gain_from_collector is not None: # get annual on hours on_array = 1. * (gain_from_collector > 0) op_hour['sol_pump'] = ((gain_from_collector > 0).sum() * self.timestep) ts_el_use['sol_pump'], el_use['sol_pump'] = pump.pump( on_array=on_array, role='solar') ts_el_use['total'] += ts_el_use['sol_pump'] el_use['total'] += el_use['sol_pump'] return ts_el_use, el_use, op_hour