Skip to content

Commit

Permalink
Model updates
Browse files Browse the repository at this point in the history
  • Loading branch information
noahprime committed Jun 6, 2024
1 parent ba3e76e commit 8783d1d
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 56 deletions.
144 changes: 95 additions & 49 deletions bed/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
"""

from numpy import maximum, log, exp, isnan
from numpy import log, exp, isnan, nansum
from xarray import align

from bed.read_config import read_config
from bed.read_data import Data
from bed.diagnostics import diagnostics
Expand All @@ -20,29 +22,61 @@ class Bed:

def __init__(self, config_file='', run_diagnostics=True):

# Read data
# Read spatial data + config
# Data object contains:
# - Temperature
# - Surface to Floor space ratio
self.data = Data(config_file)

# User defined base year demand
self.base_year_heating_demand = float(self.data.config['base_year_heating_demand'])
self.base_year_cooling_demand = float(self.data.config['base_year_cooling_demand'])
self.base_year_satiation_factor = float(self.data.config['base_year_satiation_factor'])

# Service Prices
self.base_year_service_prices = {
'H': self.data.config['base_year_heating_price'],
'C': self.data.config['base_year_cooling_price']
}
self.service_prices = {
'H': self.data.config['heating_price'],
'C': self.data.config['cooling_price']
}

# Internal gain
self.total_internal_gain = float(self.data.config['total_internal_gain'])
self.base_year_total_internal_gain = float(self.data.config['base_year_total_internal_gain'])

# Thermal Conductance / U-Factor improvement rate
self.thermal_conductance = self.data.config['thermal_conductance']
self.base_year_thermal_conductance = self.data.config['base_year_thermal_conductance']

# Calculate degree hours
self.degree_hours = self.temperature_to_degree_hours(temperature_unit='K',
comfortable_temperature=291.483)
self.degree_hours = self.temperature_to_degree_hours(
self.data.temperature[self.data.config['temperature_variable_name']],
temperature_unit=self.data.config['temperature_units'],
comfortable_temperature=self.data.config['comfortable_temperature']
)
self.base_year_degree_hours = self.temperature_to_degree_hours(
self.data.base_year_temperature[self.data.config['temperature_variable_name']],
temperature_unit=self.data.config['temperature_units'],
comfortable_temperature=self.data.config['comfortable_temperature']
)

# Calculate building energy demand
self.demand_heat = self.demand(thermal_conductance=1,
surface_to_floor_ratio=self.data.surface_to_floor_area_ratio,
internal_gain=1,
income_per_capita=1,
service_price={'H': 1, 'C': 1})
self.demand = self.get_demand(income_per_capita=1)

# diagnostics
if run_diagnostics:
diagnostics(self.data)
diagnostics(self.data, self.degree_hours, self.demand, {
'area': self.data.building_area,
'height': self.data.building_height,
'floor': self.data.floor_space,
's2far': self.data.surface_to_floor_area_ratio
})
...

def temperature_to_degree_hours(self, temperature_unit='F', comfortable_temperature=65):
def temperature_to_degree_hours(self, temperature, temperature_unit='F', comfortable_temperature=65):
"""
Calculating heating and cooling degree hours (days?)
:param temperature_unit: String for temperature unit
Expand All @@ -54,40 +88,36 @@ def temperature_to_degree_hours(self, temperature_unit='F', comfortable_temperat

# For HDD
# Get difference in temp and threshold, and flip so that colder values are positive
hdd_data = -1 * (self.data.temperature - comfortable_temperature)
hdd_data = -1 * (temperature - comfortable_temperature)
# Keep only values above axis
hdd_data = hdd_data.where(hdd_data > 0, 0)
# Integrate using trapezoidal method
hdd_data = hdd_data.integrate('time', datetime_unit='h')
# Integration puts 0 in place of nan, so reinstate the mask from temp data
hdd_data = hdd_data.where(~isnan(self.data.temperature.isel(time=0)))
hdd_data = hdd_data.where(~isnan(temperature.isel(time=0))).drop_vars('time')

# For HDD
# Get difference in temp and threshold
cdd_data = (self.data.temperature - comfortable_temperature)
cdd_data = (temperature - comfortable_temperature)
# Keep only values above axis
cdd_data = cdd_data.where(cdd_data > 0, 0)
# Integrate using trapezoidal method
cdd_data = cdd_data.integrate('time', datetime_unit='h')
# Integration puts 0 in place of nan, so reinstate the mask from temp data
cdd_data = cdd_data.where(~isnan(self.data.temperature.isel(time=0)))
cdd_data = cdd_data.where(~isnan(temperature.isel(time=0))).drop_vars('time')

# Total number of hours per grid cell in a year beyond threshold
degree_hours = {
'hdd': hdd_data,
'cdd': cdd_data
'hdd': hdd_data.transpose('y', 'x'),
'cdd': cdd_data.transpose('y', 'x')
}

logging.info('Function temperature_to_degree_hours completed.')

return degree_hours

def demand(self,
thermal_conductance=1,
surface_to_floor_ratio=1,
internal_gain=1,
income_per_capita=1,
service_price={'H': 1, 'C': 1}):
def get_demand(self,
income_per_capita=1):
"""
:param degree_hours: Array for degree hours (Hours)
:param thermal_conductance: Float for thermal conductance (GJ/m2 hour C)
Expand All @@ -100,47 +130,62 @@ def demand(self,

logging.info('Starting function demand.')

technical_component_h = self.degree_hours['hdd'] * thermal_conductance * surface_to_floor_ratio - internal_gain
technical_component_c = self.degree_hours['cdd'] * thermal_conductance * surface_to_floor_ratio + internal_gain
# Convert total internal gain to internal gain per unit floor-space
internal_gain = self.total_internal_gain / self.data.total_floor_space
base_year_internal_gain = self.base_year_total_internal_gain / self.data.base_year_total_floor_space

# Convert total demand to demand per unit floor-space
# Align, removes floating point errors of lat/lon alignment
self.data.floor_space, self.base_year_degree_hours['hdd'], self.base_year_degree_hours['cdd'] = align(self.data.floor_space, self.base_year_degree_hours['hdd'], self.base_year_degree_hours['cdd'], join='override')
fs_times_hdd = self.data.floor_space * self.base_year_degree_hours['hdd']
fs_times_cdd = self.data.floor_space * self.base_year_degree_hours['cdd']
self.base_year_heating_demand = self.base_year_heating_demand * fs_times_hdd / nansum(fs_times_hdd.data)
self.base_year_cooling_demand = self.base_year_cooling_demand * fs_times_cdd / nansum(fs_times_cdd.data)

# Evaluating component of demand equation
self.degree_hours['hdd'], self.degree_hours['cdd'], self.data.surface_to_floor_area_ratio = align(self.degree_hours['hdd'], self.degree_hours['cdd'], self.data.surface_to_floor_area_ratio, join = 'override')
self.base_year_degree_hours['hdd'], self.base_year_degree_hours['cdd'], self.data.base_year_surface_to_floor_area_ratio = align(self.degree_hours['hdd'], self.degree_hours['cdd'], self.data.surface_to_floor_area_ratio, join = 'override')
technical_component_h = self.degree_hours['hdd'] * self.thermal_conductance * self.data.surface_to_floor_area_ratio - internal_gain
technical_component_c = self.degree_hours['cdd'] * self.thermal_conductance * self.data.surface_to_floor_area_ratio + internal_gain
base_year_technical_component_h = self.base_year_degree_hours['hdd'] * self.base_year_thermal_conductance * self.data.base_year_surface_to_floor_area_ratio - base_year_internal_gain
base_year_technical_component_c = self.base_year_degree_hours['cdd'] * self.base_year_thermal_conductance * self.data.base_year_surface_to_floor_area_ratio + base_year_internal_gain

# Calibrate k:
# Set income to infinity, and let dh/dc now represent level of satiated demand -
# (this is where they use USA as a benchmark and calc other regions from Eqs S1 and S2)
k_h = self.k_calibration(usa_base_year_demand=1,
usa_degree_hours=1,
base_year_demand=1,
technical_component=technical_component_h,
degree_type='hdd')
k_c = self.k_calibration(usa_base_year_demand=1,
usa_degree_hours=1,
base_year_demand=1,
technical_component=technical_component_c,
degree_type='cdd')
k_h = self.k_calibration(base_year_technical_component_h, self.base_year_heating_demand)
k_c = self.k_calibration(base_year_technical_component_c, self.base_year_cooling_demand)

# Calibrate mu:
# Need base year demand so that we can solve for mu
mu_h = self.mu_calibration(base_year_demand=1,
mu_h = self.mu_calibration(demand=self.base_year_heating_demand,
technical_component=technical_component_h,
calibration_coefficient=k_h,
prices=service_price['H'],
income_per_capita=income_per_capita)
mu_c = self.mu_calibration(base_year_demand=1,
prices=self.base_year_service_prices['H'],
income_per_capita=self.data.base_year_income_per_capita)
mu_c = self.mu_calibration(demand=self.base_year_cooling_demand,
technical_component=technical_component_c,
calibration_coefficient=k_c,
prices=service_price['C'],
income_per_capita=income_per_capita)
prices=self.base_year_service_prices['C'],
income_per_capita=self.data.base_year_income_per_capita)

# Calculate gridded demand
economic_component_h = 1 - exp(- (log(2) * income_per_capita) / (mu_h * service_price['H']) )
economic_component_c = 1 - exp(- (log(2) * income_per_capita) / (mu_c * service_price['C']) )
economic_component_h = 1 - exp(- (log(2) * self.data.income_per_capita) / (mu_h * self.service_prices['H']) )
economic_component_c = 1 - exp(- (log(2) * self.data.income_per_capita) / (mu_c * self.service_prices['C']) )
print(f'size of economic_component_h: {economic_component_h.sizes}\nsize of k_h: {k_h.sizes}\nsize of technical_component_h: {technical_component_h.sizes}')
k_h, technical_component_h, economic_component_h, self.degree_hours['hdd'] = align(k_h, technical_component_h, economic_component_h, self.degree_hours['hdd'], join = 'override')
k_c, technical_component_c, economic_component_c, self.degree_hours['cdd'] = align(k_c, technical_component_c, economic_component_c, self.degree_hours['cdd'], join = 'override')
demand_h = k_h * technical_component_h * economic_component_h
demand_c = k_c * technical_component_c * economic_component_c

logging.info('Function demand completed.')

return {'demand_h': demand_h, 'demand_c': demand_c}
return {
'demand_h': demand_h.fillna(0).where(~isnan(self.degree_hours['hdd'])),
'demand_c': demand_c.fillna(0).where(~isnan(self.degree_hours['hdd']))
}

def k_calibration(self, usa_base_year_demand, usa_degree_hours, base_year_demand, technical_component, degree_type='hdd'):
def k_calibration(self, technical_component, demand):
"""
Calibrating unit-less k term
:param usa_base_year_demand: Float demand in USA for given year
Expand All @@ -150,16 +195,16 @@ def k_calibration(self, usa_base_year_demand, usa_degree_hours, base_year_demand
:return: Float array of calibration coefficients
"""
# Satiation level assumed to be max of observed base-year demand, and satiation level in USA modified using ratio of degree hours
satiation_level = (self.degree_hours[degree_type] / usa_degree_hours) * usa_base_year_demand * 1.1
satiation_level = maximum(satiation_level, base_year_demand)
satiation_level = demand * self.base_year_satiation_factor

# Solve for k
satiation_level, technical_component = align(satiation_level, technical_component, join = 'override')
k = satiation_level / technical_component

return k

def mu_calibration(self,
base_year_demand,
demand,
technical_component,
calibration_coefficient,
prices,
Expand All @@ -174,8 +219,9 @@ def mu_calibration(self,
:return: Float array of calibration coefficients "mu"
"""
# Breaking up mu calculation into multiple terms
demand, technical_component, calibration_coefficient = align(demand, technical_component, calibration_coefficient, join = 'override')
term_one = calibration_coefficient * technical_component
term_two = prices * log(1-base_year_demand/term_one)
term_two = prices * log(1-demand/term_one)
mu = -log(2) * income_per_capita / term_two

return mu
Expand Down
37 changes: 31 additions & 6 deletions bed/read_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,22 @@ def __init__(self, config_file=''):
self.temperature = self.read_spatial_data('temperature_data')
self.building_area = self.read_spatial_data('building_area_data')
self.building_height = self.read_spatial_data('building_height_data')
self.income_per_capita = self.read_spatial_data('income_per_capita')
self.income_per_capita = self.income_per_capita['income']

# Read spatial datasets (base year)
self.base_year_temperature = self.read_spatial_data('base_year_temperature_data')
self.base_year_building_area = self.read_spatial_data('base_year_building_area_data')
self.base_year_building_height = self.read_spatial_data('base_year_building_height_data')
self.base_year_income_per_capita = self.read_spatial_data('base_year_income_per_capita')
self.base_year_income_per_capita = self.base_year_income_per_capita['income']

# Align, removes floating point errors of lat/lon alignment
self.temperature, self.building_area, self.building_height = xr.align(self.temperature, self.building_area, self.building_height, join='override')
self.base_year_temperature, self.base_year_building_area, self.base_year_building_height = xr.align(self.base_year_temperature, self.base_year_building_area, self.base_year_building_height, join='override')

# Surface to floor ratio
self.surface_to_floor_area_ratio = self.get_surface_to_floor_area_ratio()
self.surface_to_floor_area_ratio, self.base_year_surface_to_floor_area_ratio = self.get_surface_to_floor_area_ratio()


logging.info('Class Data inside module read_data completed.')
Expand All @@ -68,19 +81,31 @@ def get_surface_to_floor_area_ratio(self):
"""
logging.info('Calculating surface area to floor space ratio')

# Formatting
self.building_area = self.building_area['band_data'].squeeze('band').drop_vars(['band', 'spatial_ref']).transpose('y', 'x')
self.building_height = self.building_height['band_data'].squeeze('band').drop_vars(['band', 'spatial_ref']).transpose('y', 'x')
self.base_year_building_area = self.base_year_building_area['band_data'].squeeze('band').drop_vars(['band', 'spatial_ref']).transpose('y', 'x')
self.base_year_building_height = self.base_year_building_height['band_data'].squeeze('band').drop_vars(['band', 'spatial_ref']).transpose('y', 'x')

# Floor space
self.floor_space = self.building_area * self.building_height / 3
self.base_year_floor_space = self.base_year_building_area * self.base_year_building_height / 3
self.total_floor_space = np.nansum(self.floor_space.data)
self.base_year_total_floor_space = np.nansum(self.base_year_floor_space.data)

# Length and Width assuming square building
lw = np.sqrt(self.building_area)
base_year_lw = np.sqrt(self.base_year_building_area)

# Surface area, 4 sides + roof
surface_area = (4 * (lw * self.building_height)) + (lw * lw)

# Floor space, building footprint area * number of floors assuming 3 meters per floor
floor_space = self.building_area * self.building_height / 3
base_year_surface_area = (4 * (base_year_lw * self.base_year_building_height)) + (base_year_lw * base_year_lw)

# Surface to floor area ratio
s2far = surface_area / floor_space
s2far = surface_area / self.floor_space
base_year_s2far = base_year_surface_area / self.base_year_floor_space

return s2far
return s2far, base_year_s2far

def read_spatial_data(self, key):
"""
Expand Down
26 changes: 25 additions & 1 deletion downloaded_data/examples/example_config.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
dir_outputs: "outputs"

# Current Year
temperature_data: "era5_tas_2018.nc"
building_area_data: "GHS_BUILT_S_E2018.tif"
building_height_data: "GHS_BUILT_H_ANBH_E2018.tif"
path_temperature_ncdf: "climate/wrfout_d03_2014-08-09_Morocco_2014.nc"
total_internal_gain: 8.26e7 # units - GJ
thermal_conductance: 1.48422 # Improvement rate of thermal conductance. Ex. assuming to improve by 0.3% a year from 2012-2018
heating_price: 1
cooling_price: 1
income_per_capita: 'income_2021.nc'

# Base Year
base_year_temperature_data: "era5_tas_2018.nc" # "era5_tas_2012.nc"
base_year_building_area_data: "GHS_BUILT_S_E2018.tif"
base_year_building_height_data: "GHS_BUILT_H_ANBH_E2018.tif"
base_year_heating_demand: 4.58e6 # Total residential space heating energy demand in Morocoo 2012 (IEA)
base_year_cooling_demand: 2.6e5 # Total residential space cooling energy demand in Morocoo 2012 (IEA)
base_year_satiation_factor: 1.1 # Assumed factor to apply to demand to increase to theoretical satiation level. Leave as 1 if you think demand was met
base_year_thermal_conductance: 1.511219 # Assuming user can provide reasonable estimate for this in the base year (indexed to US 2005 value?)
base_year_total_internal_gain: 6.86e7 # units - GJ
base_year_heating_price: 1
base_year_cooling_price: 1
base_year_income_per_capita: 'income_2021.nc'

# Parameters
temperature_variable_name: 't2m'
temperature_units: 'K'
comfortable_temperature: 291.483

0 comments on commit 8783d1d

Please sign in to comment.