From dce5bb8c06d8bc072c9d99e8958b2e5873f4ad66 Mon Sep 17 00:00:00 2001 From: s_ranjbar Date: Fri, 17 May 2024 09:52:42 -0400 Subject: [PATCH 01/15] fix: bugs in costing solved fix: SRA is fixed and finalized feat: A code is written to calculate solar angles and values are validated using PVLIB feat: solar radiation on tilted surface is calculated fix: Required attributes for PV calculations are added to CDM and surface class --- .../building_demand/surface.py | 17 ++ hub/city_model_structure/city_object.py | 29 +++- .../energy_systems/pv_generation_system.py | 36 +++++ .../formats/simplified_radiosity_algorithm.py | 2 +- ...ntreal_future_energy_systems_parameters.py | 4 +- .../results/simplified_radiosity_algorithm.py | 2 +- hub/imports/weather/epw_weather_parameters.py | 8 +- main.py | 9 +- scripts/pv_sizing_and_simulation.py | 21 +++ scripts/radiation_tilted.py | 109 +++++++++++++ scripts/solar_angles.py | 146 ++++++++++++++++++ tests/test_construction_factory.py | 2 +- tests/test_geometry_factory.py | 2 +- tests/test_usage_factory.py | 2 +- 14 files changed, 370 insertions(+), 19 deletions(-) create mode 100644 scripts/pv_sizing_and_simulation.py create mode 100644 scripts/radiation_tilted.py create mode 100644 scripts/solar_angles.py diff --git a/hub/city_model_structure/building_demand/surface.py b/hub/city_model_structure/building_demand/surface.py index 2cd42755..f54d21e1 100644 --- a/hub/city_model_structure/building_demand/surface.py +++ b/hub/city_model_structure/building_demand/surface.py @@ -46,6 +46,7 @@ class Surface: self._vegetation = None self._percentage_shared = None self._solar_collectors_area_reduction_factor = None + self._global_irradiance_tilted = {} @property def name(self): @@ -384,3 +385,19 @@ class Surface: :param value: float """ self._solar_collectors_area_reduction_factor = value + + @property + def global_irradiance_tilted(self) -> dict: + """ + Get global irradiance on a tilted surface in J/m2 + :return: dict + """ + return self._global_irradiance_tilted + + @global_irradiance_tilted.setter + def global_irradiance_tilted(self, value): + """ + Set global irradiance on a tilted surface in J/m2 + :param value: dict + """ + self._global_irradiance_tilted = value diff --git a/hub/city_model_structure/city_object.py b/hub/city_model_structure/city_object.py index c18a4aa0..0d281b65 100644 --- a/hub/city_model_structure/city_object.py +++ b/hub/city_model_structure/city_object.py @@ -41,9 +41,10 @@ class CityObject: self._ground_temperature = {} self._global_horizontal = {} self._diffuse = {} - self._beam = {} + self._direct_normal = {} self._sensors = [] self._neighbours = None + self._beam = {} @property def level_of_detail(self) -> LevelOfDetail: @@ -238,20 +239,20 @@ class CityObject: self._diffuse = value @property - def beam(self) -> dict: + def direct_normal(self) -> dict: """ Get beam radiation surrounding the city object in J/m2 :return: dict{dict{[float]}} """ - return self._beam + return self._direct_normal - @beam.setter - def beam(self, value): + @direct_normal.setter + def direct_normal(self, value): """ Set beam radiation surrounding the city object in J/m2 :param value: dict{dict{[float]}} """ - self._beam = value + self._direct_normal = value @property def lower_corner(self): @@ -302,3 +303,19 @@ class CityObject: Set the list of neighbour_objects and their properties associated to the current city_object """ self._neighbours = value + + @property + def beam(self) -> dict: + """ + Get beam radiation surrounding the city object in J/m2 + :return: dict{dict{[float]}} + """ + return self._beam + + @beam.setter + def beam(self, value): + """ + Set beam radiation surrounding the city object in J/m2 + :param value: dict{dict{[float]}} + """ + self._beam = value diff --git a/hub/city_model_structure/energy_systems/pv_generation_system.py b/hub/city_model_structure/energy_systems/pv_generation_system.py index 13409c7e..ddbc847a 100644 --- a/hub/city_model_structure/energy_systems/pv_generation_system.py +++ b/hub/city_model_structure/energy_systems/pv_generation_system.py @@ -26,6 +26,10 @@ class PvGenerationSystem(GenerationSystem): self._width = None self._height = None self._electricity_power = None + self._tilt_angle = None + self._surface_azimuth = None + self._solar_altitude_angle = None + self._solar_azimuth_angle = None @property def nominal_electricity_output(self): @@ -202,3 +206,35 @@ class PvGenerationSystem(GenerationSystem): :param value: float """ self._electricity_power = value + + @property + def tilt_angle(self): + """ + Get tilt angle of PV system in degrees + :return: float + """ + return self._tilt_angle + + @tilt_angle.setter + def tilt_angle(self, value): + """ + Set PV system tilt angle in degrees + :param value: float + """ + self._tilt_angle = value + + @property + def surface_azimuth(self): + """ + Get surface azimuth angle of PV system in degrees. 0 is North + :return: float + """ + return self._surface_azimuth + + @surface_azimuth.setter + def surface_azimuth(self, value): + """ + Set PV system tilt angle in degrees + :param value: float + """ + self._surface_azimuth = value diff --git a/hub/exports/formats/simplified_radiosity_algorithm.py b/hub/exports/formats/simplified_radiosity_algorithm.py index f9ea7f1d..25189419 100644 --- a/hub/exports/formats/simplified_radiosity_algorithm.py +++ b/hub/exports/formats/simplified_radiosity_algorithm.py @@ -67,7 +67,7 @@ class SimplifiedRadiosityAlgorithm: i = (total_days + day - 1) * 24 + hour - 1 representative_building = self._city.buildings[0] _global = representative_building.diffuse[cte.HOUR][i] / cte.WATTS_HOUR_TO_JULES - _beam = representative_building.beam[cte.HOUR][i] / cte.WATTS_HOUR_TO_JULES + _beam = representative_building.direct_normal[cte.HOUR][i] / cte.WATTS_HOUR_TO_JULES content += f'{day} {month} {hour} {_global} {_beam}\n' with open(file, 'w', encoding='utf-8') as file: file.write(content) diff --git a/hub/imports/energy_systems/montreal_future_energy_systems_parameters.py b/hub/imports/energy_systems/montreal_future_energy_systems_parameters.py index 8612f845..27c2f1cd 100644 --- a/hub/imports/energy_systems/montreal_future_energy_systems_parameters.py +++ b/hub/imports/energy_systems/montreal_future_energy_systems_parameters.py @@ -82,8 +82,7 @@ class MontrealFutureEnergySystemParameters: return _generic_energy_systems - @staticmethod - def _create_generation_systems(archetype_system): + def _create_generation_systems(self, archetype_system): _generation_systems = [] archetype_generation_systems = archetype_system.generation_systems if archetype_generation_systems is not None: @@ -107,6 +106,7 @@ class MontrealFutureEnergySystemParameters: _generation_system.cell_temperature_coefficient = archetype_generation_system.cell_temperature_coefficient _generation_system.width = archetype_generation_system.width _generation_system.height = archetype_generation_system.height + _generation_system.tilt_angle = self._city.latitude _generic_storage_system = None if archetype_generation_system.energy_storage_systems is not None: _generic_storage_system = ElectricalStorageSystem() diff --git a/hub/imports/results/simplified_radiosity_algorithm.py b/hub/imports/results/simplified_radiosity_algorithm.py index 9b09b5f8..3824e7ce 100644 --- a/hub/imports/results/simplified_radiosity_algorithm.py +++ b/hub/imports/results/simplified_radiosity_algorithm.py @@ -34,7 +34,7 @@ class SimplifiedRadiosityAlgorithm: for key in self._results: _irradiance = {} header_name = key.split(':') - result = [x for x in self._results[key]] + result = [x * cte.WATTS_HOUR_TO_JULES for x in self._results[key]] city_object_name = header_name[1] building = self._city.city_object(city_object_name) surface_id = header_name[2] diff --git a/hub/imports/weather/epw_weather_parameters.py b/hub/imports/weather/epw_weather_parameters.py index c2ed2e8f..d8f3a002 100644 --- a/hub/imports/weather/epw_weather_parameters.py +++ b/hub/imports/weather/epw_weather_parameters.py @@ -114,10 +114,14 @@ class EpwWeatherParameters: for x in self._weather_values['global_horizontal_radiation_wh_m2']] building.diffuse[cte.HOUR] = [x * cte.WATTS_HOUR_TO_JULES for x in self._weather_values['diffuse_horizontal_radiation_wh_m2']] - building.beam[cte.HOUR] = [x * cte.WATTS_HOUR_TO_JULES - for x in self._weather_values['direct_normal_radiation_wh_m2']] + building.direct_normal[cte.HOUR] = [x * cte.WATTS_HOUR_TO_JULES + for x in self._weather_values['direct_normal_radiation_wh_m2']] + building.beam[cte.HOUR] = [building.global_horizontal[cte.HOUR][i] - + building.diffuse[cte.HOUR][i] + for i in range(len(building.global_horizontal[cte.HOUR]))] building.cold_water_temperature[cte.HOUR] = wh().cold_water_temperature(building.external_temperature[cte.HOUR]) + # create the monthly and yearly values out of the hourly for building in self._city.buildings: building.external_temperature[cte.MONTH] = \ diff --git a/main.py b/main.py index 4af9643a..86ffeb24 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,6 @@ from scripts.geojson_creator import process_geojson from pathlib import Path import subprocess -from scripts.ep_run_enrich import energy_plus_workflow from hub.imports.geometry_factory import GeometryFactory from hub.helpers.dictionaries import Dictionaries from hub.imports.construction_factory import ConstructionFactory @@ -18,8 +17,11 @@ from scripts.costs.cost import Cost from scripts.costs.constants import SKIN_RETROFIT_AND_SYSTEM_RETROFIT_AND_PV, SYSTEM_RETROFIT_AND_PV import hub.helpers.constants as cte from hub.exports.exports_factory import ExportsFactory +import csv +from scripts.solar_angles import CitySolarAngles +from scripts.radiation_tilted import RadiationTilted # Specify the GeoJSON file path -# geojson_file = process_geojson(x=-73.5953602192335, y=45.492414530022515, diff=0.001) +geojson_file = process_geojson(x=-73.5681295982132, y=45.49218262677643, diff=0.001) file_path = (Path(__file__).parent / 'input_files' / 'output_buildings.geojson') # Specify the output path for the PDF file output_path = (Path(__file__).parent / 'out_files').resolve() @@ -30,9 +32,8 @@ city = GeometryFactory('geojson', year_of_construction_field='year_of_construction', function_field='function', function_to_hub=Dictionaries().montreal_function_to_hub_function).city -# Enrich city data +# # Enrich city data ConstructionFactory('nrcan', city).enrich() - UsageFactory('nrcan', city).enrich() WeatherFactory('epw', city).enrich() energy_plus_workflow(city) diff --git a/scripts/pv_sizing_and_simulation.py b/scripts/pv_sizing_and_simulation.py new file mode 100644 index 00000000..cd8e1673 --- /dev/null +++ b/scripts/pv_sizing_and_simulation.py @@ -0,0 +1,21 @@ +from scripts.radiation_tilted import RadiationTilted +from scripts.solar_angles import CitySolarAngles +import hub.helpers.constants as cte + + +class PVSizingSimulation: + def __init__(self, city, tilt_angle): + self.city = city + self.tilt_angle = tilt_angle + self.solar_angles = CitySolarAngles(file_name=self.city.name, + location_latitude=self.city.latitude, + location_longitude=self.city.longitude, + tilt_angle=self.tilt_angle, + surface_azimuth_angle=180, + standard_meridian=-75).calculate + self.enrich_radiation_data() + + def enrich_radiation_data(self): + for building in self.city.buildings: + roof_horizontal = [x / cte.WATTS_HOUR_TO_JULES for x in building.roofs[0].global_irradiance[cte.HOUR]] + RadiationTilted(building, self.solar_angles, tilt_angle=self.tilt_angle, ghi=roof_horizontal).enrich() diff --git a/scripts/radiation_tilted.py b/scripts/radiation_tilted.py new file mode 100644 index 00000000..60c8d7de --- /dev/null +++ b/scripts/radiation_tilted.py @@ -0,0 +1,109 @@ +import pandas as pd +import math +import hub.helpers.constants as cte +from hub.helpers.monthly_values import MonthlyValues + + +class RadiationTilted: + def __init__(self, building, solar_angles, tilt_angle, ghi, solar_constant=1366.1, maximum_clearness_index=1, + min_cos_zenith=0.065, maximum_zenith_angle=87): + self.building = building + self.ghi = ghi + self.tilt_angle = tilt_angle + self.zeniths = solar_angles['zenith'].tolist()[:-1] + self.incidents = solar_angles['incident angle'].tolist()[:-1] + self.date_time = solar_angles['DateTime'].tolist()[:-1] + data = {'DateTime': self.date_time, 'zenith': self.zeniths, 'incident angle': self.incidents, 'ghi': self.ghi} + self.df = pd.DataFrame(data) + self.df['DateTime'] = pd.to_datetime(self.df['DateTime']) + self.df.set_index('DateTime', inplace=True) + self.solar_constant = solar_constant + self.maximum_clearness_index = maximum_clearness_index + self.min_cos_zenith = min_cos_zenith + self.maximum_zenith_angle = maximum_zenith_angle + self.i_on = [] + self.i_oh = [] + self.k_t = [] + self.fraction_diffuse = [] + self.diffuse_horizontal = [] + self.beam_horizontal = [] + self.dni = [] + self.beam_tilted = [] + self.diffuse_tilted = [] + self.total_radiation_tilted = [] + self.calculate() + + def dni_extra(self): + for i in range(len(self.df)): + self.i_on.append(self.solar_constant * (1 + 0.033 * math.cos(math.radians(360 * self.df.index.dayofyear[i] / 365)))) + + self.df['extraterrestrial normal radiation (Wh/m2)'] = self.i_on + + def clearness_index(self): + for i in range(len(self.df)): + self.i_oh.append(self.i_on[i] * max(math.cos(math.radians(self.zeniths[i])), self.min_cos_zenith)) + self.k_t.append(self.ghi[i] / self.i_oh[i]) + self.k_t[i] = max(0, self.k_t[i]) + self.k_t[i] = min(self.maximum_clearness_index, self.k_t[i]) + self.df['extraterrestrial radiation on horizontal (Wh/m2)'] = self.i_oh + self.df['clearness index'] = self.k_t + + def diffuse_fraction(self): + for i in range(len(self.df)): + if self.k_t[i] <= 0.22: + self.fraction_diffuse.append(1 - 0.09 * self.k_t[i]) + elif self.k_t[i] <= 0.8: + self.fraction_diffuse.append(0.9511 - 0.1604 * self.k_t[i] + 4.388 * self.k_t[i] ** 2 - + 16.638 * self.k_t[i] ** 3 + 12.336 * self.k_t[i] ** 4) + else: + self.fraction_diffuse.append(0.165) + if self.zeniths[i] > self.maximum_zenith_angle: + self.fraction_diffuse[i] = 1 + + self.df['diffuse fraction'] = self.fraction_diffuse + + def radiation_components_horizontal(self): + for i in range(len(self.df)): + self.diffuse_horizontal.append(self.ghi[i] * self.fraction_diffuse[i]) + self.beam_horizontal.append(self.ghi[i] - self.diffuse_horizontal[i]) + self.dni.append((self.ghi[i] - self.diffuse_horizontal[i]) / math.cos(math.radians(self.zeniths[i]))) + if self.zeniths[i] > self.maximum_zenith_angle or self.dni[i] < 0: + self.dni[i] = 0 + + self.df['diffuse horizontal (Wh/m2)'] = self.diffuse_horizontal + self.df['dni (Wh/m2)'] = self.dni + self.df['beam horizontal (Wh/m2)'] = self.beam_horizontal + + def radiation_components_tilted(self): + for i in range(len(self.df)): + self.beam_tilted.append(self.dni[i] * math.cos(math.radians(self.incidents[i]))) + self.beam_tilted[i] = max(self.beam_tilted[i], 0) + self.diffuse_tilted.append(self.diffuse_horizontal[i] * ((1 + math.cos(math.radians(self.tilt_angle))) / 2)) + self.total_radiation_tilted.append(self.beam_tilted[i] + self.diffuse_tilted[i]) + + self.df['beam tilted (Wh/m2)'] = self.beam_tilted + self.df['diffuse tilted (Wh/m2)'] = self.diffuse_tilted + self.df['total radiation tilted (Wh/m2)'] = self.total_radiation_tilted + + def calculate(self) -> pd.DataFrame: + self.dni_extra() + self.clearness_index() + self.diffuse_fraction() + self.radiation_components_horizontal() + self.radiation_components_tilted() + return self.df + + def enrich(self): + tilted_radiation = self.total_radiation_tilted + print(len(tilted_radiation)) + self.building.roofs[0].global_irradiance_tilted[cte.HOUR] = [x * cte.WATTS_HOUR_TO_JULES for x in + tilted_radiation] + self.building.roofs[0].global_irradiance_tilted[cte.HOUR] = [x * cte.WATTS_HOUR_TO_JULES for x in + tilted_radiation] + self.building.roofs[0].global_irradiance_tilted[cte.MONTH] = ( + MonthlyValues.get_total_month(self.building.roofs[0].global_irradiance_tilted[cte.HOUR])) + self.building.roofs[0].global_irradiance_tilted[cte.YEAR] = \ + [sum(self.building.roofs[0].global_irradiance_tilted[cte.MONTH])] + + + diff --git a/scripts/solar_angles.py b/scripts/solar_angles.py new file mode 100644 index 00000000..d65fcf58 --- /dev/null +++ b/scripts/solar_angles.py @@ -0,0 +1,146 @@ +import math +import pandas as pd +from datetime import datetime +from pathlib import Path + + +class CitySolarAngles: + def __init__(self, file_name, location_latitude, location_longitude, tilt_angle, surface_azimuth_angle, + standard_meridian=-75): + self.file_name = file_name + self.location_latitude = location_latitude + self.location_longitude = location_longitude + self.location_latitude_rad = math.radians(location_latitude) + self.surface_azimuth_angle = surface_azimuth_angle + self.surface_azimuth_rad = math.radians(surface_azimuth_angle) + self.tilt_angle = tilt_angle + self.tilt_angle_rad = math.radians(tilt_angle) + self.standard_meridian = standard_meridian + self.longitude_correction = (location_longitude - standard_meridian) * 4 + self.timezone = 'Etc/GMT+5' + + self.eot = [] + self.ast = [] + self.hour_angles = [] + self.declinations = [] + self.solar_altitudes = [] + self.solar_azimuths = [] + self.zeniths = [] + self.incidents = [] + self.beam_tilted = [] + self.factor = [] + self.times = pd.date_range(start='2023-01-01', end='2024-01-01', freq='H', tz=self.timezone) + self.df = pd.DataFrame(index=self.times) + self.day_of_year = self.df.index.dayofyear + + def solar_time(self, datetime_val, day_of_year): + b = (day_of_year - 81) * 2 * math.pi / 364 + eot = 9.87 * math.sin(2 * b) - 7.53 * math.cos(b) - 1.5 * math.sin(b) + self.eot.append(eot) + + # Calculate Local Solar Time (LST) + lst_hour = datetime_val.hour + lst_minute = datetime_val.minute + lst_second = datetime_val.second + lst = lst_hour + lst_minute / 60 + lst_second / 3600 + + # Calculate Apparent Solar Time (AST) in decimal hours + ast_decimal = lst + eot / 60 + self.longitude_correction / 60 + ast_hours = int(ast_decimal) + ast_minutes = round((ast_decimal - ast_hours) * 60) + + # Ensure ast_minutes is within valid range + if ast_minutes == 60: + ast_hours += 1 + ast_minutes = 0 + elif ast_minutes < 0: + ast_minutes = 0 + ast_time = datetime(year=datetime_val.year, month=datetime_val.month, day=datetime_val.day, + hour=ast_hours, minute=ast_minutes) + self.ast.append(ast_time) + return ast_time + + def declination_angle(self, day_of_year): + declination = 23.45 * math.sin(math.radians(360 / 365 * (284 + day_of_year))) + declination_radian = math.radians(declination) + self.declinations.append(declination) + return declination_radian + + def hour_angle(self, ast_time): + hour_angle = ((ast_time.hour * 60 + ast_time.minute) - 720) / 4 + hour_angle_radian = math.radians(hour_angle) + self.hour_angles.append(hour_angle) + return hour_angle_radian + + def solar_altitude(self, declination_radian, hour_angle_radian): + solar_altitude_radians = math.asin(math.cos(self.location_latitude_rad) * math.cos(declination_radian) * + math.cos(hour_angle_radian) + math.sin(self.location_latitude_rad) * + math.sin(declination_radian)) + solar_altitude = math.degrees(solar_altitude_radians) + self.solar_altitudes.append(solar_altitude) + return solar_altitude_radians + + def zenith(self, solar_altitude_radians): + solar_altitude = math.degrees(solar_altitude_radians) + zenith_degree = 90 - solar_altitude + zenith_radian = math.radians(zenith_degree) + self.zeniths.append(zenith_degree) + return zenith_radian + + def solar_azimuth_analytical(self, hourangle, declination, zenith): + numer = (math.cos(zenith) * math.sin(self.location_latitude_rad) - math.sin(declination)) + denom = (math.sin(zenith) * math.cos(self.location_latitude_rad)) + if math.isclose(denom, 0.0, abs_tol=1e-8): + cos_azi = 1.0 + else: + cos_azi = numer / denom + + cos_azi = max(-1.0, min(1.0, cos_azi)) + + sign_ha = math.copysign(1, hourangle) + solar_azimuth_radians = sign_ha * math.acos(cos_azi) + math.pi + solar_azimuth_degrees = math.degrees(solar_azimuth_radians) + self.solar_azimuths.append(solar_azimuth_degrees) + return solar_azimuth_radians + + def incident_angle(self, solar_altitude_radians, solar_azimuth_radians): + incident_radian = math.acos(math.cos(solar_altitude_radians) * + math.cos(abs(solar_azimuth_radians - self.surface_azimuth_rad)) * + math.sin(self.tilt_angle_rad) + math.sin(solar_altitude_radians) * + math.cos(self.tilt_angle_rad)) + incident_angle_degrees = math.degrees(incident_radian) + self.incidents.append(incident_angle_degrees) + return incident_radian + + @property + def calculate(self) -> pd.DataFrame: + for i in range(len(self.times)): + datetime_val = self.times[i] + day_of_year = self.day_of_year[i] + declination_radians = self.declination_angle(day_of_year) + ast_time = self.solar_time(datetime_val, day_of_year) + hour_angle_radians = self.hour_angle(ast_time) + solar_altitude_radians = self.solar_altitude(declination_radians, hour_angle_radians) + zenith_radians = self.zenith(solar_altitude_radians) + solar_azimuth_radians = self.solar_azimuth_analytical(hour_angle_radians, declination_radians, zenith_radians) + incident_angle_radian = self.incident_angle(solar_altitude_radians, solar_azimuth_radians) + + self.df['DateTime'] = self.times + self.df['AST'] = self.ast + self.df['hour angle'] = self.hour_angles + self.df['eot'] = self.eot + self.df['declination angle'] = self.declinations + self.df['solar altitude'] = self.solar_altitudes + self.df['zenith'] = self.zeniths + self.df['solar azimuth'] = self.solar_azimuths + self.df['incident angle'] = self.incidents + + return self.df + + + + + + + + diff --git a/tests/test_construction_factory.py b/tests/test_construction_factory.py index 710894bd..a340594a 100644 --- a/tests/test_construction_factory.py +++ b/tests/test_construction_factory.py @@ -81,7 +81,7 @@ class TestConstructionFactory(TestCase): self.assertEqual(len(building.external_temperature), 0, 'building external temperature is calculated') self.assertEqual(len(building.global_horizontal), 0, 'building global horizontal is calculated') self.assertEqual(len(building.diffuse), 0, 'building diffuse is calculated') - self.assertEqual(len(building.beam), 0, 'building beam is calculated') + self.assertEqual(len(building.direct_normal), 0, 'building beam is calculated') self.assertIsNotNone(building.lower_corner, 'building lower corner is none') self.assertEqual(len(building.sensors), 0, 'building sensors are assigned') self.assertIsNotNone(building.internal_zones, 'no internal zones created') diff --git a/tests/test_geometry_factory.py b/tests/test_geometry_factory.py index 3b5bd8f8..d66c815f 100644 --- a/tests/test_geometry_factory.py +++ b/tests/test_geometry_factory.py @@ -52,7 +52,7 @@ class TestGeometryFactory(TestCase): self.assertEqual(len(building.external_temperature), 0, 'building external temperature is calculated') self.assertEqual(len(building.global_horizontal), 0, 'building global horizontal is calculated') self.assertEqual(len(building.diffuse), 0, 'building diffuse is calculated') - self.assertEqual(len(building.beam), 0, 'building beam is calculated') + self.assertEqual(len(building.direct_normal), 0, 'building beam is calculated') self.assertIsNotNone(building.lower_corner, 'building lower corner is none') self.assertEqual(len(building.sensors), 0, 'building sensors are assigned') self.assertIsNotNone(building.internal_zones, 'no internal zones created') diff --git a/tests/test_usage_factory.py b/tests/test_usage_factory.py index 6b6d821a..49cb45db 100644 --- a/tests/test_usage_factory.py +++ b/tests/test_usage_factory.py @@ -44,7 +44,7 @@ class TestUsageFactory(TestCase): self.assertEqual(len(building.external_temperature), 0, 'building external temperature is calculated') self.assertEqual(len(building.global_horizontal), 0, 'building global horizontal is calculated') self.assertEqual(len(building.diffuse), 0, 'building diffuse is calculated') - self.assertEqual(len(building.beam), 0, 'building beam is calculated') + self.assertEqual(len(building.direct_normal), 0, 'building beam is calculated') self.assertIsNotNone(building.lower_corner, 'building lower corner is none') self.assertEqual(len(building.sensors), 0, 'building sensors are assigned') self.assertIsNotNone(building.internal_zones, 'no internal zones created') From e636459b867c2023ce87986f7f4536c67d04adca Mon Sep 17 00:00:00 2001 From: s_ranjbar Date: Tue, 28 May 2024 11:25:18 -0400 Subject: [PATCH 02/15] feat: two new codes added to separate energy system retrofit and pv calculations --- energy_system_retrofit.py | 64 +++++++++++++++++++++++++++++++++++++ main.py | 62 +---------------------------------- pv_assessment.py | 39 ++++++++++++++++++++++ scripts/radiation_tilted.py | 3 +- 4 files changed, 105 insertions(+), 63 deletions(-) create mode 100644 energy_system_retrofit.py create mode 100644 pv_assessment.py diff --git a/energy_system_retrofit.py b/energy_system_retrofit.py new file mode 100644 index 00000000..2e49b382 --- /dev/null +++ b/energy_system_retrofit.py @@ -0,0 +1,64 @@ +from scripts.geojson_creator import process_geojson +from pathlib import Path +import subprocess +from scripts.ep_run_enrich import energy_plus_workflow +from hub.imports.geometry_factory import GeometryFactory +from hub.helpers.dictionaries import Dictionaries +from hub.imports.construction_factory import ConstructionFactory +from hub.imports.usage_factory import UsageFactory +from hub.imports.weather_factory import WeatherFactory +from hub.imports.results_factory import ResultFactory +from scripts.energy_system_analysis_report import EnergySystemAnalysisReport +from scripts import random_assignation +from hub.imports.energy_systems_factory import EnergySystemsFactory +from scripts.energy_system_sizing import SystemSizing +from scripts.energy_system_retrofit_results import system_results, new_system_results +from scripts.energy_system_sizing_and_simulation_factory import EnergySystemsSimulationFactory +from scripts.costs.cost import Cost +from scripts.costs.constants import SKIN_RETROFIT_AND_SYSTEM_RETROFIT_AND_PV, SYSTEM_RETROFIT_AND_PV +import hub.helpers.constants as cte +from hub.exports.exports_factory import ExportsFactory +# Specify the GeoJSON file path +geojson_file = process_geojson(x=-73.5681295982132, y=45.49218262677643, diff=0.0001) +file_path = (Path(__file__).parent / 'input_files' / 'output_buildings.geojson') +# Specify the output path for the PDF file +output_path = (Path(__file__).parent / 'out_files').resolve() +# Create city object from GeoJSON file +city = GeometryFactory('geojson', + path=file_path, + height_field='height', + year_of_construction_field='year_of_construction', + function_field='function', + function_to_hub=Dictionaries().montreal_function_to_hub_function).city +# Enrich city data +ConstructionFactory('nrcan', city).enrich() + +UsageFactory('nrcan', city).enrich() +WeatherFactory('epw', city).enrich() +ExportsFactory('sra', city, output_path).export() +sra_path = (output_path / f'{city.name}_sra.xml').resolve() +subprocess.run(['sra', str(sra_path)]) +ResultFactory('sra', city, output_path).enrich() +energy_plus_workflow(city) +random_assignation.call_random(city.buildings, random_assignation.residential_systems_percentage) +EnergySystemsFactory('montreal_custom', city).enrich() +SystemSizing(city.buildings).montreal_custom() +current_system = new_system_results(city.buildings) +random_assignation.call_random(city.buildings, random_assignation.residential_new_systems_percentage) +EnergySystemsFactory('montreal_future', city).enrich() +for building in city.buildings: + EnergySystemsSimulationFactory('archetype1', building=building, output_path=output_path).enrich() + print(building.energy_consumption_breakdown[cte.ELECTRICITY][cte.COOLING] + + building.energy_consumption_breakdown[cte.ELECTRICITY][cte.HEATING] + + building.energy_consumption_breakdown[cte.ELECTRICITY][cte.DOMESTIC_HOT_WATER]) +new_system = new_system_results(city.buildings) +# EnergySystemAnalysisReport(city, output_path).create_report(current_system, new_system) +for building in city.buildings: + costs = Cost(building=building, retrofit_scenario=SYSTEM_RETROFIT_AND_PV).life_cycle + costs.to_csv(output_path / f'{building.name}_lcc.csv') + (costs.loc['global_operational_costs', f'Scenario {SYSTEM_RETROFIT_AND_PV}']. + to_csv(output_path / f'{building.name}_op.csv')) + costs.loc['global_capital_costs', f'Scenario {SYSTEM_RETROFIT_AND_PV}'].to_csv( + output_path / f'{building.name}_cc.csv') + costs.loc['global_maintenance_costs', f'Scenario {SYSTEM_RETROFIT_AND_PV}'].to_csv( + output_path / f'{building.name}_m.csv') \ No newline at end of file diff --git a/main.py b/main.py index 86ffeb24..6fb66a5e 100644 --- a/main.py +++ b/main.py @@ -1,64 +1,4 @@ -from scripts.geojson_creator import process_geojson -from pathlib import Path -import subprocess -from hub.imports.geometry_factory import GeometryFactory -from hub.helpers.dictionaries import Dictionaries -from hub.imports.construction_factory import ConstructionFactory -from hub.imports.usage_factory import UsageFactory -from hub.imports.weather_factory import WeatherFactory -from hub.imports.results_factory import ResultFactory -from scripts.energy_system_analysis_report import EnergySystemAnalysisReport -from scripts import random_assignation -from hub.imports.energy_systems_factory import EnergySystemsFactory -from scripts.energy_system_sizing import SystemSizing -from scripts.energy_system_retrofit_results import system_results, new_system_results -from scripts.energy_system_sizing_and_simulation_factory import EnergySystemsSimulationFactory -from scripts.costs.cost import Cost -from scripts.costs.constants import SKIN_RETROFIT_AND_SYSTEM_RETROFIT_AND_PV, SYSTEM_RETROFIT_AND_PV -import hub.helpers.constants as cte -from hub.exports.exports_factory import ExportsFactory -import csv -from scripts.solar_angles import CitySolarAngles -from scripts.radiation_tilted import RadiationTilted -# Specify the GeoJSON file path -geojson_file = process_geojson(x=-73.5681295982132, y=45.49218262677643, diff=0.001) -file_path = (Path(__file__).parent / 'input_files' / 'output_buildings.geojson') -# Specify the output path for the PDF file -output_path = (Path(__file__).parent / 'out_files').resolve() -# Create city object from GeoJSON file -city = GeometryFactory('geojson', - path=file_path, - height_field='height', - year_of_construction_field='year_of_construction', - function_field='function', - function_to_hub=Dictionaries().montreal_function_to_hub_function).city -# # Enrich city data -ConstructionFactory('nrcan', city).enrich() -UsageFactory('nrcan', city).enrich() -WeatherFactory('epw', city).enrich() -energy_plus_workflow(city) -random_assignation.call_random(city.buildings, random_assignation.residential_systems_percentage) -EnergySystemsFactory('montreal_custom', city).enrich() -SystemSizing(city.buildings).montreal_custom() -current_system = new_system_results(city.buildings) -random_assignation.call_random(city.buildings, random_assignation.residential_new_systems_percentage) -EnergySystemsFactory('montreal_future', city).enrich() -for building in city.buildings: - EnergySystemsSimulationFactory('archetype1', building=building, output_path=output_path).enrich() - print(building.energy_consumption_breakdown[cte.ELECTRICITY][cte.COOLING] + - building.energy_consumption_breakdown[cte.ELECTRICITY][cte.HEATING] + - building.energy_consumption_breakdown[cte.ELECTRICITY][cte.DOMESTIC_HOT_WATER]) -new_system = new_system_results(city.buildings) -# EnergySystemAnalysisReport(city, output_path).create_report(current_system, new_system) -for building in city.buildings: - costs = Cost(building=building, retrofit_scenario=SYSTEM_RETROFIT_AND_PV).life_cycle - costs.to_csv(output_path / f'{building.name}_lcc.csv') - (costs.loc['global_operational_costs', f'Scenario {SYSTEM_RETROFIT_AND_PV}']. - to_csv(output_path / f'{building.name}_op.csv')) - costs.loc['global_capital_costs', f'Scenario {SYSTEM_RETROFIT_AND_PV}'].to_csv( - output_path / f'{building.name}_cc.csv') - costs.loc['global_maintenance_costs', f'Scenario {SYSTEM_RETROFIT_AND_PV}'].to_csv( - output_path / f'{building.name}_m.csv') + diff --git a/pv_assessment.py b/pv_assessment.py new file mode 100644 index 00000000..7d8f5ce5 --- /dev/null +++ b/pv_assessment.py @@ -0,0 +1,39 @@ +from scripts.geojson_creator import process_geojson +from pathlib import Path +import subprocess +from hub.imports.geometry_factory import GeometryFactory +from hub.helpers.dictionaries import Dictionaries +from hub.imports.construction_factory import ConstructionFactory +from hub.imports.usage_factory import UsageFactory +from hub.imports.weather_factory import WeatherFactory +from hub.imports.results_factory import ResultFactory +from scripts.solar_angles import CitySolarAngles +from scripts.radiation_tilted import RadiationTilted +import hub.helpers.constants as cte +from hub.exports.exports_factory import ExportsFactory +# Specify the GeoJSON file path +geojson_file = process_geojson(x=-73.5681295982132, y=45.49218262677643, diff=0.0001) +file_path = (Path(__file__).parent / 'input_files' / 'output_buildings.geojson') +# Specify the output path for the PDF file +output_path = (Path(__file__).parent / 'out_files').resolve() +# Create city object from GeoJSON file +city = GeometryFactory('geojson', + path=file_path, + height_field='height', + year_of_construction_field='year_of_construction', + function_field='function', + function_to_hub=Dictionaries().montreal_function_to_hub_function).city +# Enrich city data +ConstructionFactory('nrcan', city).enrich() + +UsageFactory('nrcan', city).enrich() +WeatherFactory('epw', city).enrich() +ExportsFactory('sra', city, output_path).export() +sra_path = (output_path / f'{city.name}_sra.xml').resolve() +subprocess.run(['sra', str(sra_path)]) +ResultFactory('sra', city, output_path).enrich() +for building in city.buildings: + roof_horizontal = [x / cte.WATTS_HOUR_TO_JULES for x in building.roofs[0].global_irradiance[cte.HOUR]] + solar_angles = ( + CitySolarAngles(city.name, city.latitude, city.longitude, tilt_angle=45, surface_azimuth_angle=180).calculate) + RadiationTilted(building, solar_angles, tilt_angle=45, ghi=roof_horizontal).enrich() diff --git a/scripts/radiation_tilted.py b/scripts/radiation_tilted.py index 60c8d7de..4eebf332 100644 --- a/scripts/radiation_tilted.py +++ b/scripts/radiation_tilted.py @@ -95,7 +95,6 @@ class RadiationTilted: def enrich(self): tilted_radiation = self.total_radiation_tilted - print(len(tilted_radiation)) self.building.roofs[0].global_irradiance_tilted[cte.HOUR] = [x * cte.WATTS_HOUR_TO_JULES for x in tilted_radiation] self.building.roofs[0].global_irradiance_tilted[cte.HOUR] = [x * cte.WATTS_HOUR_TO_JULES for x in @@ -103,7 +102,7 @@ class RadiationTilted: self.building.roofs[0].global_irradiance_tilted[cte.MONTH] = ( MonthlyValues.get_total_month(self.building.roofs[0].global_irradiance_tilted[cte.HOUR])) self.building.roofs[0].global_irradiance_tilted[cte.YEAR] = \ - [sum(self.building.roofs[0].global_irradiance_tilted[cte.MONTH])] + [sum(self.building.roofs[0].global_irradiance_tilted[cte.MONTH])] From 84bde67d0f7bf8609931128c2c758e372430ca70 Mon Sep 17 00:00:00 2001 From: s_ranjbar Date: Thu, 30 May 2024 17:26:50 -0400 Subject: [PATCH 03/15] feat: PV sizing module is added --- pv_assessment.py | 46 ++++++++++++++--- scripts/pv_sizing_and_simulation.py | 77 +++++++++++++++++++++++------ scripts/radiation_tilted.py | 7 ++- 3 files changed, 107 insertions(+), 23 deletions(-) diff --git a/pv_assessment.py b/pv_assessment.py index 7d8f5ce5..7978df3a 100644 --- a/pv_assessment.py +++ b/pv_assessment.py @@ -1,3 +1,4 @@ +import pandas as pd from scripts.geojson_creator import process_geojson from pathlib import Path import subprocess @@ -8,11 +9,12 @@ from hub.imports.usage_factory import UsageFactory from hub.imports.weather_factory import WeatherFactory from hub.imports.results_factory import ResultFactory from scripts.solar_angles import CitySolarAngles -from scripts.radiation_tilted import RadiationTilted +from scripts.ep_run_enrich import energy_plus_workflow import hub.helpers.constants as cte from hub.exports.exports_factory import ExportsFactory +from scripts.pv_sizing_and_simulation import PVSizingSimulation # Specify the GeoJSON file path -geojson_file = process_geojson(x=-73.5681295982132, y=45.49218262677643, diff=0.0001) +geojson_file = process_geojson(x=-73.5681295982132, y=45.49218262677643, diff=0.0005) file_path = (Path(__file__).parent / 'input_files' / 'output_buildings.geojson') # Specify the output path for the PDF file output_path = (Path(__file__).parent / 'out_files').resolve() @@ -32,8 +34,40 @@ ExportsFactory('sra', city, output_path).export() sra_path = (output_path / f'{city.name}_sra.xml').resolve() subprocess.run(['sra', str(sra_path)]) ResultFactory('sra', city, output_path).enrich() +energy_plus_workflow(city) +solar_angles = CitySolarAngles(city.name, + city.latitude, + city.longitude, + tilt_angle=45, + surface_azimuth_angle=180).calculate +df = pd.DataFrame() +df.index = ['yearly lighting (kWh)', 'yearly appliance (kWh)', 'yearly heating (kWh)', 'yearly cooling (kWh)', + 'yearly dhw (kWh)', 'roof area (m2)', 'used area for pv (m2)', 'number of panels', 'pv production (kWh)'] for building in city.buildings: - roof_horizontal = [x / cte.WATTS_HOUR_TO_JULES for x in building.roofs[0].global_irradiance[cte.HOUR]] - solar_angles = ( - CitySolarAngles(city.name, city.latitude, city.longitude, tilt_angle=45, surface_azimuth_angle=180).calculate) - RadiationTilted(building, solar_angles, tilt_angle=45, ghi=roof_horizontal).enrich() + ghi = [x / cte.WATTS_HOUR_TO_JULES for x in building.roofs[0].global_irradiance[cte.HOUR]] + pv_sizing_simulation = PVSizingSimulation(building, + solar_angles, + tilt_angle=45, + module_height=1, + module_width=2, + ghi=ghi) + pv_sizing_simulation.pv_output() + yearly_lighting = building.lighting_electrical_demand[cte.YEAR][0] / 1000 + yearly_appliance = building.appliances_electrical_demand[cte.YEAR][0] / 1000 + yearly_heating = building.heating_demand[cte.YEAR][0] / (3.6e6 * 3) + yearly_cooling = building.cooling_demand[cte.YEAR][0] / (3.6e6 * 4.5) + yearly_dhw = building.domestic_hot_water_heat_demand[cte.YEAR][0] / 1000 + roof_area = building.roofs[0].perimeter_area + used_roof = pv_sizing_simulation.available_space() + number_of_pv_panels = pv_sizing_simulation.total_number_of_panels + yearly_pv = building.onsite_electrical_production[cte.YEAR][0] / 1000 + df[f'{building.name}'] = [yearly_lighting, yearly_appliance, yearly_heating, yearly_cooling, yearly_dhw, roof_area, + used_roof, number_of_pv_panels, yearly_pv] + +df.to_csv(output_path / 'pv.csv') + + + + + + diff --git a/scripts/pv_sizing_and_simulation.py b/scripts/pv_sizing_and_simulation.py index cd8e1673..877a1499 100644 --- a/scripts/pv_sizing_and_simulation.py +++ b/scripts/pv_sizing_and_simulation.py @@ -1,21 +1,66 @@ +import math + from scripts.radiation_tilted import RadiationTilted -from scripts.solar_angles import CitySolarAngles import hub.helpers.constants as cte +from hub.helpers.monthly_values import MonthlyValues + + +class PVSizingSimulation(RadiationTilted): + def __init__(self, building, solar_angles, tilt_angle, module_height, module_width, ghi): + super().__init__(building, solar_angles, tilt_angle, ghi) + self.module_height = module_height + self.module_width = module_width + self.total_number_of_panels = 0 + self.enrich() + + def available_space(self): + roof_area = self.building.roofs[0].perimeter_area + maintenance_factor = 0.1 + orientation_factor = 0.2 + if self.building.function == cte.RESIDENTIAL: + mechanical_equipment_factor = 0.2 + else: + mechanical_equipment_factor = 0.3 + available_roof = (maintenance_factor + orientation_factor + mechanical_equipment_factor) * roof_area + return available_roof + + def inter_row_spacing(self): + winter_solstice = self.df[(self.df['AST'].dt.month == 12) & + (self.df['AST'].dt.day == 21) & + (self.df['AST'].dt.hour == 12)] + solar_altitude = winter_solstice['solar altitude'].values[0] + solar_azimuth = winter_solstice['solar azimuth'].values[0] + distance = ((self.module_height * abs(math.cos(math.radians(solar_azimuth)))) / + math.tan(math.radians(solar_altitude))) + distance = float(format(distance, '.1f')) + return distance + + def number_of_panels(self, available_roof, inter_row_distance): + space_dimension = math.sqrt(available_roof) + space_dimension = float(format(space_dimension, '.2f')) + panels_per_row = math.ceil(space_dimension / self.module_width) + number_of_rows = math.ceil(space_dimension / inter_row_distance) + self.total_number_of_panels = panels_per_row * number_of_rows + return panels_per_row, number_of_rows + + def pv_output(self): + radiation = self.total_radiation_tilted + pv_module_area = self.module_width * self.module_height + available_roof = self.available_space() + inter_row_spacing = self.inter_row_spacing() + self.number_of_panels(available_roof, inter_row_spacing) + system_efficiency = 0.2 + pv_hourly_production = [x * system_efficiency * self.total_number_of_panels * pv_module_area for x in radiation] + self.building.onsite_electrical_production[cte.HOUR] = pv_hourly_production + self.building.onsite_electrical_production[cte.MONTH] = ( + MonthlyValues.get_total_month(self.building.onsite_electrical_production[cte.HOUR])) + self.building.onsite_electrical_production[cte.YEAR] = [sum(self.building.onsite_electrical_production[cte.MONTH])] + + + + + + -class PVSizingSimulation: - def __init__(self, city, tilt_angle): - self.city = city - self.tilt_angle = tilt_angle - self.solar_angles = CitySolarAngles(file_name=self.city.name, - location_latitude=self.city.latitude, - location_longitude=self.city.longitude, - tilt_angle=self.tilt_angle, - surface_azimuth_angle=180, - standard_meridian=-75).calculate - self.enrich_radiation_data() - def enrich_radiation_data(self): - for building in self.city.buildings: - roof_horizontal = [x / cte.WATTS_HOUR_TO_JULES for x in building.roofs[0].global_irradiance[cte.HOUR]] - RadiationTilted(building, self.solar_angles, tilt_angle=self.tilt_angle, ghi=roof_horizontal).enrich() diff --git a/scripts/radiation_tilted.py b/scripts/radiation_tilted.py index 4eebf332..beb62088 100644 --- a/scripts/radiation_tilted.py +++ b/scripts/radiation_tilted.py @@ -13,9 +13,14 @@ class RadiationTilted: self.zeniths = solar_angles['zenith'].tolist()[:-1] self.incidents = solar_angles['incident angle'].tolist()[:-1] self.date_time = solar_angles['DateTime'].tolist()[:-1] - data = {'DateTime': self.date_time, 'zenith': self.zeniths, 'incident angle': self.incidents, 'ghi': self.ghi} + self.ast = solar_angles['AST'].tolist()[:-1] + self.solar_azimuth = solar_angles['solar azimuth'].tolist()[:-1] + self.solar_altitude = solar_angles['solar altitude'].tolist()[:-1] + data = {'DateTime': self.date_time, 'AST': self.ast, 'solar altitude': self.solar_altitude, 'zenith': self.zeniths, + 'solar azimuth': self.solar_azimuth, 'incident angle': self.incidents, 'ghi': self.ghi} self.df = pd.DataFrame(data) self.df['DateTime'] = pd.to_datetime(self.df['DateTime']) + self.df['AST'] = pd.to_datetime(self.df['AST']) self.df.set_index('DateTime', inplace=True) self.solar_constant = solar_constant self.maximum_clearness_index = maximum_clearness_index From cb842b59176c4e12b521d0147958b330cc5e8f79 Mon Sep 17 00:00:00 2001 From: s_ranjbar Date: Thu, 30 May 2024 17:26:50 -0400 Subject: [PATCH 04/15] feat: PV sizing module is added fix: bugs in catalogue and building are fixed feat: new archetype completed fix: building enrichment with results from archetype 13 is completed --- .../montreal_future_system_catalogue.py | 4 +- hub/city_model_structure/building.py | 61 ++-- .../energy_systems/thermal_storage_system.py | 19 +- .../montreal_future_systems.xml | 22 +- ...ntreal_future_energy_systems_parameters.py | 1 + main.py | 6 - scripts/random_assignation.py | 4 +- .../system_simulation_models/archetype1.py | 2 +- .../system_simulation_models/archetype13.py | 339 +++++++++++++++--- 9 files changed, 361 insertions(+), 97 deletions(-) diff --git a/hub/catalog_factories/energy_systems/montreal_future_system_catalogue.py b/hub/catalog_factories/energy_systems/montreal_future_system_catalogue.py index d8910960..625e362c 100644 --- a/hub/catalog_factories/energy_systems/montreal_future_system_catalogue.py +++ b/hub/catalog_factories/energy_systems/montreal_future_system_catalogue.py @@ -298,7 +298,7 @@ class MontrealFutureSystemCatalogue(Catalog): layers = [insulation_layer, tank_layer] nominal_capacity = tes['nominal_capacity'] losses_ratio = tes['losses_ratio'] - heating_coil_capacity = None + heating_coil_capacity = tes['heating_coil_capacity'] storage_component = ThermalStorageSystem(storage_id=storage_id, model_name=model_name, type_energy_stored=type_energy_stored, @@ -338,7 +338,7 @@ class MontrealFutureSystemCatalogue(Catalog): nominal_capacity = template['nominal_capacity'] losses_ratio = template['losses_ratio'] volume = template['physical_characteristics']['volume'] - heating_coil_capacity = None + heating_coil_capacity = template['heating_coil_capacity'] storage_component = ThermalStorageSystem(storage_id=storage_id, model_name=model_name, type_energy_stored=type_energy_stored, diff --git a/hub/city_model_structure/building.py b/hub/city_model_structure/building.py index 09b5e2b1..b928535b 100644 --- a/hub/city_model_structure/building.py +++ b/hub/city_model_structure/building.py @@ -450,7 +450,7 @@ class Building(CityObject): monthly_values = PeakLoads(self).heating_peak_loads_from_methodology if monthly_values is None: return None - results[cte.MONTH] = [x * cte.WATTS_HOUR_TO_JULES for x in monthly_values] + results[cte.MONTH] = [x for x in monthly_values] results[cte.YEAR] = [max(monthly_values)] return results @@ -876,37 +876,38 @@ class Building(CityObject): if demand_type in generation_system.energy_consumption: fuel_breakdown[f'{generation_system.fuel_type}'][f'{demand_type}'] = ( generation_system.energy_consumption)[f'{demand_type}'][cte.YEAR][0] + storage_systems = generation_system.energy_storage_systems + if storage_systems: + for storage_system in storage_systems: + if storage_system.type_energy_stored == 'thermal' and storage_system.heating_coil_energy_consumption: + fuel_breakdown[cte.ELECTRICITY][f'{demand_type}'] += storage_system.heating_coil_energy_consumption[cte.YEAR][0] #TODO: When simulation models of all energy system archetypes are created, this part can be removed - heating = 0 - cooling = 0 - dhw = 0 + heating_fuels = [] + dhw_fuels = [] + for energy_system in self.energy_systems: + if cte.HEATING in energy_system.demand_types: + for generation_system in energy_system.generation_systems: + heating_fuels.append(generation_system.fuel_type) + if cte.DOMESTIC_HOT_WATER in energy_system.demand_types: + for generation_system in energy_system.generation_systems: + dhw_fuels.append(generation_system.fuel_type) for key in fuel_breakdown: - if cte.HEATING not in fuel_breakdown[key]: - heating += 1 if key == cte.ELECTRICITY and cte.COOLING not in fuel_breakdown[key]: - cooling += 1 - if cte.DOMESTIC_HOT_WATER not in fuel_breakdown[key]: - dhw += 1 - if heating > 0: - for energy_system in energy_systems: - if cte.HEATING in energy_system.demand_types: - for generation_system in energy_system.generation_systems: - fuel_breakdown[generation_system.fuel_type][cte.HEATING] = self.heating_consumption[cte.YEAR][0] / 3600 - if dhw > 0: - for energy_system in energy_systems: - if cte.DOMESTIC_HOT_WATER in energy_system.demand_types: - for generation_system in energy_system.generation_systems: - fuel_breakdown[generation_system.fuel_type][cte.DOMESTIC_HOT_WATER] = \ - self.domestic_hot_water_consumption[cte.YEAR][0] / 3600 - if cooling > 0: - for energy_system in energy_systems: - if cte.COOLING in energy_system.demand_types: - for generation_system in energy_system.generation_systems: - fuel_breakdown[generation_system.fuel_type][cte.COOLING] = self.cooling_consumption[cte.YEAR][0] / 3600 - - - - - + for energy_system in energy_systems: + if cte.COOLING in energy_system.demand_types and cte.COOLING not in fuel_breakdown[key]: + for generation_system in energy_system.generation_systems: + fuel_breakdown[generation_system.fuel_type][cte.COOLING] = self.cooling_consumption[cte.YEAR][0] / 3600 + for fuel in heating_fuels: + if cte.HEATING not in fuel_breakdown[fuel]: + for energy_system in energy_systems: + if cte.HEATING in energy_system.demand_types: + for generation_system in energy_system.generation_systems: + fuel_breakdown[generation_system.fuel_type][cte.HEATING] = self.heating_consumption[cte.YEAR][0] / 3600 + for fuel in dhw_fuels: + if cte.DOMESTIC_HOT_WATER not in fuel_breakdown[fuel]: + for energy_system in energy_systems: + if cte.DOMESTIC_HOT_WATER in energy_system.demand_types: + for generation_system in energy_system.generation_systems: + fuel_breakdown[generation_system.fuel_type][cte.DOMESTIC_HOT_WATER] = self.domestic_hot_water_consumption[cte.YEAR][0] self._fuel_consumption_breakdown = fuel_breakdown return self._fuel_consumption_breakdown diff --git a/hub/city_model_structure/energy_systems/thermal_storage_system.py b/hub/city_model_structure/energy_systems/thermal_storage_system.py index 01a910f7..fd9b8f81 100644 --- a/hub/city_model_structure/energy_systems/thermal_storage_system.py +++ b/hub/city_model_structure/energy_systems/thermal_storage_system.py @@ -24,6 +24,7 @@ class ThermalStorageSystem(EnergyStorageSystem): self._maximum_operating_temperature = None self._heating_coil_capacity = None self._temperature = None + self._heating_coil_energy_consumption = None @property def volume(self): @@ -95,7 +96,7 @@ class ThermalStorageSystem(EnergyStorageSystem): Get heating coil capacity in Watts :return: float """ - return self._maximum_operating_temperature + return self._heating_coil_capacity @heating_coil_capacity.setter def heating_coil_capacity(self, value): @@ -120,3 +121,19 @@ class ThermalStorageSystem(EnergyStorageSystem): :param value: dict{[float]} """ self._temperature = value + + @property + def heating_coil_energy_consumption(self) -> dict: + """ + Get fuel consumption in W, m3, or kg + :return: dict{[float]} + """ + return self._heating_coil_energy_consumption + + @heating_coil_energy_consumption.setter + def heating_coil_energy_consumption(self, value): + """ + Set fuel consumption in W, m3, or kg + :param value: dict{[float]} + """ + self._heating_coil_energy_consumption = value diff --git a/hub/data/energy_systems/montreal_future_systems.xml b/hub/data/energy_systems/montreal_future_systems.xml index b42993d4..455235ae 100644 --- a/hub/data/energy_systems/montreal_future_systems.xml +++ b/hub/data/energy_systems/montreal_future_systems.xml @@ -911,7 +911,7 @@ - + 4.5 @@ -931,7 +931,13 @@ - + + bi-quadratic + COP + source_temperature + supply_temperature + + True @@ -1049,7 +1055,7 @@ 3.5 electricity - Water + Air Water @@ -1065,7 +1071,13 @@ - + + bi-quadratic + COP + source_temperature + supply_temperature + + @@ -1259,7 +1271,7 @@ sensible - + 5000 diff --git a/hub/imports/energy_systems/montreal_future_energy_systems_parameters.py b/hub/imports/energy_systems/montreal_future_energy_systems_parameters.py index 27c2f1cd..1bcde834 100644 --- a/hub/imports/energy_systems/montreal_future_energy_systems_parameters.py +++ b/hub/imports/energy_systems/montreal_future_energy_systems_parameters.py @@ -160,6 +160,7 @@ class MontrealFutureEnergySystemParameters: _generic_storage_system.height = storage_system.height _generic_storage_system.layers = storage_system.layers _generic_storage_system.storage_medium = storage_system.storage_medium + _generic_storage_system.heating_coil_capacity = storage_system.heating_coil_capacity _storage_systems.append(_generic_storage_system) _generation_system.energy_storage_systems = _storage_systems if archetype_generation_system.domestic_hot_water: diff --git a/main.py b/main.py index 6fb66a5e..e69de29b 100644 --- a/main.py +++ b/main.py @@ -1,6 +0,0 @@ - - - - - - diff --git a/scripts/random_assignation.py b/scripts/random_assignation.py index 38831b66..958f269d 100644 --- a/scripts/random_assignation.py +++ b/scripts/random_assignation.py @@ -28,8 +28,8 @@ residential_systems_percentage = {'system 1 gas': 44, 'system 8 gas': 44, 'system 8 electricity': 6} -residential_new_systems_percentage = {'PV+ASHP+GasBoiler+TES': 100, - 'PV+4Pipe+DHW': 0, +residential_new_systems_percentage = {'PV+ASHP+GasBoiler+TES': 0, + 'PV+4Pipe+DHW': 100, 'PV+ASHP+ElectricBoiler+TES': 0, 'PV+GSHP+GasBoiler+TES': 0, 'PV+GSHP+ElectricBoiler+TES': 0, diff --git a/scripts/system_simulation_models/archetype1.py b/scripts/system_simulation_models/archetype1.py index 19af7eed..6e8bc4f1 100644 --- a/scripts/system_simulation_models/archetype1.py +++ b/scripts/system_simulation_models/archetype1.py @@ -128,7 +128,7 @@ class Archetype1: hp = self.hvac_sizing()[0] eer_curve_coefficients = [float(coefficient) for coefficient in hp.cooling_efficiency_curve.coefficients] cooling_efficiency = float(hp.cooling_efficiency) - demand = self._hourly_heating_demand + demand = self._hourly_cooling_demand hp.source_temperature = self._t_out variable_names = ["t_sup_hp", "t_ret", "m", "q_hp", "hp_electricity", "hp_eer"] num_hours = len(demand) diff --git a/scripts/system_simulation_models/archetype13.py b/scripts/system_simulation_models/archetype13.py index 03057cdc..13904273 100644 --- a/scripts/system_simulation_models/archetype13.py +++ b/scripts/system_simulation_models/archetype13.py @@ -1,49 +1,67 @@ import math - import hub.helpers.constants as cte - - +import csv +from hub.helpers.monthly_values import MonthlyValues class Archetype13: def __init__(self, building, output_path): + self._building = building + self._name = building.name self._pv_system = building.energy_systems[0] self._hvac_system = building.energy_systems[1] self._dhw_system = building.energy_systems[-1] + self._dhw_peak_flow_rate = (building.thermal_zones_from_internal_zones[0].total_floor_area * + building.thermal_zones_from_internal_zones[0].domestic_hot_water.peak_flow * + cte.WATER_DENSITY) self._heating_peak_load = building.heating_peak_load[cte.YEAR][0] self._cooling_peak_load = building.cooling_peak_load[cte.YEAR][0] self._domestic_hot_water_peak_load = building.domestic_hot_water_peak_load[cte.YEAR][0] self._hourly_heating_demand = [0] + [demand / 3600 for demand in building.heating_demand[cte.HOUR]] self._hourly_cooling_demand = [demand / 3600 for demand in building.cooling_demand[cte.HOUR]] - self._hourly_dhw_demand = building.domestic_hot_water_heat_demand[cte.HOUR] + self._hourly_dhw_demand = [0] + building.domestic_hot_water_heat_demand[cte.HOUR] self._output_path = output_path - self._t_out = building.external_temperature + self._t_out = [0] + building.external_temperature[cte.HOUR] + self.results = {} def hvac_sizing(self): storage_factor = 3 - heat_pump = self._hvac_system.generation_systems[0] - boiler = self._hvac_system.generation_systems[1] - thermal_storage = heat_pump.energy_storage_systems[0] + heat_pump = self._hvac_system.generation_systems[1] + boiler = self._hvac_system.generation_systems[0] + thermal_storage = boiler.energy_storage_systems[0] heat_pump.nominal_heat_output = round(0.5 * self._heating_peak_load / 3600) heat_pump.nominal_cooling_output = round(self._cooling_peak_load / 3600) boiler.nominal_heat_output = round(0.5 * self._heating_peak_load / 3600) thermal_storage.volume = round( - (self._heating_peak_load * storage_factor) / (cte.WATER_HEAT_CAPACITY * cte.WATER_DENSITY * 30)) + (self._heating_peak_load * storage_factor) / (cte.WATER_HEAT_CAPACITY * cte.WATER_DENSITY * 25)) return heat_pump, boiler, thermal_storage - def hvac_simulation(self): + def dhw_sizing(self): + storage_factor = 3 + dhw_hp = self._dhw_system.generation_systems[0] + dhw_hp.nominal_heat_output = 0.7 * self._domestic_hot_water_peak_load + dhw_hp.source_temperature = self._t_out + dhw_tes = dhw_hp.energy_storage_systems[0] + dhw_tes.volume = round( + (self._domestic_hot_water_peak_load * storage_factor * 3600) / (cte.WATER_HEAT_CAPACITY * cte.WATER_DENSITY * 10)) + return dhw_hp, dhw_tes + + def heating_system_simulation(self): hp, boiler, tes = self.hvac_sizing() - if hp.source_medium == cte.AIR: - hp.source_temperature = self._t_out[cte.HOUR] - # Heating System Simulation - variable_names = ["t_sup", "t_tank", "t_ret", "m_ch", "m_dis", "q_hp", "q_boiler", "hp_cop", - "hp_electricity", "boiler_gas", "boiler_consumption", "heating_consumption"] - num_hours = len(self._hourly_heating_demand) + cop_curve_coefficients = [float(coefficient) for coefficient in hp.heat_efficiency_curve.coefficients] + demand = self._hourly_heating_demand + hp.source_temperature = self._t_out + variable_names = ["t_sup_hp", "t_tank", "t_ret", "m_ch", "m_dis", "q_hp", "q_boiler", "hp_cop", + "hp_electricity", "boiler_gas_consumption", "t_sup_boiler", "boiler_energy_consumption", + "heating_consumption"] + num_hours = len(demand) variables = {name: [0] * num_hours for name in variable_names} - (t_sup, t_tank, t_ret, m_ch, m_dis, q_hp, q_boiler, hp_cop, - hp_electricity, boiler_gas, boiler_consumption, heating_consumption) = [variables[name] for name in variable_names] - t_tank[0] = 30 + (t_sup_hp, t_tank, t_ret, m_ch, m_dis, q_hp, q_boiler, hp_cop, + hp_electricity, boiler_gas_consumption, t_sup_boiler, boiler_energy_consumption, heating_consumption) = \ + [variables[name] for name in variable_names] + t_tank[0] = 55 dt = 3600 hp_heating_cap = hp.nominal_heat_output - hp_efficiency = float(hp.heat_efficiency) + boiler_heating_cap = boiler.nominal_heat_output + hp_delta_t = 5 boiler_efficiency = float(boiler.heat_efficiency) v, h = float(tes.volume), float(tes.height) r_tot = sum(float(layer.thickness) / float(layer.material.conductivity) for layer in @@ -53,48 +71,269 @@ class Archetype13: a_side = math.pi * d * h a_top = math.pi * d ** 2 / 4 ua = u_tot * (2 * a_top + a_side) - for i in range(len(self._hourly_heating_demand) - 1): + # storage temperature prediction + for i in range(len(demand) - 1): t_tank[i + 1] = (t_tank[i] + - ((m_ch[i] * (t_sup[i] - t_tank[i])) + - (ua * (self._t_out[i] - t_tank[i] + 5)) / cte.WATER_HEAT_CAPACITY - + (m_ch[i] * (t_sup_boiler[i] - t_tank[i]) + + (ua * (self._t_out[i] - t_tank[i])) / cte.WATER_HEAT_CAPACITY - m_dis[i] * (t_tank[i] - t_ret[i])) * (dt / (cte.WATER_DENSITY * v))) + # hp operation if t_tank[i + 1] < 40: q_hp[i + 1] = hp_heating_cap - m_ch[i + 1] = q_hp[i + 1] / (cte.WATER_HEAT_CAPACITY * 7) - t_sup[i + 1] = (q_hp[i + 1] / (m_ch[i + 1] * cte.WATER_HEAT_CAPACITY)) + t_tank[i + 1] - elif 45 <= t_tank[i + 1] < 55 and q_hp[i] == 0: + m_ch[i + 1] = q_hp[i + 1] / (cte.WATER_HEAT_CAPACITY * hp_delta_t) + t_sup_hp[i + 1] = (q_hp[i + 1] / (m_ch[i + 1] * cte.WATER_HEAT_CAPACITY)) + t_tank[i + 1] + elif 40 <= t_tank[i + 1] < 55 and q_hp[i] == 0: q_hp[i + 1] = 0 m_ch[i + 1] = 0 - t_sup[i + 1] = t_tank[i + 1] - elif 45 <= t_tank[i + 1] < 55 and q_hp[i] > 0: + t_sup_hp[i + 1] = t_tank[i + 1] + elif 40 <= t_tank[i + 1] < 55 and q_hp[i] > 0: q_hp[i + 1] = hp_heating_cap - m_ch[i + 1] = q_hp[i + 1] / (cte.WATER_HEAT_CAPACITY * 3) - t_sup[i + 1] = (q_hp[i + 1] / (m_ch[i + 1] * cte.WATER_HEAT_CAPACITY)) + t_tank[i + 1] + m_ch[i + 1] = q_hp[i + 1] / (cte.WATER_HEAT_CAPACITY * hp_delta_t) + t_sup_hp[i + 1] = (q_hp[i + 1] / (m_ch[i + 1] * cte.WATER_HEAT_CAPACITY)) + t_tank[i + 1] else: - q_hp[i + 1], m_ch[i + 1], t_sup[i + 1] = 0, 0, t_tank[i + 1] - - hp_electricity[i + 1] = q_hp[i + 1] / hp_efficiency - if self._hourly_heating_demand[i + 1] == 0: - m_dis[i + 1], t_return, t_ret[i + 1] = 0, t_tank[i + 1], t_tank[i + 1] + q_hp[i + 1], m_ch[i + 1], t_sup_hp[i + 1] = 0, 0, t_tank[i + 1] + t_sup_hp_fahrenheit = 1.8 * t_sup_hp[i + 1] + 32 + t_out_fahrenheit = 1.8 * self._t_out[i + 1] + 32 + if q_hp[i + 1] > 0: + hp_cop[i + 1] = (cop_curve_coefficients[0] + + cop_curve_coefficients[1] * t_sup_hp_fahrenheit + + cop_curve_coefficients[2] * t_sup_hp_fahrenheit ** 2 + + cop_curve_coefficients[3] * t_out_fahrenheit + + cop_curve_coefficients[4] * t_out_fahrenheit ** 2 + + cop_curve_coefficients[5] * t_sup_hp_fahrenheit * t_out_fahrenheit) + hp_electricity[i + 1] = q_hp[i + 1] / hp_cop[i + 1] else: - if self._hourly_heating_demand[i + 1] > 0.5 * self._heating_peak_load: + hp_cop[i + 1] = 0 + hp_electricity[i + 1] = 0 + # boiler operation + if q_hp[i + 1] > 0: + if t_sup_hp[i + 1] < 45: + q_boiler[i + 1] = boiler_heating_cap + elif demand[i + 1] > 0.5 * self._heating_peak_load / dt: + q_boiler[i + 1] = 0.5 * boiler_heating_cap + boiler_energy_consumption[i + 1] = q_boiler[i + 1] / boiler_efficiency + boiler_gas_consumption[i + 1] = (q_boiler[i + 1] * dt) / (boiler_efficiency * cte.NATURAL_GAS_LHV) + t_sup_boiler[i + 1] = t_sup_hp[i + 1] + (q_boiler[i + 1] / (m_ch[i + 1] * cte.WATER_HEAT_CAPACITY)) + # storage discharging + if demand[i + 1] == 0: + m_dis[i + 1] = 0 + t_ret[i + 1] = t_tank[i + 1] + else: + if demand[i + 1] > 0.5 * self._heating_peak_load / dt: factor = 8 else: factor = 4 - m_dis[i + 1] = self._heating_peak_load / (cte.WATER_HEAT_CAPACITY * factor * 3600) - t_return = t_tank[i + 1] - self._hourly_heating_demand[i + 1] / (m_dis[i + 1] * cte.WATER_HEAT_CAPACITY) - if m_dis[i + 1] == 0 or (m_dis[i + 1] > 0 and t_return < 25): - t_ret[i + 1] = max(25, t_tank[i + 1]) + m_dis[i + 1] = self._heating_peak_load / (cte.WATER_HEAT_CAPACITY * factor * dt) + t_ret[i + 1] = t_tank[i + 1] - demand[i + 1] / (m_dis[i + 1] * cte.WATER_HEAT_CAPACITY) + + hp.energy_consumption[cte.HEATING] = {} + hp.energy_consumption[cte.HEATING][cte.HOUR] = hp_electricity + hp.energy_consumption[cte.HEATING][cte.MONTH] = MonthlyValues.get_total_month( + hp.energy_consumption[cte.HEATING][cte.HOUR]) + hp.energy_consumption[cte.HEATING][cte.YEAR] = [ + sum(hp.energy_consumption[cte.HEATING][cte.MONTH])] + boiler.energy_consumption[cte.HEATING] = {} + boiler.energy_consumption[cte.HEATING][cte.HOUR] = boiler_energy_consumption + boiler.energy_consumption[cte.HEATING][cte.MONTH] = MonthlyValues.get_total_month( + boiler.energy_consumption[cte.HEATING][cte.HOUR]) + boiler.energy_consumption[cte.HEATING][cte.YEAR] = [ + sum(boiler.energy_consumption[cte.HEATING][cte.MONTH])] + tes.temperature = t_tank + self.results['Heating Demand (W)'] = demand + self.results['HP Heat Output (W)'] = q_hp + self.results['HP Source Temperature'] = self._t_out + self.results['HP Supply Temperature'] = t_sup_hp + self.results['HP COP'] = hp_cop + self.results['HP Electricity Consumption (W)'] = hp_electricity + self.results['Boiler Heat Output (W)'] = q_boiler + self.results['Boiler Supply Temperature'] = t_sup_boiler + self.results['Boiler Gas Consumption'] = boiler_gas_consumption + self.results['TES Temperature'] = t_tank + self.results['TES Charging Flow Rate (kg/s)'] = m_ch + self.results['TES Discharge Flow Rate (kg/s)'] = m_dis + self.results['Heating Loop Return Temperature'] = t_ret + return hp_electricity, boiler_energy_consumption + + def cooling_system_simulation(self): + hp = self.hvac_sizing()[0] + eer_curve_coefficients = [float(coefficient) for coefficient in hp.cooling_efficiency_curve.coefficients] + cooling_efficiency = float(hp.cooling_efficiency) + demand = self._hourly_cooling_demand + hp.source_temperature = self._t_out + variable_names = ["t_sup_hp", "t_ret", "m", "q_hp", "hp_electricity", "hp_eer"] + num_hours = len(demand) + variables = {name: [0] * num_hours for name in variable_names} + (t_sup_hp, t_ret, m, q_hp, hp_electricity, hp_eer) = [variables[name] for name in variable_names] + t_ret[0] = 13 + dt = 3600 + for i in range(len(demand) - 1): + if demand[i] > 0: + m[i] = self._cooling_peak_load / (cte.WATER_HEAT_CAPACITY * 5 * dt) + if t_ret[i] > 13: + if demand[i] < 0.25 * self._cooling_peak_load / dt: + q_hp[i] = 0.25 * hp.nominal_cooling_output + elif demand[i] < 0.5 * self._cooling_peak_load / dt: + q_hp[i] = 0.5 * hp.nominal_cooling_output + else: + q_hp[i] = hp.nominal_cooling_output + t_sup_hp[i] = t_ret[i] - q_hp[i] / (m[i] * cte.WATER_HEAT_CAPACITY) else: - t_ret[i + 1] = t_tank[i + 1] - self._hourly_heating_demand[i + 1] / (m_dis[i + 1] * cte.WATER_HEAT_CAPACITY * 3600) - tes_output = m_dis[i + 1] * cte.WATER_HEAT_CAPACITY * (t_tank[i + 1] - t_ret[i + 1]) - if tes_output < (self._hourly_heating_demand[i + 1] / 3600): - q_boiler[i + 1] = (self._hourly_heating_demand[i + 1] / 3600) - tes_output - boiler_gas[i + 1] = (q_boiler[i + 1] * dt) / 50e6 - boiler_consumption[i + 1] = q_boiler[i + 1] / boiler_efficiency - heating_consumption[i + 1] = boiler_consumption[i + 1] + hp_electricity[i + 1] - data = list(zip(t_tank, t_sup, t_ret, m_ch, m_dis, q_hp, hp_electricity, boiler_gas, q_boiler, - self._hourly_heating_demand)) + q_hp[i] = 0 + t_sup_hp[i] = t_ret[i] + t_ret[i + 1] = t_sup_hp[i] + demand[i] / (m[i] * cte.WATER_HEAT_CAPACITY) + else: + m[i] = 0 + q_hp[i] = 0 + t_sup_hp[i] = t_ret[i] + t_ret[i + 1] = t_ret[i] + t_sup_hp_fahrenheit = 1.8 * t_sup_hp[i] + 32 + t_out_fahrenheit = 1.8 * self._t_out[i] + 32 + if q_hp[i] > 0: + hp_eer[i] = (eer_curve_coefficients[0] + + eer_curve_coefficients[1] * t_sup_hp_fahrenheit + + eer_curve_coefficients[2] * t_sup_hp_fahrenheit ** 2 + + eer_curve_coefficients[3] * t_out_fahrenheit + + eer_curve_coefficients[4] * t_out_fahrenheit ** 2 + + eer_curve_coefficients[5] * t_sup_hp_fahrenheit * t_out_fahrenheit) + hp_electricity[i] = q_hp[i] / cooling_efficiency + else: + hp_eer[i] = 0 + hp_electricity[i] = 0 + hp.energy_consumption[cte.COOLING] = {} + hp.energy_consumption[cte.COOLING][cte.HOUR] = hp_electricity + hp.energy_consumption[cte.COOLING][cte.MONTH] = MonthlyValues.get_total_month( + hp.energy_consumption[cte.COOLING][cte.HOUR]) + hp.energy_consumption[cte.COOLING][cte.YEAR] = [ + sum(hp.energy_consumption[cte.COOLING][cte.MONTH])] + self.results['Cooling Demand (W)'] = demand + self.results['HP Cooling Output (W)'] = q_hp + self.results['HP Cooling Supply Temperature'] = t_sup_hp + self.results['HP Cooling COP'] = hp_eer + self.results['HP Electricity Consumption'] = hp_electricity + self.results['Cooling Loop Flow Rate (kg/s)'] = m + self.results['Cooling Loop Return Temperature'] = t_ret + return hp_electricity + + def dhw_system_simulation(self): + hp, tes = self.dhw_sizing() + cop_curve_coefficients = [float(coefficient) for coefficient in hp.heat_efficiency_curve.coefficients] + demand = self._hourly_dhw_demand + variable_names = ["t_sup_hp", "t_tank", "m_ch", "m_dis", "q_hp", "q_coil", "hp_cop", + "hp_electricity", "available hot water (m3)", "refill flow rate (kg/s)"] + num_hours = len(demand) + variables = {name: [0] * num_hours for name in variable_names} + (t_sup_hp, t_tank, m_ch, m_dis, m_refill, q_hp, q_coil, hp_cop, hp_electricity, v_dhw) = \ + [variables[name] for name in variable_names] + t_tank[0] = 70 + v_dhw[0] = tes.volume + dt = 3600 + hp_heating_cap = hp.nominal_heat_output + hp_delta_t = 8 + v, h = float(tes.volume), float(tes.height) + r_tot = sum(float(layer.thickness) / float(layer.material.conductivity) for layer in + tes.layers) + u_tot = 1 / r_tot + d = math.sqrt((4 * v) / (math.pi * h)) + a_side = math.pi * d * h + a_top = math.pi * d ** 2 / 4 + ua = u_tot * (2 * a_top + a_side) + freshwater_temperature = 18 + for i in range(len(demand) - 1): + delta_t_demand = demand[i] * (dt / (cte.WATER_DENSITY * cte.WATER_HEAT_CAPACITY * v)) + if t_tank[i] < 65: + q_hp[i] = hp_heating_cap + delta_t_hp = q_hp[i] * (dt / (cte.WATER_DENSITY * cte.WATER_HEAT_CAPACITY * v)) + if demand[i] > 0: + dhw_needed = (demand[i] * cte.HOUR_TO_SECONDS) / (cte.WATER_HEAT_CAPACITY * t_tank[i] * cte.WATER_DENSITY) + m_dis[i] = dhw_needed * cte.WATER_DENSITY / cte.HOUR_TO_SECONDS + m_refill[i] = m_dis[i] + delta_t_freshwater = m_refill[i] * (t_tank[i] - freshwater_temperature) * (dt / (v * cte.WATER_DENSITY)) + diff = delta_t_freshwater + delta_t_demand - delta_t_hp + if diff > 0: + if diff > 0: + power = diff * (cte.WATER_DENSITY * cte.WATER_HEAT_CAPACITY * v) / dt + if power <= float(tes.heating_coil_capacity): + q_coil[i] = power + else: + q_coil[i] = float(tes.heating_coil_capacity) + elif t_tank[i] < 65: + q_coil[i] = float(tes.heating_coil_capacity) + delta_t_coil = q_coil[i] * (dt / (cte.WATER_DENSITY * cte.WATER_HEAT_CAPACITY * v)) + + if q_hp[i] > 0: + m_ch[i] = q_hp[i] / (cte.WATER_HEAT_CAPACITY * hp_delta_t) + t_sup_hp[i] = (q_hp[i] / (m_ch[i] * cte.WATER_HEAT_CAPACITY)) + t_tank[i] + else: + m_ch[i] = 0 + t_sup_hp[i] = t_tank[i] + t_sup_hp_fahrenheit = 1.8 * t_sup_hp[i] + 32 + t_out_fahrenheit = 1.8 * self._t_out[i] + 32 + if q_hp[i] > 0: + hp_cop[i] = (cop_curve_coefficients[0] + + cop_curve_coefficients[1] * t_sup_hp_fahrenheit + + cop_curve_coefficients[2] * t_sup_hp_fahrenheit ** 2 + + cop_curve_coefficients[3] * t_out_fahrenheit + + cop_curve_coefficients[4] * t_out_fahrenheit ** 2 + + cop_curve_coefficients[5] * t_sup_hp_fahrenheit * t_out_fahrenheit) + hp_electricity[i] = q_hp[i] / 3.5 + else: + hp_cop[i] = 0 + hp_electricity[i] = 0 + + t_tank[i + 1] = t_tank[i] + (delta_t_hp - delta_t_freshwater - delta_t_demand + delta_t_coil) + + hp.energy_consumption[cte.DOMESTIC_HOT_WATER] = {} + hp.energy_consumption[cte.DOMESTIC_HOT_WATER][cte.HOUR] = hp_electricity + hp.energy_consumption[cte.DOMESTIC_HOT_WATER][cte.MONTH] = MonthlyValues.get_total_month( + hp.energy_consumption[cte.DOMESTIC_HOT_WATER][cte.HOUR]) + hp.energy_consumption[cte.DOMESTIC_HOT_WATER][cte.YEAR] = [ + sum(hp.energy_consumption[cte.DOMESTIC_HOT_WATER][cte.MONTH])] + tes.heating_coil_energy_consumption = {} + tes.heating_coil_energy_consumption[cte.HOUR] = q_coil + tes.heating_coil_energy_consumption[cte.MONTH] = MonthlyValues.get_total_month( + tes.heating_coil_energy_consumption[cte.HOUR]) + tes.heating_coil_energy_consumption[cte.YEAR] = [ + sum(tes.heating_coil_energy_consumption[cte.MONTH])] + tes.temperature = t_tank + + + self.results['DHW Demand (W)'] = demand + self.results['DHW HP Heat Output (W)'] = q_hp + self.results['DHW HP Electricity Consumption (W)'] = hp_electricity + self.results['DHW HP Source Temperature'] = self._t_out + self.results['DHW HP Supply Temperature'] = t_sup_hp + self.results['DHW HP COP'] = hp_cop + self.results['DHW TES Heating Coil Heat Output (W)'] = q_coil + self.results['DHW TES Temperature'] = t_tank + self.results['DHW TES Charging Flow Rate (kg/s)'] = m_ch + self.results['DHW Flow Rate (kg/s)'] = m_dis + self.results['DHW TES Refill Flow Rate (kg/s)'] = m_refill + self.results['Available Water in Tank (m3)'] = v_dhw + return hp_electricity, q_coil def enrich_buildings(self): - self.hvac_sizing() + hp_heating, boiler_consumption = self.heating_system_simulation() + hp_cooling = self.cooling_system_simulation() + hp_dhw, heating_coil = self.dhw_system_simulation() + heating_consumption = [hp_heating[i] + boiler_consumption[i] for i in range(len(hp_heating))] + dhw_consumption = [hp_dhw[i] + heating_coil[i] for i in range(len(hp_dhw))] + self._building.heating_consumption[cte.HOUR] = heating_consumption + self._building.heating_consumption[cte.MONTH] = ( + MonthlyValues.get_total_month(self._building.heating_consumption[cte.HOUR])) + self._building.heating_consumption[cte.YEAR] = sum(self._building.heating_consumption[cte.MONTH]) + self._building.cooling_consumption[cte.HOUR] = hp_cooling + self._building.cooling_consumption[cte.MONTH] = ( + MonthlyValues.get_total_month(self._building.cooling_consumption[cte.HOUR])) + self._building.cooling_consumption[cte.YEAR] = sum(self._building.cooling_consumption[cte.MONTH]) + self._building.domestic_hot_water_consumption[cte.HOUR] = dhw_consumption + self._building.domestic_hot_water_consumption[cte.MONTH] = ( + MonthlyValues.get_total_month(self._building.domestic_hot_water_consumption[cte.HOUR])) + self._building.domestic_hot_water_consumption[cte.YEAR] = ( + sum(self._building.domestic_hot_water_consumption[cte.MONTH])) + file_name = f'energy_system_simulation_results_{self._name}.csv' + with open(self._output_path / file_name, 'w', newline='') as csvfile: + output_file = csv.writer(csvfile) + # Write header + output_file.writerow(self.results.keys()) + # Write data + output_file.writerows(zip(*self.results.values())) From ee0b985245d14ac301ac073fcb6b458204ef685d Mon Sep 17 00:00:00 2001 From: s_ranjbar Date: Thu, 13 Jun 2024 15:41:20 -0400 Subject: [PATCH 05/15] feat: stratified model for tes added, all the demand and consumptions are converted to J, cold water temperature fixed --- hub/city_model_structure/building.py | 4 +- hub/helpers/constants.py | 1 + hub/imports/results/ep_multiple_buildings.py | 9 +- hub/imports/weather/epw_weather_parameters.py | 4 +- hub/imports/weather/helpers/weather.py | 32 +- main.py | 45 ++ ...gy_system_sizing_and_simulation_factory.py | 1 + .../system_simulation_models/archetype13.py | 155 ++++--- .../archetype13_stratified_tes.py | 392 ++++++++++++++++++ 9 files changed, 563 insertions(+), 80 deletions(-) create mode 100644 scripts/system_simulation_models/archetype13_stratified_tes.py diff --git a/hub/city_model_structure/building.py b/hub/city_model_structure/building.py index b928535b..660f0f7e 100644 --- a/hub/city_model_structure/building.py +++ b/hub/city_model_structure/building.py @@ -896,13 +896,13 @@ class Building(CityObject): for energy_system in energy_systems: if cte.COOLING in energy_system.demand_types and cte.COOLING not in fuel_breakdown[key]: for generation_system in energy_system.generation_systems: - fuel_breakdown[generation_system.fuel_type][cte.COOLING] = self.cooling_consumption[cte.YEAR][0] / 3600 + fuel_breakdown[generation_system.fuel_type][cte.COOLING] = self.cooling_consumption[cte.YEAR][0] for fuel in heating_fuels: if cte.HEATING not in fuel_breakdown[fuel]: for energy_system in energy_systems: if cte.HEATING in energy_system.demand_types: for generation_system in energy_system.generation_systems: - fuel_breakdown[generation_system.fuel_type][cte.HEATING] = self.heating_consumption[cte.YEAR][0] / 3600 + fuel_breakdown[generation_system.fuel_type][cte.HEATING] = self.heating_consumption[cte.YEAR][0] for fuel in dhw_fuels: if cte.DOMESTIC_HOT_WATER not in fuel_breakdown[fuel]: for energy_system in energy_systems: diff --git a/hub/helpers/constants.py b/hub/helpers/constants.py index af22e94d..ad32c835 100644 --- a/hub/helpers/constants.py +++ b/hub/helpers/constants.py @@ -10,6 +10,7 @@ Project Coder Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca KELVIN = 273.15 WATER_DENSITY = 1000 # kg/m3 WATER_HEAT_CAPACITY = 4182 # J/kgK +WATER_THERMAL_CONDUCTIVITY = 0.65 # W/mK NATURAL_GAS_LHV = 36.6e6 # J/m3 AIR_DENSITY = 1.293 # kg/m3 AIR_HEAT_CAPACITY = 1005.2 # J/kgK diff --git a/hub/imports/results/ep_multiple_buildings.py b/hub/imports/results/ep_multiple_buildings.py index 9da12c7a..bd9a2dd3 100644 --- a/hub/imports/results/ep_multiple_buildings.py +++ b/hub/imports/results/ep_multiple_buildings.py @@ -60,9 +60,12 @@ class EnergyPlusMultipleBuildings: for building in self._city.buildings: building.heating_demand[cte.HOUR] = building_energy_demands[f'Building {building.name} Heating Demand (J)'] building.cooling_demand[cte.HOUR] = building_energy_demands[f'Building {building.name} Cooling Demand (J)'] - building.domestic_hot_water_heat_demand[cte.HOUR] = building_energy_demands[f'Building {building.name} DHW Demand (W)'] - building.appliances_electrical_demand[cte.HOUR] = building_energy_demands[f'Building {building.name} Appliances (W)'] - building.lighting_electrical_demand[cte.HOUR] = building_energy_demands[f'Building {building.name} Lighting (W)'] + building.domestic_hot_water_heat_demand[cte.HOUR] = \ + [x * cte.WATTS_HOUR_TO_JULES for x in building_energy_demands[f'Building {building.name} DHW Demand (W)']] + building.appliances_electrical_demand[cte.HOUR] = \ + [x * cte.WATTS_HOUR_TO_JULES for x in building_energy_demands[f'Building {building.name} Appliances (W)']] + building.lighting_electrical_demand[cte.HOUR] = \ + [x * cte.WATTS_HOUR_TO_JULES for x in building_energy_demands[f'Building {building.name} Lighting (W)']] building.heating_demand[cte.MONTH] = MonthlyValues.get_total_month(building.heating_demand[cte.HOUR]) building.cooling_demand[cte.MONTH] = MonthlyValues.get_total_month(building.cooling_demand[cte.HOUR]) building.domestic_hot_water_heat_demand[cte.MONTH] = ( diff --git a/hub/imports/weather/epw_weather_parameters.py b/hub/imports/weather/epw_weather_parameters.py index d8f3a002..0c362261 100644 --- a/hub/imports/weather/epw_weather_parameters.py +++ b/hub/imports/weather/epw_weather_parameters.py @@ -126,10 +126,10 @@ class EpwWeatherParameters: for building in self._city.buildings: building.external_temperature[cte.MONTH] = \ MonthlyValues().get_mean_values(building.external_temperature[cte.HOUR]) - building.external_temperature[cte.YEAR] = [sum(building.external_temperature[cte.HOUR]) / 9870] + building.external_temperature[cte.YEAR] = [sum(building.external_temperature[cte.HOUR]) / 8760] building.cold_water_temperature[cte.MONTH] = \ MonthlyValues().get_mean_values(building.cold_water_temperature[cte.HOUR]) - building.cold_water_temperature[cte.YEAR] = [sum(building.cold_water_temperature[cte.HOUR]) / 9870] + building.cold_water_temperature[cte.YEAR] = [sum(building.cold_water_temperature[cte.HOUR]) / 8760] # If the usage has already being imported, the domestic hot water missing values must be calculated here that # the cold water temperature is finally known diff --git a/hub/imports/weather/helpers/weather.py b/hub/imports/weather/helpers/weather.py index 7603cb5b..755f9ad3 100644 --- a/hub/imports/weather/helpers/weather.py +++ b/hub/imports/weather/helpers/weather.py @@ -8,7 +8,7 @@ Project Coder Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca import logging import math import hub.helpers.constants as cte - +from datetime import datetime, timedelta class Weather: """ @@ -55,25 +55,19 @@ class Weather: # and Craig Christensen, National Renewable Energy Laboratory # ambient temperatures( in °C) # cold water temperatures( in °C) - ambient_temperature_fahrenheit = [] - average_temperature = 0 - maximum_temperature = -1000 - minimum_temperature = 1000 - for temperature in ambient_temperature: - value = temperature * 9 / 5 + 32 - ambient_temperature_fahrenheit.append(value) - average_temperature += value / 8760 - if value > maximum_temperature: - maximum_temperature = value - if value < minimum_temperature: - minimum_temperature = value - delta_temperature = maximum_temperature - minimum_temperature - ratio = 0.4 + 0.01 * (average_temperature - 44) - lag = 35 - 1 * (average_temperature - 44) + t_out_fahrenheit = [1.8 * t_out + 32 for t_out in ambient_temperature] + t_out_average = sum(t_out_fahrenheit) / len(t_out_fahrenheit) + max_difference = max(t_out_fahrenheit) - min(t_out_fahrenheit) + ratio = 0.4 + 0.01 * (t_out_average - 44) + lag = 35 - (t_out_average - 35) + number_of_day = [a for a in range(1, 366)] + day_of_year = [day for day in number_of_day for _ in range(24)] + cold_temperature_fahrenheit = [] cold_temperature = [] - for temperature in ambient_temperature_fahrenheit: - radians = (0.986 * (temperature-15-lag) - 90) * math.pi / 180 - cold_temperature.append((average_temperature + 6 + ratio * (delta_temperature/2) * math.sin(radians) - 32) * 5/9) + for i in range(len(ambient_temperature)): + cold_temperature_fahrenheit.append(t_out_average + 6 + ratio * (max_difference / 2) * + math.sin(math.radians(0.986 * (day_of_year[i] - 15 - lag) - 90))) + cold_temperature.append((cold_temperature_fahrenheit[i] - 32) / 1.8) return cold_temperature def epw_file(self, region_code): diff --git a/main.py b/main.py index e69de29b..798b5d4c 100644 --- a/main.py +++ b/main.py @@ -0,0 +1,45 @@ +from scripts.geojson_creator import process_geojson +from pathlib import Path +import subprocess +from scripts.ep_run_enrich import energy_plus_workflow +from hub.imports.geometry_factory import GeometryFactory +from hub.helpers.dictionaries import Dictionaries +from hub.imports.construction_factory import ConstructionFactory +from hub.imports.usage_factory import UsageFactory +from hub.imports.weather_factory import WeatherFactory +from hub.imports.results_factory import ResultFactory +from scripts.energy_system_analysis_report import EnergySystemAnalysisReport +from scripts import random_assignation +from hub.imports.energy_systems_factory import EnergySystemsFactory +from scripts.energy_system_sizing import SystemSizing +from scripts.energy_system_retrofit_results import system_results, new_system_results +from scripts.energy_system_sizing_and_simulation_factory import EnergySystemsSimulationFactory +from scripts.costs.cost import Cost +from scripts.costs.constants import SKIN_RETROFIT_AND_SYSTEM_RETROFIT_AND_PV, SYSTEM_RETROFIT_AND_PV +import hub.helpers.constants as cte +from hub.exports.exports_factory import ExportsFactory +# Specify the GeoJSON file path +geojson_file = process_geojson(x=-73.5681295982132, y=45.49218262677643, diff=0.0001) +file_path = (Path(__file__).parent / 'input_files' / 'output_buildings.geojson') +# Specify the output path for the PDF file +output_path = (Path(__file__).parent / 'out_files').resolve() +# Create city object from GeoJSON file +city = GeometryFactory('geojson', + path=file_path, + height_field='height', + year_of_construction_field='year_of_construction', + function_field='function', + function_to_hub=Dictionaries().montreal_function_to_hub_function).city +# Enrich city data +ConstructionFactory('nrcan', city).enrich() + +UsageFactory('nrcan', city).enrich() +WeatherFactory('epw', city).enrich() +energy_plus_workflow(city) +random_assignation.call_random(city.buildings, random_assignation.residential_new_systems_percentage) +EnergySystemsFactory('montreal_future', city).enrich() + +for building in city.buildings: + EnergySystemsSimulationFactory('archetype13', building=building, output_path=output_path).enrich() + +print('test') diff --git a/scripts/energy_system_sizing_and_simulation_factory.py b/scripts/energy_system_sizing_and_simulation_factory.py index 95a4d4e8..b0526e2c 100644 --- a/scripts/energy_system_sizing_and_simulation_factory.py +++ b/scripts/energy_system_sizing_and_simulation_factory.py @@ -6,6 +6,7 @@ Project Coder Saeed Ranjbar saeed.ranjbar@mail.concordia.ca """ from scripts.system_simulation_models.archetype13 import Archetype13 +from scripts.system_simulation_models.archetype13_stratified_tes import Archetype13Stratified from scripts.system_simulation_models.archetype1 import Archetype1 diff --git a/scripts/system_simulation_models/archetype13.py b/scripts/system_simulation_models/archetype13.py index 13904273..786115db 100644 --- a/scripts/system_simulation_models/archetype13.py +++ b/scripts/system_simulation_models/archetype13.py @@ -2,6 +2,8 @@ import math import hub.helpers.constants as cte import csv from hub.helpers.monthly_values import MonthlyValues + + class Archetype13: def __init__(self, building, output_path): self._building = building @@ -15,12 +17,13 @@ class Archetype13: self._heating_peak_load = building.heating_peak_load[cte.YEAR][0] self._cooling_peak_load = building.cooling_peak_load[cte.YEAR][0] self._domestic_hot_water_peak_load = building.domestic_hot_water_peak_load[cte.YEAR][0] - self._hourly_heating_demand = [0] + [demand / 3600 for demand in building.heating_demand[cte.HOUR]] - self._hourly_cooling_demand = [demand / 3600 for demand in building.cooling_demand[cte.HOUR]] - self._hourly_dhw_demand = [0] + building.domestic_hot_water_heat_demand[cte.HOUR] + self._hourly_heating_demand = [demand / cte.HOUR_TO_SECONDS for demand in building.heating_demand[cte.HOUR]] + self._hourly_cooling_demand = [demand / cte.HOUR_TO_SECONDS for demand in building.cooling_demand[cte.HOUR]] + self._hourly_dhw_demand = building.domestic_hot_water_heat_demand[cte.HOUR] self._output_path = output_path - self._t_out = [0] + building.external_temperature[cte.HOUR] + self._t_out = building.external_temperature[cte.HOUR] self.results = {} + self.dt = 900 def hvac_sizing(self): storage_factor = 3 @@ -47,7 +50,9 @@ class Archetype13: def heating_system_simulation(self): hp, boiler, tes = self.hvac_sizing() cop_curve_coefficients = [float(coefficient) for coefficient in hp.heat_efficiency_curve.coefficients] - demand = self._hourly_heating_demand + number_of_ts = int(cte.HOUR_TO_SECONDS / self.dt) + demand = [0] + [x for x in self._hourly_heating_demand for _ in range(number_of_ts)] + t_out = [0] + [x for x in self._t_out for _ in range(number_of_ts)] hp.source_temperature = self._t_out variable_names = ["t_sup_hp", "t_tank", "t_ret", "m_ch", "m_dis", "q_hp", "q_boiler", "hp_cop", "hp_electricity", "boiler_gas_consumption", "t_sup_boiler", "boiler_energy_consumption", @@ -56,9 +61,8 @@ class Archetype13: variables = {name: [0] * num_hours for name in variable_names} (t_sup_hp, t_tank, t_ret, m_ch, m_dis, q_hp, q_boiler, hp_cop, hp_electricity, boiler_gas_consumption, t_sup_boiler, boiler_energy_consumption, heating_consumption) = \ - [variables[name] for name in variable_names] + [variables[name] for name in variable_names] t_tank[0] = 55 - dt = 3600 hp_heating_cap = hp.nominal_heat_output boiler_heating_cap = boiler.nominal_heat_output hp_delta_t = 5 @@ -75,8 +79,8 @@ class Archetype13: for i in range(len(demand) - 1): t_tank[i + 1] = (t_tank[i] + (m_ch[i] * (t_sup_boiler[i] - t_tank[i]) + - (ua * (self._t_out[i] - t_tank[i])) / cte.WATER_HEAT_CAPACITY - - m_dis[i] * (t_tank[i] - t_ret[i])) * (dt / (cte.WATER_DENSITY * v))) + (ua * (t_out[i] - t_tank[i])) / cte.WATER_HEAT_CAPACITY - + m_dis[i] * (t_tank[i] - t_ret[i])) * (self.dt / (cte.WATER_DENSITY * v))) # hp operation if t_tank[i + 1] < 40: q_hp[i + 1] = hp_heating_cap @@ -93,7 +97,7 @@ class Archetype13: else: q_hp[i + 1], m_ch[i + 1], t_sup_hp[i + 1] = 0, 0, t_tank[i + 1] t_sup_hp_fahrenheit = 1.8 * t_sup_hp[i + 1] + 32 - t_out_fahrenheit = 1.8 * self._t_out[i + 1] + 32 + t_out_fahrenheit = 1.8 * t_out[i + 1] + 32 if q_hp[i + 1] > 0: hp_cop[i + 1] = (cop_curve_coefficients[0] + cop_curve_coefficients[1] * t_sup_hp_fahrenheit + @@ -109,39 +113,54 @@ class Archetype13: if q_hp[i + 1] > 0: if t_sup_hp[i + 1] < 45: q_boiler[i + 1] = boiler_heating_cap - elif demand[i + 1] > 0.5 * self._heating_peak_load / dt: + elif demand[i + 1] > 0.5 * self._heating_peak_load / self.dt: q_boiler[i + 1] = 0.5 * boiler_heating_cap boiler_energy_consumption[i + 1] = q_boiler[i + 1] / boiler_efficiency - boiler_gas_consumption[i + 1] = (q_boiler[i + 1] * dt) / (boiler_efficiency * cte.NATURAL_GAS_LHV) + boiler_gas_consumption[i + 1] = (q_boiler[i + 1] * self.dt) / (boiler_efficiency * cte.NATURAL_GAS_LHV) t_sup_boiler[i + 1] = t_sup_hp[i + 1] + (q_boiler[i + 1] / (m_ch[i + 1] * cte.WATER_HEAT_CAPACITY)) # storage discharging if demand[i + 1] == 0: m_dis[i + 1] = 0 t_ret[i + 1] = t_tank[i + 1] else: - if demand[i + 1] > 0.5 * self._heating_peak_load / dt: + if demand[i + 1] > 0.5 * self._heating_peak_load / cte.HOUR_TO_SECONDS: factor = 8 else: factor = 4 - m_dis[i + 1] = self._heating_peak_load / (cte.WATER_HEAT_CAPACITY * factor * dt) + m_dis[i + 1] = self._heating_peak_load / (cte.WATER_HEAT_CAPACITY * factor * cte.HOUR_TO_SECONDS) t_ret[i + 1] = t_tank[i + 1] - demand[i + 1] / (m_dis[i + 1] * cte.WATER_HEAT_CAPACITY) - + tes.temperature = [] + hp_electricity_j = [(x * cte.WATTS_HOUR_TO_JULES) / number_of_ts for x in hp_electricity] + boiler_consumption_j = [(x * cte.WATTS_HOUR_TO_JULES) / number_of_ts for x in boiler_energy_consumption] + hp_hourly = [] + boiler_hourly = [] + boiler_sum = 0 + hp_sum = 0 + for i in range(1, len(demand)): + hp_sum += hp_electricity_j[i] + boiler_sum += boiler_consumption_j[i] + if (i - 1) % number_of_ts == 0: + tes.temperature.append(t_tank[i]) + hp_hourly.append(hp_sum) + boiler_hourly.append(boiler_sum) + hp_sum = 0 + boiler_sum = 0 hp.energy_consumption[cte.HEATING] = {} - hp.energy_consumption[cte.HEATING][cte.HOUR] = hp_electricity + hp.energy_consumption[cte.HEATING][cte.HOUR] = hp_hourly hp.energy_consumption[cte.HEATING][cte.MONTH] = MonthlyValues.get_total_month( hp.energy_consumption[cte.HEATING][cte.HOUR]) hp.energy_consumption[cte.HEATING][cte.YEAR] = [ sum(hp.energy_consumption[cte.HEATING][cte.MONTH])] boiler.energy_consumption[cte.HEATING] = {} - boiler.energy_consumption[cte.HEATING][cte.HOUR] = boiler_energy_consumption + boiler.energy_consumption[cte.HEATING][cte.HOUR] = boiler_hourly boiler.energy_consumption[cte.HEATING][cte.MONTH] = MonthlyValues.get_total_month( boiler.energy_consumption[cte.HEATING][cte.HOUR]) boiler.energy_consumption[cte.HEATING][cte.YEAR] = [ sum(boiler.energy_consumption[cte.HEATING][cte.MONTH])] - tes.temperature = t_tank + self.results['Heating Demand (W)'] = demand self.results['HP Heat Output (W)'] = q_hp - self.results['HP Source Temperature'] = self._t_out + self.results['HP Source Temperature'] = t_out self.results['HP Supply Temperature'] = t_sup_hp self.results['HP COP'] = hp_cop self.results['HP Electricity Consumption (W)'] = hp_electricity @@ -152,42 +171,47 @@ class Archetype13: self.results['TES Charging Flow Rate (kg/s)'] = m_ch self.results['TES Discharge Flow Rate (kg/s)'] = m_dis self.results['Heating Loop Return Temperature'] = t_ret - return hp_electricity, boiler_energy_consumption + return hp_hourly, boiler_hourly def cooling_system_simulation(self): hp = self.hvac_sizing()[0] eer_curve_coefficients = [float(coefficient) for coefficient in hp.cooling_efficiency_curve.coefficients] cooling_efficiency = float(hp.cooling_efficiency) - demand = self._hourly_cooling_demand + number_of_ts = int(cte.HOUR_TO_SECONDS / self.dt) + demand = [0] + [x for x in self._hourly_cooling_demand for _ in range(number_of_ts)] + t_out = [0] + [x for x in self._t_out for _ in range(number_of_ts)] hp.source_temperature = self._t_out variable_names = ["t_sup_hp", "t_ret", "m", "q_hp", "hp_electricity", "hp_eer"] num_hours = len(demand) variables = {name: [0] * num_hours for name in variable_names} (t_sup_hp, t_ret, m, q_hp, hp_electricity, hp_eer) = [variables[name] for name in variable_names] t_ret[0] = 13 - dt = 3600 - for i in range(len(demand) - 1): + + for i in range(1, len(demand)): if demand[i] > 0: - m[i] = self._cooling_peak_load / (cte.WATER_HEAT_CAPACITY * 5 * dt) - if t_ret[i] > 13: - if demand[i] < 0.25 * self._cooling_peak_load / dt: + m[i] = self._cooling_peak_load / (cte.WATER_HEAT_CAPACITY * 5 * cte.HOUR_TO_SECONDS) + if t_ret[i - 1] >= 13: + if demand[i] < 0.25 * self._cooling_peak_load / cte.HOUR_TO_SECONDS: q_hp[i] = 0.25 * hp.nominal_cooling_output - elif demand[i] < 0.5 * self._cooling_peak_load / dt: + elif demand[i] < 0.5 * self._cooling_peak_load / cte.HOUR_TO_SECONDS: q_hp[i] = 0.5 * hp.nominal_cooling_output else: q_hp[i] = hp.nominal_cooling_output - t_sup_hp[i] = t_ret[i] - q_hp[i] / (m[i] * cte.WATER_HEAT_CAPACITY) + t_sup_hp[i] = t_ret[i - 1] - q_hp[i] / (m[i] * cte.WATER_HEAT_CAPACITY) else: q_hp[i] = 0 - t_sup_hp[i] = t_ret[i] - t_ret[i + 1] = t_sup_hp[i] + demand[i] / (m[i] * cte.WATER_HEAT_CAPACITY) + t_sup_hp[i] = t_ret[i - 1] + if m[i] == 0: + t_ret[i] = t_sup_hp[i] + else: + t_ret[i] = t_sup_hp[i] + demand[i] / (m[i] * cte.WATER_HEAT_CAPACITY) else: m[i] = 0 q_hp[i] = 0 - t_sup_hp[i] = t_ret[i] - t_ret[i + 1] = t_ret[i] + t_sup_hp[i] = t_ret[i -1] + t_ret[i] = t_ret[i - 1] t_sup_hp_fahrenheit = 1.8 * t_sup_hp[i] + 32 - t_out_fahrenheit = 1.8 * self._t_out[i] + 32 + t_out_fahrenheit = 1.8 * t_out[i] + 32 if q_hp[i] > 0: hp_eer[i] = (eer_curve_coefficients[0] + eer_curve_coefficients[1] * t_sup_hp_fahrenheit + @@ -195,12 +219,20 @@ class Archetype13: eer_curve_coefficients[3] * t_out_fahrenheit + eer_curve_coefficients[4] * t_out_fahrenheit ** 2 + eer_curve_coefficients[5] * t_sup_hp_fahrenheit * t_out_fahrenheit) - hp_electricity[i] = q_hp[i] / cooling_efficiency + hp_electricity[i] = q_hp[i] / hp_eer[i] else: hp_eer[i] = 0 hp_electricity[i] = 0 + hp_electricity_j = [(x * cte.WATTS_HOUR_TO_JULES) / number_of_ts for x in hp_electricity] + hp_hourly = [] + hp_sum = 0 + for i in range(1, len(demand)): + hp_sum += hp_electricity_j[i] + if (i - 1) % number_of_ts == 0: + hp_hourly.append(hp_sum) + hp_sum = 0 hp.energy_consumption[cte.COOLING] = {} - hp.energy_consumption[cte.COOLING][cte.HOUR] = hp_electricity + hp.energy_consumption[cte.COOLING][cte.HOUR] = hp_hourly hp.energy_consumption[cte.COOLING][cte.MONTH] = MonthlyValues.get_total_month( hp.energy_consumption[cte.COOLING][cte.HOUR]) hp.energy_consumption[cte.COOLING][cte.YEAR] = [ @@ -212,12 +244,14 @@ class Archetype13: self.results['HP Electricity Consumption'] = hp_electricity self.results['Cooling Loop Flow Rate (kg/s)'] = m self.results['Cooling Loop Return Temperature'] = t_ret - return hp_electricity + return hp_hourly def dhw_system_simulation(self): hp, tes = self.dhw_sizing() cop_curve_coefficients = [float(coefficient) for coefficient in hp.heat_efficiency_curve.coefficients] - demand = self._hourly_dhw_demand + number_of_ts = int(cte.HOUR_TO_SECONDS / self.dt) + demand = [0] + [x for x in self._hourly_dhw_demand for _ in range(number_of_ts)] + t_out = [0] + [x for x in self._t_out for _ in range(number_of_ts)] variable_names = ["t_sup_hp", "t_tank", "m_ch", "m_dis", "q_hp", "q_coil", "hp_cop", "hp_electricity", "available hot water (m3)", "refill flow rate (kg/s)"] num_hours = len(demand) @@ -226,7 +260,7 @@ class Archetype13: [variables[name] for name in variable_names] t_tank[0] = 70 v_dhw[0] = tes.volume - dt = 3600 + hp_heating_cap = hp.nominal_heat_output hp_delta_t = 8 v, h = float(tes.volume), float(tes.height) @@ -239,26 +273,24 @@ class Archetype13: ua = u_tot * (2 * a_top + a_side) freshwater_temperature = 18 for i in range(len(demand) - 1): - delta_t_demand = demand[i] * (dt / (cte.WATER_DENSITY * cte.WATER_HEAT_CAPACITY * v)) + delta_t_demand = demand[i] * (self.dt / (cte.WATER_DENSITY * cte.WATER_HEAT_CAPACITY * v)) if t_tank[i] < 65: q_hp[i] = hp_heating_cap - delta_t_hp = q_hp[i] * (dt / (cte.WATER_DENSITY * cte.WATER_HEAT_CAPACITY * v)) + delta_t_hp = q_hp[i] * (self.dt / (cte.WATER_DENSITY * cte.WATER_HEAT_CAPACITY * v)) if demand[i] > 0: dhw_needed = (demand[i] * cte.HOUR_TO_SECONDS) / (cte.WATER_HEAT_CAPACITY * t_tank[i] * cte.WATER_DENSITY) m_dis[i] = dhw_needed * cte.WATER_DENSITY / cte.HOUR_TO_SECONDS m_refill[i] = m_dis[i] - delta_t_freshwater = m_refill[i] * (t_tank[i] - freshwater_temperature) * (dt / (v * cte.WATER_DENSITY)) + delta_t_freshwater = m_refill[i] * (t_tank[i] - freshwater_temperature) * (self.dt / (v * cte.WATER_DENSITY)) diff = delta_t_freshwater + delta_t_demand - delta_t_hp if diff > 0: if diff > 0: - power = diff * (cte.WATER_DENSITY * cte.WATER_HEAT_CAPACITY * v) / dt + power = diff * (cte.WATER_DENSITY * cte.WATER_HEAT_CAPACITY * v) / self.dt if power <= float(tes.heating_coil_capacity): q_coil[i] = power else: q_coil[i] = float(tes.heating_coil_capacity) - elif t_tank[i] < 65: - q_coil[i] = float(tes.heating_coil_capacity) - delta_t_coil = q_coil[i] * (dt / (cte.WATER_DENSITY * cte.WATER_HEAT_CAPACITY * v)) + delta_t_coil = q_coil[i] * (self.dt / (cte.WATER_DENSITY * cte.WATER_HEAT_CAPACITY * v)) if q_hp[i] > 0: m_ch[i] = q_hp[i] / (cte.WATER_HEAT_CAPACITY * hp_delta_t) @@ -267,7 +299,7 @@ class Archetype13: m_ch[i] = 0 t_sup_hp[i] = t_tank[i] t_sup_hp_fahrenheit = 1.8 * t_sup_hp[i] + 32 - t_out_fahrenheit = 1.8 * self._t_out[i] + 32 + t_out_fahrenheit = 1.8 * t_out[i] + 32 if q_hp[i] > 0: hp_cop[i] = (cop_curve_coefficients[0] + cop_curve_coefficients[1] * t_sup_hp_fahrenheit + @@ -275,32 +307,47 @@ class Archetype13: cop_curve_coefficients[3] * t_out_fahrenheit + cop_curve_coefficients[4] * t_out_fahrenheit ** 2 + cop_curve_coefficients[5] * t_sup_hp_fahrenheit * t_out_fahrenheit) - hp_electricity[i] = q_hp[i] / 3.5 + hp_electricity[i] = q_hp[i] / hp_cop[i] else: hp_cop[i] = 0 hp_electricity[i] = 0 t_tank[i + 1] = t_tank[i] + (delta_t_hp - delta_t_freshwater - delta_t_demand + delta_t_coil) + tes.temperature = [] + hp_electricity_j = [(x * cte.WATTS_HOUR_TO_JULES) / number_of_ts for x in hp_electricity] + heating_coil_j = [(x * cte.WATTS_HOUR_TO_JULES) / number_of_ts for x in q_coil] + hp_hourly = [] + coil_hourly = [] + coil_sum = 0 + hp_sum = 0 + for i in range(1, len(demand)): + hp_sum += hp_electricity_j[i] + coil_sum += heating_coil_j[i] + if (i - 1) % number_of_ts == 0: + tes.temperature.append(t_tank[i]) + hp_hourly.append(hp_sum) + coil_hourly.append(coil_sum) + hp_sum = 0 + coil_sum = 0 hp.energy_consumption[cte.DOMESTIC_HOT_WATER] = {} - hp.energy_consumption[cte.DOMESTIC_HOT_WATER][cte.HOUR] = hp_electricity + hp.energy_consumption[cte.DOMESTIC_HOT_WATER][cte.HOUR] = hp_hourly hp.energy_consumption[cte.DOMESTIC_HOT_WATER][cte.MONTH] = MonthlyValues.get_total_month( hp.energy_consumption[cte.DOMESTIC_HOT_WATER][cte.HOUR]) hp.energy_consumption[cte.DOMESTIC_HOT_WATER][cte.YEAR] = [ sum(hp.energy_consumption[cte.DOMESTIC_HOT_WATER][cte.MONTH])] tes.heating_coil_energy_consumption = {} - tes.heating_coil_energy_consumption[cte.HOUR] = q_coil + tes.heating_coil_energy_consumption[cte.HOUR] = coil_hourly tes.heating_coil_energy_consumption[cte.MONTH] = MonthlyValues.get_total_month( tes.heating_coil_energy_consumption[cte.HOUR]) tes.heating_coil_energy_consumption[cte.YEAR] = [ sum(tes.heating_coil_energy_consumption[cte.MONTH])] tes.temperature = t_tank - self.results['DHW Demand (W)'] = demand self.results['DHW HP Heat Output (W)'] = q_hp self.results['DHW HP Electricity Consumption (W)'] = hp_electricity - self.results['DHW HP Source Temperature'] = self._t_out + self.results['DHW HP Source Temperature'] = t_out self.results['DHW HP Supply Temperature'] = t_sup_hp self.results['DHW HP COP'] = hp_cop self.results['DHW TES Heating Coil Heat Output (W)'] = q_coil @@ -309,7 +356,7 @@ class Archetype13: self.results['DHW Flow Rate (kg/s)'] = m_dis self.results['DHW TES Refill Flow Rate (kg/s)'] = m_refill self.results['Available Water in Tank (m3)'] = v_dhw - return hp_electricity, q_coil + return hp_hourly, coil_hourly def enrich_buildings(self): hp_heating, boiler_consumption = self.heating_system_simulation() @@ -320,11 +367,11 @@ class Archetype13: self._building.heating_consumption[cte.HOUR] = heating_consumption self._building.heating_consumption[cte.MONTH] = ( MonthlyValues.get_total_month(self._building.heating_consumption[cte.HOUR])) - self._building.heating_consumption[cte.YEAR] = sum(self._building.heating_consumption[cte.MONTH]) + self._building.heating_consumption[cte.YEAR] = [sum(self._building.heating_consumption[cte.MONTH])] self._building.cooling_consumption[cte.HOUR] = hp_cooling self._building.cooling_consumption[cte.MONTH] = ( MonthlyValues.get_total_month(self._building.cooling_consumption[cte.HOUR])) - self._building.cooling_consumption[cte.YEAR] = sum(self._building.cooling_consumption[cte.MONTH]) + self._building.cooling_consumption[cte.YEAR] = [sum(self._building.cooling_consumption[cte.MONTH])] self._building.domestic_hot_water_consumption[cte.HOUR] = dhw_consumption self._building.domestic_hot_water_consumption[cte.MONTH] = ( MonthlyValues.get_total_month(self._building.domestic_hot_water_consumption[cte.HOUR])) diff --git a/scripts/system_simulation_models/archetype13_stratified_tes.py b/scripts/system_simulation_models/archetype13_stratified_tes.py new file mode 100644 index 00000000..c5bdd1e7 --- /dev/null +++ b/scripts/system_simulation_models/archetype13_stratified_tes.py @@ -0,0 +1,392 @@ +import math +import hub.helpers.constants as cte +import csv +from hub.helpers.monthly_values import MonthlyValues +import numpy as np + + +class Archetype13Stratified: + def __init__(self, building, output_path): + self._building = building + self._name = building.name + self._pv_system = building.energy_systems[0] + self._hvac_system = building.energy_systems[1] + self._dhw_system = building.energy_systems[-1] + self._dhw_peak_flow_rate = (building.thermal_zones_from_internal_zones[0].total_floor_area * + building.thermal_zones_from_internal_zones[0].domestic_hot_water.peak_flow * + cte.WATER_DENSITY) + self._heating_peak_load = building.heating_peak_load[cte.YEAR][0] + self._cooling_peak_load = building.cooling_peak_load[cte.YEAR][0] + self._domestic_hot_water_peak_load = building.domestic_hot_water_peak_load[cte.YEAR][0] + self._hourly_heating_demand = [demand / 3600 for demand in building.heating_demand[cte.HOUR]] + self._hourly_cooling_demand = [demand / 3600 for demand in building.cooling_demand[cte.HOUR]] + self._hourly_dhw_demand = [0] + building.domestic_hot_water_heat_demand[cte.HOUR] + self._output_path = output_path + self._t_out = building.external_temperature[cte.HOUR] + self.results = {} + + def hvac_sizing(self): + storage_factor = 3 + heat_pump = self._hvac_system.generation_systems[1] + boiler = self._hvac_system.generation_systems[0] + thermal_storage = boiler.energy_storage_systems[0] + heat_pump.nominal_heat_output = round(0.5 * self._heating_peak_load / 3600) + heat_pump.nominal_cooling_output = round(self._cooling_peak_load / 3600) + boiler.nominal_heat_output = round(0.5 * self._heating_peak_load / 3600) + thermal_storage.volume = round( + (self._heating_peak_load * storage_factor) / (cte.WATER_HEAT_CAPACITY * cte.WATER_DENSITY * 25)) + return heat_pump, boiler, thermal_storage + + def dhw_sizing(self): + storage_factor = 3 + dhw_hp = self._dhw_system.generation_systems[0] + dhw_hp.nominal_heat_output = 0.7 * self._domestic_hot_water_peak_load + dhw_hp.source_temperature = self._t_out + dhw_tes = dhw_hp.energy_storage_systems[0] + dhw_tes.volume = round( + (self._domestic_hot_water_peak_load * storage_factor * 3600) / (cte.WATER_HEAT_CAPACITY * cte.WATER_DENSITY * 10)) + return dhw_hp, dhw_tes + + def heating_system_simulation_stratified(self): + hp, boiler, tes = self.hvac_sizing() + cop_curve_coefficients = [float(coefficient) for coefficient in hp.heat_efficiency_curve.coefficients] + demand = [0] + [x for x in self._hourly_heating_demand for _ in range(12)] + hp.source_temperature = self._t_out + t_out = [0] + [x for x in self._t_out for _ in range(12)] + variable_names = ["t_sup_hp", "t1", "t2", "t3", "t4", "t_tank", "t_ret", "m_ch", "m_dis", "q_hp", "q_boiler", + "hp_cop", "hp_electricity", "boiler_gas_consumption", "t_sup_boiler", "boiler_energy_consumption", + "heating_consumption"] + num_hours = len(demand) + variables = {name: [0] * num_hours for name in variable_names} + (t_sup_hp, t1, t2, t3, t4, t_tank, t_ret, m_ch, m_dis, q_hp, q_boiler, hp_cop, + hp_electricity, boiler_gas_consumption, t_sup_boiler, boiler_energy_consumption, heating_consumption) = \ + [variables[name] for name in variable_names] + t_tank[0] = 55 + t1[0] = 55 + t2[0] = 55 + t3[0] = 55 + t4[0] = 55 + dt = 300 + hp_heating_cap = hp.nominal_heat_output + boiler_heating_cap = boiler.nominal_heat_output + hp_delta_t = 5 + boiler_efficiency = float(boiler.heat_efficiency) + v, h = float(tes.volume) / 4, float(tes.height) / 4 + r_tot = sum(float(layer.thickness) / float(layer.material.conductivity) for layer in + tes.layers) + u_tot = 1 / r_tot + d = math.sqrt((4 * v) / (math.pi * h)) + a_side = math.pi * d * h + a_top = math.pi * d ** 2 / 4 + ua_side = u_tot * a_side + ua_top_bottom = u_tot * (a_top + a_side) + # storage temperature prediction + for i in range(len(demand) - 1): + t1[i + 1] = t1[i] + ((m_ch[i] * (t_sup_boiler[i] - t1[i])) + ( + np.heaviside((m_dis[i] - m_ch[i]), 0) * (m_ch[i] - m_dis[i]) * (t1[i] - t2[i])) + ( + ua_top_bottom * (t_out[i] - t1[i])) / cte.WATER_HEAT_CAPACITY - cte.WATER_THERMAL_CONDUCTIVITY * (a_top * (t1[i] - t2[i])) / ( + cte.WATER_HEAT_CAPACITY * h)) * (dt / (cte.WATER_DENSITY * v)) + t2[i + 1] = t2[i] + ((np.heaviside((m_dis[i] - m_ch[i]), 0) * (m_ch[i] - m_dis[i]) * (t2[i] - t3[i])) + ( + ua_side * (t_out[i] - t2[i])) / cte.WATER_HEAT_CAPACITY - (cte.WATER_THERMAL_CONDUCTIVITY * (a_top * (t2[i] - t1[i])) / (cte.WATER_HEAT_CAPACITY * h)) - ( + cte.WATER_THERMAL_CONDUCTIVITY * (a_top * (t2[i] - t3[i])) / (cte.WATER_HEAT_CAPACITY * h)) + ( + np.heaviside((m_ch[i] - m_dis[i]), 0) * (m_ch[i] - m_dis[i]) * ( + t1[i] - t2[i]))) * (dt / (cte.WATER_DENSITY * v)) + t3[i + 1] = t3[i] + ((np.heaviside((m_dis[i] - m_ch[i]), 0) * (m_ch[i] - m_dis[i]) * (t3[i] - t4[i])) + ( + ua_side * (t_out[i] - t3[i])) / cte.WATER_HEAT_CAPACITY - (cte.WATER_THERMAL_CONDUCTIVITY * (a_top * (t3[i] - t2[i])) / (cte.WATER_HEAT_CAPACITY * h)) - ( + cte.WATER_THERMAL_CONDUCTIVITY * (a_top * (t3[i] - t4[i])) / (cte.WATER_HEAT_CAPACITY * h)) + ( + np.heaviside((m_ch[i] - m_dis[i]), 0) * (m_ch[i] - m_dis[i]) * ( + t2[i] - t3[i]))) * (dt / (cte.WATER_DENSITY * v)) + t4[i + 1] = t4[i] + (np.heaviside((m_ch[i] - m_dis[i]), 0) * ((m_ch[i] - m_dis[i]) * (t3[i] - t4[i])) + ( + ua_top_bottom * (t_out[i] - t4[-1])) / cte.WATER_HEAT_CAPACITY - m_dis[i] * ((t4[i] - t_ret[i])) - ( + cte.WATER_THERMAL_CONDUCTIVITY * (a_top * (t4[i] - t3[i])) / (cte.WATER_HEAT_CAPACITY * h))) * (dt / (cte.WATER_DENSITY * v)) + # hp operation + if t1[i + 1] < 40: + q_hp[i + 1] = hp_heating_cap + m_ch[i + 1] = q_hp[i + 1] / (cte.WATER_HEAT_CAPACITY * hp_delta_t) + t_sup_hp[i + 1] = (q_hp[i + 1] / (m_ch[i + 1] * cte.WATER_HEAT_CAPACITY)) + t4[i + 1] + elif 40 <= t1[i + 1] < 55 and q_hp[i] == 0: + q_hp[i + 1] = 0 + m_ch[i + 1] = 0 + t_sup_hp[i + 1] = t4[i + 1] + elif 40 <= t1[i + 1] < 55 and q_hp[i] > 0: + q_hp[i + 1] = hp_heating_cap + m_ch[i + 1] = q_hp[i + 1] / (cte.WATER_HEAT_CAPACITY * hp_delta_t) + t_sup_hp[i + 1] = (q_hp[i + 1] / (m_ch[i + 1] * cte.WATER_HEAT_CAPACITY)) + t4[i + 1] + else: + q_hp[i + 1], m_ch[i + 1], t_sup_hp[i + 1] = 0, 0, t4[i + 1] + t_sup_hp_fahrenheit = 1.8 * t_sup_hp[i + 1] + 32 + t_out_fahrenheit = 1.8 * t_out[i + 1] + 32 + if q_hp[i + 1] > 0: + hp_cop[i + 1] = (cop_curve_coefficients[0] + + cop_curve_coefficients[1] * t_sup_hp_fahrenheit + + cop_curve_coefficients[2] * t_sup_hp_fahrenheit ** 2 + + cop_curve_coefficients[3] * t_out_fahrenheit + + cop_curve_coefficients[4] * t_out_fahrenheit ** 2 + + cop_curve_coefficients[5] * t_sup_hp_fahrenheit * t_out_fahrenheit) + hp_electricity[i + 1] = q_hp[i + 1] / hp_cop[i + 1] + else: + hp_cop[i + 1] = 0 + hp_electricity[i + 1] = 0 + # boiler operation + if q_hp[i + 1] > 0: + if t_sup_hp[i + 1] < 45: + q_boiler[i + 1] = boiler_heating_cap + elif demand[i + 1] > 0.5 * self._heating_peak_load / dt: + q_boiler[i + 1] = 0.5 * boiler_heating_cap + boiler_energy_consumption[i + 1] = q_boiler[i + 1] / boiler_efficiency + boiler_gas_consumption[i + 1] = (q_boiler[i + 1] * dt) / (boiler_efficiency * cte.NATURAL_GAS_LHV) + t_sup_boiler[i + 1] = t_sup_hp[i + 1] + (q_boiler[i + 1] / (m_ch[i + 1] * cte.WATER_HEAT_CAPACITY)) + # storage discharging + if demand[i + 1] == 0: + m_dis[i + 1] = 0 + t_ret[i + 1] = t1[i + 1] + else: + if demand[i + 1] > 0.5 * self._heating_peak_load / cte.HOUR_TO_SECONDS: + factor = 8 + else: + factor = 4 + m_dis[i + 1] = self._heating_peak_load / (cte.WATER_HEAT_CAPACITY * factor * cte.HOUR_TO_SECONDS) + t_ret[i + 1] = t1[i + 1] - demand[i + 1] / (m_dis[i + 1] * cte.WATER_HEAT_CAPACITY) + + hp_electricity_wh = [x / 12 for x in hp_electricity] + boiler_consumption_wh = [x / 12 for x in boiler_energy_consumption] + hp_hourly = [] + boiler_hourly = [] + tes.temperature = {} + tes.temperature['layer_1'] = [] + tes.temperature['layer_2'] = [] + tes.temperature['layer_3'] = [] + tes.temperature['layer_4'] = [] + for i in range(1, len(demand), 12): + tes.temperature['layer_1'].append(t1[i]) + tes.temperature['layer_2'].append(t2[i]) + tes.temperature['layer_3'].append(t3[i]) + tes.temperature['layer_4'].append(t4[i]) + demand_modified = demand[1:] + hp_hourly.append(hp_electricity[1]) + boiler_hourly.append(boiler_energy_consumption[1]) + boiler_sum = 0 + hp_sum = 0 + for i in range(1, len(demand_modified) + 1): + hp_sum += hp_electricity_wh[i] + boiler_sum += boiler_consumption_wh[i] + if i % 12 == 0: + hp_hourly.append(hp_sum) + boiler_hourly.append(boiler_sum) + hp_sum = 0 + boiler_sum = 0 + + hp.energy_consumption[cte.HEATING] = {} + hp.energy_consumption[cte.HEATING][cte.HOUR] = hp_hourly + hp.energy_consumption[cte.HEATING][cte.MONTH] = MonthlyValues.get_total_month( + hp.energy_consumption[cte.HEATING][cte.HOUR]) + hp.energy_consumption[cte.HEATING][cte.YEAR] = [ + sum(hp.energy_consumption[cte.HEATING][cte.MONTH])] + boiler.energy_consumption[cte.HEATING] = {} + boiler.energy_consumption[cte.HEATING][cte.HOUR] = boiler_hourly + boiler.energy_consumption[cte.HEATING][cte.MONTH] = MonthlyValues.get_total_month( + boiler.energy_consumption[cte.HEATING][cte.HOUR]) + boiler.energy_consumption[cte.HEATING][cte.YEAR] = [ + sum(boiler.energy_consumption[cte.HEATING][cte.MONTH])] + + self.results['Heating Demand (W)'] = demand + self.results['HP Heat Output (W)'] = q_hp + self.results['HP Source Temperature'] = t_out + self.results['HP Supply Temperature'] = t_sup_hp + self.results['HP COP'] = hp_cop + self.results['HP Electricity Consumption (W)'] = hp_electricity + self.results['Boiler Heat Output (W)'] = q_boiler + self.results['Boiler Supply Temperature'] = t_sup_boiler + self.results['Boiler Gas Consumption'] = boiler_gas_consumption + self.results['TES Layer 1 Temperature'] = t1 + self.results['TES Layer 2 Temperature'] = t2 + self.results['TES Layer 3 Temperature'] = t3 + self.results['TES Layer 4 Temperature'] = t4 + self.results['TES Charging Flow Rate (kg/s)'] = m_ch + self.results['TES Discharge Flow Rate (kg/s)'] = m_dis + self.results['Heating Loop Return Temperature'] = t_ret + return hp_electricity, boiler_energy_consumption + + def cooling_system_simulation(self): + hp = self.hvac_sizing()[0] + eer_curve_coefficients = [float(coefficient) for coefficient in hp.cooling_efficiency_curve.coefficients] + cooling_efficiency = float(hp.cooling_efficiency) + demand = self._hourly_cooling_demand + hp.source_temperature = self._t_out + variable_names = ["t_sup_hp", "t_ret", "m", "q_hp", "hp_electricity", "hp_eer"] + num_hours = len(demand) + variables = {name: [0] * num_hours for name in variable_names} + (t_sup_hp, t_ret, m, q_hp, hp_electricity, hp_eer) = [variables[name] for name in variable_names] + t_ret[0] = 13 + dt = 3600 + for i in range(len(demand) - 1): + if demand[i] > 0: + m[i] = self._cooling_peak_load / (cte.WATER_HEAT_CAPACITY * 5 * dt) + if t_ret[i] > 13: + if demand[i] < 0.25 * self._cooling_peak_load / dt: + q_hp[i] = 0.25 * hp.nominal_cooling_output + elif demand[i] < 0.5 * self._cooling_peak_load / dt: + q_hp[i] = 0.5 * hp.nominal_cooling_output + else: + q_hp[i] = hp.nominal_cooling_output + t_sup_hp[i] = t_ret[i] - q_hp[i] / (m[i] * cte.WATER_HEAT_CAPACITY) + else: + q_hp[i] = 0 + t_sup_hp[i] = t_ret[i] + t_ret[i + 1] = t_sup_hp[i] + demand[i] / (m[i] * cte.WATER_HEAT_CAPACITY) + else: + m[i] = 0 + q_hp[i] = 0 + t_sup_hp[i] = t_ret[i] + t_ret[i + 1] = t_ret[i] + t_sup_hp_fahrenheit = 1.8 * t_sup_hp[i] + 32 + t_out_fahrenheit = 1.8 * self._t_out[i] + 32 + if q_hp[i] > 0: + hp_eer[i] = (eer_curve_coefficients[0] + + eer_curve_coefficients[1] * t_sup_hp_fahrenheit + + eer_curve_coefficients[2] * t_sup_hp_fahrenheit ** 2 + + eer_curve_coefficients[3] * t_out_fahrenheit + + eer_curve_coefficients[4] * t_out_fahrenheit ** 2 + + eer_curve_coefficients[5] * t_sup_hp_fahrenheit * t_out_fahrenheit) + hp_electricity[i] = q_hp[i] / cooling_efficiency + else: + hp_eer[i] = 0 + hp_electricity[i] = 0 + hp.energy_consumption[cte.COOLING] = {} + hp.energy_consumption[cte.COOLING][cte.HOUR] = hp_electricity + hp.energy_consumption[cte.COOLING][cte.MONTH] = MonthlyValues.get_total_month( + hp.energy_consumption[cte.COOLING][cte.HOUR]) + hp.energy_consumption[cte.COOLING][cte.YEAR] = [ + sum(hp.energy_consumption[cte.COOLING][cte.MONTH])] + # self.results['Cooling Demand (W)'] = demand + # self.results['HP Cooling Output (W)'] = q_hp + # self.results['HP Cooling Supply Temperature'] = t_sup_hp + # self.results['HP Cooling COP'] = hp_eer + # self.results['HP Electricity Consumption'] = hp_electricity + # self.results['Cooling Loop Flow Rate (kg/s)'] = m + # self.results['Cooling Loop Return Temperature'] = t_ret + return hp_electricity + + def dhw_system_simulation(self): + hp, tes = self.dhw_sizing() + cop_curve_coefficients = [float(coefficient) for coefficient in hp.heat_efficiency_curve.coefficients] + demand = self._hourly_dhw_demand + variable_names = ["t_sup_hp", "t_tank", "m_ch", "m_dis", "q_hp", "q_coil", "hp_cop", + "hp_electricity", "available hot water (m3)", "refill flow rate (kg/s)"] + num_hours = len(demand) + variables = {name: [0] * num_hours for name in variable_names} + (t_sup_hp, t_tank, m_ch, m_dis, m_refill, q_hp, q_coil, hp_cop, hp_electricity, v_dhw) = \ + [variables[name] for name in variable_names] + t_tank[0] = 70 + v_dhw[0] = tes.volume + dt = 3600 + hp_heating_cap = hp.nominal_heat_output + hp_delta_t = 8 + v, h = float(tes.volume), float(tes.height) + r_tot = sum(float(layer.thickness) / float(layer.material.conductivity) for layer in + tes.layers) + u_tot = 1 / r_tot + d = math.sqrt((4 * v) / (math.pi * h)) + a_side = math.pi * d * h + a_top = math.pi * d ** 2 / 4 + ua = u_tot * (2 * a_top + a_side) + freshwater_temperature = 18 + for i in range(len(demand) - 1): + delta_t_demand = demand[i] * (dt / (cte.WATER_DENSITY * cte.WATER_HEAT_CAPACITY * v)) + if t_tank[i] < 65: + q_hp[i] = hp_heating_cap + delta_t_hp = q_hp[i] * (dt / (cte.WATER_DENSITY * cte.WATER_HEAT_CAPACITY * v)) + if demand[i] > 0: + dhw_needed = (demand[i] * cte.HOUR_TO_SECONDS) / (cte.WATER_HEAT_CAPACITY * t_tank[i] * cte.WATER_DENSITY) + m_dis[i] = dhw_needed * cte.WATER_DENSITY / cte.HOUR_TO_SECONDS + m_refill[i] = m_dis[i] + delta_t_freshwater = m_refill[i] * (t_tank[i] - freshwater_temperature) * (dt / (v * cte.WATER_DENSITY)) + diff = delta_t_freshwater + delta_t_demand - delta_t_hp + if diff > 0: + if diff > 0: + power = diff * (cte.WATER_DENSITY * cte.WATER_HEAT_CAPACITY * v) / dt + if power <= float(tes.heating_coil_capacity): + q_coil[i] = power + else: + q_coil[i] = float(tes.heating_coil_capacity) + elif t_tank[i] < 65: + q_coil[i] = float(tes.heating_coil_capacity) + delta_t_coil = q_coil[i] * (dt / (cte.WATER_DENSITY * cte.WATER_HEAT_CAPACITY * v)) + + if q_hp[i] > 0: + m_ch[i] = q_hp[i] / (cte.WATER_HEAT_CAPACITY * hp_delta_t) + t_sup_hp[i] = (q_hp[i] / (m_ch[i] * cte.WATER_HEAT_CAPACITY)) + t_tank[i] + else: + m_ch[i] = 0 + t_sup_hp[i] = t_tank[i] + t_sup_hp_fahrenheit = 1.8 * t_sup_hp[i] + 32 + t_out_fahrenheit = 1.8 * self._t_out[i] + 32 + if q_hp[i] > 0: + hp_cop[i] = (cop_curve_coefficients[0] + + cop_curve_coefficients[1] * t_sup_hp_fahrenheit + + cop_curve_coefficients[2] * t_sup_hp_fahrenheit ** 2 + + cop_curve_coefficients[3] * t_out_fahrenheit + + cop_curve_coefficients[4] * t_out_fahrenheit ** 2 + + cop_curve_coefficients[5] * t_sup_hp_fahrenheit * t_out_fahrenheit) + hp_electricity[i] = q_hp[i] / 3.5 + else: + hp_cop[i] = 0 + hp_electricity[i] = 0 + + t_tank[i + 1] = t_tank[i] + (delta_t_hp - delta_t_freshwater - delta_t_demand + delta_t_coil) + + hp.energy_consumption[cte.DOMESTIC_HOT_WATER] = {} + hp.energy_consumption[cte.DOMESTIC_HOT_WATER][cte.HOUR] = hp_electricity + hp.energy_consumption[cte.DOMESTIC_HOT_WATER][cte.MONTH] = MonthlyValues.get_total_month( + hp.energy_consumption[cte.DOMESTIC_HOT_WATER][cte.HOUR]) + hp.energy_consumption[cte.DOMESTIC_HOT_WATER][cte.YEAR] = [ + sum(hp.energy_consumption[cte.DOMESTIC_HOT_WATER][cte.MONTH])] + tes.heating_coil_energy_consumption = {} + tes.heating_coil_energy_consumption[cte.HOUR] = q_coil + tes.heating_coil_energy_consumption[cte.MONTH] = MonthlyValues.get_total_month( + tes.heating_coil_energy_consumption[cte.HOUR]) + tes.heating_coil_energy_consumption[cte.YEAR] = [ + sum(tes.heating_coil_energy_consumption[cte.MONTH])] + tes.temperature = t_tank + + + # self.results['DHW Demand (W)'] = demand + # self.results['DHW HP Heat Output (W)'] = q_hp + # self.results['DHW HP Electricity Consumption (W)'] = hp_electricity + # self.results['DHW HP Source Temperature'] = self._t_out + # self.results['DHW HP Supply Temperature'] = t_sup_hp + # self.results['DHW HP COP'] = hp_cop + # self.results['DHW TES Heating Coil Heat Output (W)'] = q_coil + # self.results['DHW TES Temperature'] = t_tank + # self.results['DHW TES Charging Flow Rate (kg/s)'] = m_ch + # self.results['DHW Flow Rate (kg/s)'] = m_dis + # self.results['DHW TES Refill Flow Rate (kg/s)'] = m_refill + # self.results['Available Water in Tank (m3)'] = v_dhw + return hp_electricity, q_coil + + def enrich_buildings(self): + hp_heating, boiler_consumption = self.heating_system_simulation_stratified() + # hp_cooling = self.cooling_system_simulation() + # hp_dhw, heating_coil = self.dhw_system_simulation() + heating_consumption = [hp_heating[i] + boiler_consumption[i] for i in range(len(hp_heating))] + # dhw_consumption = [hp_dhw[i] + heating_coil[i] for i in range(len(hp_dhw))] + # self._building.heating_consumption[cte.HOUR] = heating_consumption + # self._building.heating_consumption[cte.MONTH] = ( + # MonthlyValues.get_total_month(self._building.heating_consumption[cte.HOUR])) + # self._building.heating_consumption[cte.YEAR] = sum(self._building.heating_consumption[cte.MONTH]) + # self._building.cooling_consumption[cte.HOUR] = hp_cooling + # self._building.cooling_consumption[cte.MONTH] = ( + # MonthlyValues.get_total_month(self._building.cooling_consumption[cte.HOUR])) + # self._building.cooling_consumption[cte.YEAR] = sum(self._building.cooling_consumption[cte.MONTH]) + # self._building.domestic_hot_water_consumption[cte.HOUR] = dhw_consumption + # self._building.domestic_hot_water_consumption[cte.MONTH] = ( + # MonthlyValues.get_total_month(self._building.domestic_hot_water_consumption[cte.HOUR])) + # self._building.domestic_hot_water_consumption[cte.YEAR] = ( + # sum(self._building.domestic_hot_water_consumption[cte.MONTH])) + file_name = f'energy_system_simulation_results_{self._name}.csv' + with open(self._output_path / file_name, 'w', newline='') as csvfile: + output_file = csv.writer(csvfile) + # Write header + output_file.writerow(self.results.keys()) + # Write data + output_file.writerows(zip(*self.results.values())) From ef21ad949d3dc03b3a02d5fa0c292c147095b120 Mon Sep 17 00:00:00 2001 From: s_ranjbar Date: Thu, 20 Jun 2024 09:39:47 -0400 Subject: [PATCH 06/15] fix: different fuel pricings are added, operational and maintenance cost calculations are modified, cost catalogue adjusted to the changes --- .../cost/montreal_complete_cost_catalog.py | 68 ++++-- .../data_models/cost/fuel.py | 14 +- .../data_models/cost/item_description.py | 18 +- .../data_models/cost/operational_cost.py | 27 +-- .../data_models/cost/pricing_rate.py | 62 ++++++ hub/city_model_structure/building.py | 19 +- .../building_demand/surface.py | 17 ++ hub/data/costs/fuel_rates.json | 106 +++++++++ hub/data/costs/montreal_costs_completed.xml | 88 ++++++-- main.py | 45 ---- scripts/costs/capital_costs.py | 97 ++++----- scripts/costs/configuration.py | 11 +- scripts/costs/constants.py | 4 +- scripts/costs/cost.py | 9 +- scripts/costs/peak_load.py | 2 +- scripts/costs/total_maintenance_costs.py | 86 +++++++- scripts/costs/total_operational_costs.py | 201 +++++++++++++++--- scripts/energy_system_sizing.py | 8 +- scripts/random_assignation.py | 8 +- 19 files changed, 678 insertions(+), 212 deletions(-) create mode 100644 hub/catalog_factories/data_models/cost/pricing_rate.py create mode 100644 hub/data/costs/fuel_rates.json diff --git a/hub/catalog_factories/cost/montreal_complete_cost_catalog.py b/hub/catalog_factories/cost/montreal_complete_cost_catalog.py index 7d6e8ade..5e3927ee 100644 --- a/hub/catalog_factories/cost/montreal_complete_cost_catalog.py +++ b/hub/catalog_factories/cost/montreal_complete_cost_catalog.py @@ -6,6 +6,7 @@ Project Coder Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ import xmltodict +import json from hub.catalog_factories.catalog import Catalog from hub.catalog_factories.data_models.cost.archetype import Archetype from hub.catalog_factories.data_models.cost.content import Content @@ -15,6 +16,7 @@ from hub.catalog_factories.data_models.cost.item_description import ItemDescript from hub.catalog_factories.data_models.cost.operational_cost import OperationalCost from hub.catalog_factories.data_models.cost.fuel import Fuel from hub.catalog_factories.data_models.cost.income import Income +from hub.catalog_factories.data_models.cost.pricing_rate import PricingRate class MontrealNewCatalog(Catalog): @@ -24,6 +26,7 @@ class MontrealNewCatalog(Catalog): def __init__(self, path): path = (path / 'montreal_costs_completed.xml').resolve() + self._fuel_rates_path = (path.parent / 'fuel_rates.json').resolve() with open(path, 'r', encoding='utf-8') as xml: self._archetypes = xmltodict.parse(xml.read(), force_list='archetype') @@ -75,7 +78,22 @@ class MontrealNewCatalog(Catalog): refurbishment_unit=_refurbishment_unit, reposition=None, reposition_unit=None, - lifetime=None) + lifetime=None, + maintenance=None, + maintenance_unit=None) + elif 'maintenance_cost' in item.keys(): + maintenance_cost = float(item['maintenance_cost']['#text']) + maintenance_unit = item['maintenance_cost']['@cost_unit'] + _item_description = ItemDescription(item_type, + initial_investment=None, + initial_investment_unit=None, + refurbishment=None, + refurbishment_unit=None, + reposition=None, + reposition_unit=None, + lifetime=None, + maintenance=maintenance_cost, + maintenance_unit=maintenance_unit) else: _reposition = float(item['reposition']['#text']) _reposition_unit = item['reposition']['@cost_unit'] @@ -89,7 +107,9 @@ class MontrealNewCatalog(Catalog): refurbishment_unit=None, reposition=_reposition, reposition_unit=_reposition_unit, - lifetime=_lifetime) + lifetime=_lifetime, + maintenance=None, + maintenance_unit=None) return _item_description @@ -137,13 +157,35 @@ class MontrealNewCatalog(Catalog): return capital_costs - @staticmethod - def _get_operational_costs(entry): + def load_fuel_rates(self): + rates = [] + with open(self._fuel_rates_path, 'r') as f: + fuel_rates = json.load(f) + for rate in fuel_rates['rates']['fuels']['rate']: + name = rate['name'] + rate_type = rate['rate_type'] + units = rate['units'] + values = rate['values'] + rates.append(PricingRate(name=name, rate_type=rate_type, units=units, values=values)) + return rates + + + def search_fuel_rates(self, rates, name): + variable = None + for rate in rates: + if rate.name == name: + variable = rate + return variable + + + + def _get_operational_costs(self, entry): fuels = [] + rates = self.load_fuel_rates() for item in entry['fuels']['fuel']: fuel_type = item['@fuel_type'] - fuel_variable = float(item['variable']['#text']) - fuel_variable_units = item['variable']['@cost_unit'] + fuel_variable = item['variable'] + variable = self.search_fuel_rates(rates, fuel_variable) fuel_fixed_monthly = None fuel_fixed_peak = None density = None @@ -165,20 +207,22 @@ class MontrealNewCatalog(Catalog): fuel = Fuel(fuel_type, fixed_monthly=fuel_fixed_monthly, fixed_power=fuel_fixed_peak, - variable=fuel_variable, - variable_units=fuel_variable_units, + variable=variable, density=density, density_unit=density_unit, lower_heating_value=lower_heating_value, lower_heating_value_unit=lower_heating_value_unit) fuels.append(fuel) - heating_equipment_maintenance = float(entry['maintenance']['heating_equipment']['#text']) - cooling_equipment_maintenance = float(entry['maintenance']['cooling_equipment']['#text']) + hvac_equipment = entry['maintenance']['hvac_equipment'] + items = [] + for item in hvac_equipment: + items.append(self.item_description(item, hvac_equipment[item])) + + photovoltaic_system_maintenance = float(entry['maintenance']['photovoltaic_system']['#text']) co2_emissions = float(entry['co2_cost']['#text']) _operational_cost = OperationalCost(fuels, - heating_equipment_maintenance, - cooling_equipment_maintenance, + items, photovoltaic_system_maintenance, co2_emissions) return _operational_cost diff --git a/hub/catalog_factories/data_models/cost/fuel.py b/hub/catalog_factories/data_models/cost/fuel.py index 5eb4866c..560b4da9 100644 --- a/hub/catalog_factories/data_models/cost/fuel.py +++ b/hub/catalog_factories/data_models/cost/fuel.py @@ -6,7 +6,7 @@ Project Coder Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ from typing import Union - +from hub.catalog_factories.data_models.cost.pricing_rate import PricingRate class Fuel: """ @@ -16,7 +16,6 @@ class Fuel: fixed_monthly=None, fixed_power=None, variable=None, - variable_units=None, density=None, density_unit=None, lower_heating_value=None, @@ -26,7 +25,6 @@ class Fuel: self._fixed_monthly = fixed_monthly self._fixed_power = fixed_power self._variable = variable - self._variable_units = variable_units self._density = density self._density_unit = density_unit self._lower_heating_value = lower_heating_value @@ -59,12 +57,12 @@ class Fuel: return None @property - def variable(self) -> Union[tuple[None, None], tuple[float, str]]: + def variable(self) -> Union[None, PricingRate]: """ Get variable costs in given units :return: None, None or float, str """ - return self._variable, self._variable_units + return self._variable @property def density(self) -> Union[tuple[None, None], tuple[float, str]]: @@ -84,11 +82,13 @@ class Fuel: def to_dictionary(self): """Class content to dictionary""" + variable_price = None + if self.variable is not None: + variable_price = self.variable.to_dictionary() content = {'Fuel': {'fuel type': self.type, 'fixed operational costs [currency/month]': self.fixed_monthly, 'fixed operational costs depending on the peak power consumed [currency/month W]': self.fixed_power, - 'variable operational costs': self.variable[0], - 'units': self.variable[1], + 'variable operational costs': variable_price, 'density': self.density[0], 'density unit': self.density[1], 'lower heating value': self.lower_heating_value[0], diff --git a/hub/catalog_factories/data_models/cost/item_description.py b/hub/catalog_factories/data_models/cost/item_description.py index a193bb23..192d7503 100644 --- a/hub/catalog_factories/data_models/cost/item_description.py +++ b/hub/catalog_factories/data_models/cost/item_description.py @@ -19,7 +19,9 @@ class ItemDescription: refurbishment_unit=None, reposition=None, reposition_unit=None, - lifetime=None): + lifetime=None, + maintenance=None, + maintenance_unit=None): self._item_type = item_type self._initial_investment = initial_investment @@ -29,6 +31,8 @@ class ItemDescription: self._reposition = reposition self._reposition_unit = reposition_unit self._lifetime = lifetime + self._maintenance = maintenance + self._maintenance_unit = maintenance_unit @property def type(self): @@ -70,6 +74,14 @@ class ItemDescription: """ return self._lifetime + @property + def maintenance(self) -> Union[tuple[None, None], tuple[float, str]]: + """ + Get reposition costs of the specific item in given units + :return: None, None or float, str + """ + return self._maintenance, self._maintenance_unit + def to_dictionary(self): """Class content to dictionary""" content = {'Item': {'type': self.type, @@ -79,7 +91,9 @@ class ItemDescription: 'refurbishment units': self.refurbishment[1], 'reposition': self.reposition[0], 'reposition units': self.reposition[1], - 'life time [years]': self.lifetime + 'life time [years]': self.lifetime, + 'maintenance': self.maintenance[0], + 'maintenance units': self.maintenance[1] } } diff --git a/hub/catalog_factories/data_models/cost/operational_cost.py b/hub/catalog_factories/data_models/cost/operational_cost.py index 1275830a..4f69cceb 100644 --- a/hub/catalog_factories/data_models/cost/operational_cost.py +++ b/hub/catalog_factories/data_models/cost/operational_cost.py @@ -7,16 +7,15 @@ Project Coder Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca from typing import List from hub.catalog_factories.data_models.cost.fuel import Fuel - +from hub.catalog_factories.data_models.cost.item_description import ItemDescription class OperationalCost: """ Operational cost class """ - def __init__(self, fuels, maintenance_heating, maintenance_cooling, maintenance_pv, co2): + def __init__(self, fuels, maintenance_hvac, maintenance_pv, co2): self._fuels = fuels - self._maintenance_heating = maintenance_heating - self._maintenance_cooling = maintenance_cooling + self._maintenance_hvac = maintenance_hvac self._maintenance_pv = maintenance_pv self._co2 = co2 @@ -30,20 +29,12 @@ class OperationalCost: return self._fuels @property - def maintenance_heating(self): + def maintenance_hvac(self) -> List[ItemDescription]: """ - Get cost of maintaining the heating system in currency/W + Get cost of maintaining the hvac system in currency/W :return: float """ - return self._maintenance_heating - - @property - def maintenance_cooling(self): - """ - Get cost of maintaining the cooling system in currency/W - :return: float - """ - return self._maintenance_cooling + return self._maintenance_hvac @property def maintenance_pv(self): @@ -64,11 +55,13 @@ class OperationalCost: def to_dictionary(self): """Class content to dictionary""" _fuels = [] + _hvac_maintenance = [] for _fuel in self.fuels: _fuels.append(_fuel.to_dictionary()) + for _hvac in self.maintenance_hvac: + _hvac_maintenance.append(_hvac.to_dictionary()) content = {'Maintenance': {'fuels': _fuels, - 'cost of maintaining the heating system [currency/W]': self.maintenance_heating, - 'cost of maintaining the cooling system [currency/W]': self.maintenance_cooling, + 'cost of maintaining the hvac system [currency/W]': _hvac_maintenance, 'cost of maintaining the PV system [currency/W]': self.maintenance_pv, 'cost of CO2 emissions [currency/kgCO2]': self.co2 } diff --git a/hub/catalog_factories/data_models/cost/pricing_rate.py b/hub/catalog_factories/data_models/cost/pricing_rate.py new file mode 100644 index 00000000..fe1c219c --- /dev/null +++ b/hub/catalog_factories/data_models/cost/pricing_rate.py @@ -0,0 +1,62 @@ +from typing import Union + + +class PricingRate: + def __init__(self, name=None, rate_type=None, time_range=None, units=None, values=None): + self._name = name + self._rate_type = rate_type + self._time_range = time_range + self._units = units + self._values = values + + + @property + def name(self): + """ + name of the rate + :return: str + """ + return self._name + + @property + def rate_type(self): + """ + type of rate between fixed and variable + :return: str + """ + return self._rate_type + + @property + def time_range(self) -> Union[None, str]: + """ + Get schedule time range from: + ['minute', 'hour', 'day', 'week', 'month', 'year'] + :return: None or str + """ + return self._time_range + + @property + def units(self): + """ + get the consumption unit + :return: str + """ + return self._units + + @property + def values(self): + """ + Get schedule values + :return: [Any] + """ + return self._values + + def to_dictionary(self): + """Class content to dictionary""" + content = {'Pricing': {'name': self.name, + 'time range': self.time_range, + 'type': self.rate_type, + 'units': self.units, + 'values': self.values} + } + return content diff --git a/hub/city_model_structure/building.py b/hub/city_model_structure/building.py index 660f0f7e..ac284d29 100644 --- a/hub/city_model_structure/building.py +++ b/hub/city_model_structure/building.py @@ -92,6 +92,7 @@ class Building(CityObject): logging.error('Building %s [%s] has an unexpected surface type %s.', self.name, self.aliases, surface.type) self._domestic_hot_water_peak_load = None self._fuel_consumption_breakdown = {} + self._pv_generation = {} @property def shell(self) -> Polyhedron: @@ -450,8 +451,8 @@ class Building(CityObject): monthly_values = PeakLoads(self).heating_peak_loads_from_methodology if monthly_values is None: return None - results[cte.MONTH] = [x for x in monthly_values] - results[cte.YEAR] = [max(monthly_values)] + results[cte.MONTH] = [x / cte.WATTS_HOUR_TO_JULES for x in monthly_values] + results[cte.YEAR] = [max(monthly_values) / cte.WATTS_HOUR_TO_JULES] return results @property @@ -467,8 +468,8 @@ class Building(CityObject): monthly_values = PeakLoads(self).cooling_peak_loads_from_methodology if monthly_values is None: return None - results[cte.MONTH] = [x * cte.WATTS_HOUR_TO_JULES for x in monthly_values] - results[cte.YEAR] = [max(monthly_values)] + results[cte.MONTH] = [x / cte.WATTS_HOUR_TO_JULES for x in monthly_values] + results[cte.YEAR] = [max(monthly_values) / cte.WATTS_HOUR_TO_JULES] return results @property @@ -484,7 +485,7 @@ class Building(CityObject): if monthly_values is None: return None results[cte.MONTH] = [x for x in monthly_values] - results[cte.YEAR] = [max(monthly_values)] + results[cte.YEAR] = [max(monthly_values) / cte.WATTS_HOUR_TO_JULES] return results @property @@ -911,3 +912,11 @@ class Building(CityObject): fuel_breakdown[generation_system.fuel_type][cte.DOMESTIC_HOT_WATER] = self.domestic_hot_water_consumption[cte.YEAR][0] self._fuel_consumption_breakdown = fuel_breakdown return self._fuel_consumption_breakdown + + @property + def pv_generation(self) -> dict: + return self._pv_generation + + @pv_generation.setter + def pv_generation(self, value): + self._pv_generation = value diff --git a/hub/city_model_structure/building_demand/surface.py b/hub/city_model_structure/building_demand/surface.py index f54d21e1..65f90ae3 100644 --- a/hub/city_model_structure/building_demand/surface.py +++ b/hub/city_model_structure/building_demand/surface.py @@ -47,6 +47,7 @@ class Surface: self._percentage_shared = None self._solar_collectors_area_reduction_factor = None self._global_irradiance_tilted = {} + self._installed_solar_collector_area = None @property def name(self): @@ -401,3 +402,19 @@ class Surface: :param value: dict """ self._global_irradiance_tilted = value + + @property + def installed_solar_collector_area(self): + """ + Get installed solar collector area in m2 + :return: dict + """ + return self._installed_solar_collector_area + + @installed_solar_collector_area.setter + def installed_solar_collector_area(self, value): + """ + Set installed solar collector area in m2 + :return: dict + """ + self._installed_solar_collector_area = value \ No newline at end of file diff --git a/hub/data/costs/fuel_rates.json b/hub/data/costs/fuel_rates.json new file mode 100644 index 00000000..0841e818 --- /dev/null +++ b/hub/data/costs/fuel_rates.json @@ -0,0 +1,106 @@ +{ + "rates": { + "fuels": { + "rate": [ + { + "name": "Electricity-D", + "fuel_type": "Electricity", + "rate_name": "D", + "units": "CAD/kWh", + "usage_type": "residential", + "maximum_power_demand_kW": 65, + "rate_type": "fixed", + "notes": null, + "start_date": null, + "end_date": null, + "values": [ + 0.075 + ] + }, + { + "name": "Electricity-Flex-D", + "fuel_type": "Electricity", + "rate_name": "Flex-D", + "units": "CAD/kWh", + "usage_type": "residential", + "maximum_power_demand_kW": 65, + "rate_type": "variable", + "notes": null, + "start_date": null, + "end_date": null, + "values": [ + 0.075, + 0.075, + 0.075, + 0.075, + 0.075, + 0.075, + 0.551, + 0.551, + 0.551, + 0.075, + 0.075, + 0.075, + 0.075, + 0.075, + 0.075, + 0.075, + 0.551, + 0.551, + 0.551, + 0.551, + 0.075, + 0.075, + 0.075, + 0.075 + ] + }, + { + "name": "Gas-Energir", + "fuel_type": "Gas", + "rate_name": null, + "units": "CAD/m3", + "usage_type": "residential", + "maximum_power_demand_kW": null, + "rate_type": "fixed", + "notes": null, + "start_date": null, + "end_date": null, + "values": [ + 0.4 + ] + }, + { + "name": "Diesel-Fixed", + "fuel_type": "Diesel", + "rate_name": null, + "units": "CAD/l", + "usage_type": "residential", + "maximum_power_demand_kW": null, + "rate_type": "fixed", + "notes": null, + "start_date": null, + "end_date": null, + "values": [ + 1.2 + ] + }, + { + "name": "Biomass-Fixed", + "fuel_type": "Biomass", + "rate_name": null, + "units": "CAD/kg", + "usage_type": "residential", + "maximum_power_demand_kW": null, + "rate_type": "fixed", + "notes": null, + "start_date": null, + "end_date": null, + "values": [ + 0.04 + ] + } + ] + } + } +} \ No newline at end of file diff --git a/hub/data/costs/montreal_costs_completed.xml b/hub/data/costs/montreal_costs_completed.xml index 81b67768..fc23634a 100644 --- a/hub/data/costs/montreal_costs_completed.xml +++ b/hub/data/costs/montreal_costs_completed.xml @@ -25,8 +25,8 @@ - 0 - 0 + 300 + 300 25 @@ -124,34 +124,58 @@ - 12.27 - 0 - 0.075 - - + 12.27 + 0 + Electricity-D + + + 12.27 + 0 + Electricity-Flex-D 17.71 - 0.4 + Gas-Energir 0.777 47.1 - 1.2 + Diesel-Fixed 0.846 42.6 - 0.04 + Biomass-Fixed 18 - 40 - 40 + + + 100 + + + 60 + + + 50 + + + 50 + + + 100 + + + 60 + + + 50 + + 1 30 @@ -294,31 +318,57 @@ 12.27 0 - 0.075 + Electricity-D + + + 12.27 + 0 + Electricity-Flex-D 17.71 - 0.0640 + Gas-Energir 0.777 47.1 - 1.2 + Diesel-Fixed 0.846 42.6 - 0.04 + Biomass-Fixed 18 - 40 - 40 - 0 + + + 100 + + + 60 + + + 50 + + + 50 + + + 100 + + + 60 + + + 50 + + + 1 30 diff --git a/main.py b/main.py index 798b5d4c..e69de29b 100644 --- a/main.py +++ b/main.py @@ -1,45 +0,0 @@ -from scripts.geojson_creator import process_geojson -from pathlib import Path -import subprocess -from scripts.ep_run_enrich import energy_plus_workflow -from hub.imports.geometry_factory import GeometryFactory -from hub.helpers.dictionaries import Dictionaries -from hub.imports.construction_factory import ConstructionFactory -from hub.imports.usage_factory import UsageFactory -from hub.imports.weather_factory import WeatherFactory -from hub.imports.results_factory import ResultFactory -from scripts.energy_system_analysis_report import EnergySystemAnalysisReport -from scripts import random_assignation -from hub.imports.energy_systems_factory import EnergySystemsFactory -from scripts.energy_system_sizing import SystemSizing -from scripts.energy_system_retrofit_results import system_results, new_system_results -from scripts.energy_system_sizing_and_simulation_factory import EnergySystemsSimulationFactory -from scripts.costs.cost import Cost -from scripts.costs.constants import SKIN_RETROFIT_AND_SYSTEM_RETROFIT_AND_PV, SYSTEM_RETROFIT_AND_PV -import hub.helpers.constants as cte -from hub.exports.exports_factory import ExportsFactory -# Specify the GeoJSON file path -geojson_file = process_geojson(x=-73.5681295982132, y=45.49218262677643, diff=0.0001) -file_path = (Path(__file__).parent / 'input_files' / 'output_buildings.geojson') -# Specify the output path for the PDF file -output_path = (Path(__file__).parent / 'out_files').resolve() -# Create city object from GeoJSON file -city = GeometryFactory('geojson', - path=file_path, - height_field='height', - year_of_construction_field='year_of_construction', - function_field='function', - function_to_hub=Dictionaries().montreal_function_to_hub_function).city -# Enrich city data -ConstructionFactory('nrcan', city).enrich() - -UsageFactory('nrcan', city).enrich() -WeatherFactory('epw', city).enrich() -energy_plus_workflow(city) -random_assignation.call_random(city.buildings, random_assignation.residential_new_systems_percentage) -EnergySystemsFactory('montreal_future', city).enrich() - -for building in city.buildings: - EnergySystemsSimulationFactory('archetype13', building=building, output_path=output_path).enrich() - -print('test') diff --git a/scripts/costs/capital_costs.py b/scripts/costs/capital_costs.py index 68d751d8..c87f3eba 100644 --- a/scripts/costs/capital_costs.py +++ b/scripts/costs/capital_costs.py @@ -12,7 +12,8 @@ import numpy_financial as npf from hub.city_model_structure.building import Building import hub.helpers.constants as cte from scripts.costs.configuration import Configuration -from scripts.costs.constants import SKIN_RETROFIT, SKIN_RETROFIT_AND_SYSTEM_RETROFIT_AND_PV, SYSTEM_RETROFIT_AND_PV +from scripts.costs.constants import (SKIN_RETROFIT, SKIN_RETROFIT_AND_SYSTEM_RETROFIT_AND_PV, + SYSTEM_RETROFIT_AND_PV, CURRENT_STATUS, PV) from scripts.costs.cost_base import CostBase @@ -36,7 +37,6 @@ class CapitalCosts(CostBase): 'D3050_other_hvac_ahu', 'D3060_storage_systems', 'D40_dhw', - 'D5020_lighting_and_branch_wiring' ], dtype='float' ) @@ -50,7 +50,6 @@ class CapitalCosts(CostBase): self._yearly_capital_costs.loc[0, 'D3080_other_hvac_ahu'] = 0 self._yearly_capital_costs.loc[0, 'D3060_storage_systems'] = 0 self._yearly_capital_costs.loc[0, 'D40_dhw'] = 0 - # self._yearly_capital_costs.loc[0, 'D5020_lighting_and_branch_wiring'] = 0 self._yearly_capital_incomes = pd.DataFrame( index=self._rng, @@ -70,12 +69,14 @@ class CapitalCosts(CostBase): for roof in self._building.roofs: self._surface_pv += roof.solid_polygon.area * roof.solar_collectors_area_reduction_factor + for roof in self._building.roofs: + if roof.installed_solar_collector_area is not None: + self._surface_pv += roof.installed_solar_collector_area + else: + self._surface_pv += roof.solid_polygon.area * roof.solar_collectors_area_reduction_factor def calculate(self) -> tuple[pd.DataFrame, pd.DataFrame]: - if self._configuration.retrofit_scenario in (SKIN_RETROFIT, SKIN_RETROFIT_AND_SYSTEM_RETROFIT_AND_PV): - self.skin_capital_cost() - if self._configuration.retrofit_scenario in (SYSTEM_RETROFIT_AND_PV, SKIN_RETROFIT_AND_SYSTEM_RETROFIT_AND_PV): - self.energy_system_capital_cost() - + self.skin_capital_cost() + self.energy_system_capital_cost() self.skin_yearly_capital_costs() self.yearly_energy_system_costs() self.yearly_incomes() @@ -106,10 +107,11 @@ class CapitalCosts(CostBase): capital_cost_transparent = surface_transparent * chapter.item('B2020_transparent').refurbishment[0] capital_cost_roof = surface_roof * chapter.item('B3010_opaque_roof').refurbishment[0] capital_cost_ground = surface_ground * chapter.item('B1010_superstructure').refurbishment[0] - self._yearly_capital_costs.loc[0, 'B2010_opaque_walls'] = capital_cost_opaque * self._own_capital - self._yearly_capital_costs.loc[0, 'B2020_transparent'] = capital_cost_transparent * self._own_capital - self._yearly_capital_costs.loc[0, 'B3010_opaque_roof'] = capital_cost_roof * self._own_capital - self._yearly_capital_costs.loc[0, 'B1010_superstructure'] = capital_cost_ground * self._own_capital + if self._configuration.retrofit_scenario not in (SYSTEM_RETROFIT_AND_PV, CURRENT_STATUS, PV): + self._yearly_capital_costs.loc[0, 'B2010_opaque_walls'] = capital_cost_opaque * self._own_capital + self._yearly_capital_costs.loc[0, 'B2020_transparent'] = capital_cost_transparent * self._own_capital + self._yearly_capital_costs.loc[0, 'B3010_opaque_roof'] = capital_cost_roof * self._own_capital + self._yearly_capital_costs.loc[0, 'B1010_superstructure'] = capital_cost_ground * self._own_capital capital_cost_skin = capital_cost_opaque + capital_cost_ground + capital_cost_transparent + capital_cost_roof return capital_cost_opaque, capital_cost_transparent, capital_cost_roof, capital_cost_ground, capital_cost_skin @@ -147,18 +149,13 @@ class CapitalCosts(CostBase): def energy_system_capital_cost(self): chapter = self._capital_costs_chapter.chapter('D_services') - energy_system_components = self.system_components() - system_components = energy_system_components[0] - component_categories = energy_system_components[1] - component_sizes = energy_system_components[-1] + system_components, component_categories, component_sizes = self.system_components() capital_cost_heating_and_cooling_equipment = 0 capital_cost_domestic_hot_water_equipment = 0 capital_cost_energy_storage_equipment = 0 capital_cost_distribution_equipment = 0 capital_cost_lighting = 0 capital_cost_pv = self._surface_pv * chapter.item('D2010_photovoltaic_system').initial_investment[0] - # capital_cost_lighting = self._total_floor_area * \ - # chapter.item('D5020_lighting_and_branch_wiring').initial_investment[0] for (i, component) in enumerate(system_components): if component_categories[i] == 'generation': capital_cost_heating_and_cooling_equipment += chapter.item(component).initial_investment[0] * component_sizes[i] @@ -171,26 +168,31 @@ class CapitalCosts(CostBase): else: capital_cost_energy_storage_equipment += chapter.item(component).initial_investment[0] * component_sizes[i] - self._yearly_capital_costs.loc[0, 'D2010_photovoltaic_system'] = capital_cost_pv - self._yearly_capital_costs.loc[0, 'D3020_heat_and_cooling_generating_systems'] = ( - capital_cost_heating_and_cooling_equipment * self._own_capital) - self._yearly_capital_costs.loc[0, 'D3040_distribution_systems'] = ( - capital_cost_distribution_equipment * self._own_capital) - self._yearly_capital_costs.loc[0, 'D3060_storage_systems'] = ( - capital_cost_energy_storage_equipment * self._own_capital) - self._yearly_capital_costs.loc[0, 'D40_dhw'] = ( - capital_cost_domestic_hot_water_equipment * self._own_capital) - # self._yearly_capital_costs.loc[0, 'D5020_lighting_and_branch_wiring'] = capital_cost_lighting * self._own_capital - capital_cost_hvac = capital_cost_heating_and_cooling_equipment + capital_cost_distribution_equipment + capital_cost_energy_storage_equipment + capital_cost_domestic_hot_water_equipment + if self._configuration.retrofit_scenario in (SKIN_RETROFIT_AND_SYSTEM_RETROFIT_AND_PV, SYSTEM_RETROFIT_AND_PV, PV): + self._yearly_capital_costs.loc[0, 'D2010_photovoltaic_system'] = capital_cost_pv + if self._configuration.retrofit_scenario in (SYSTEM_RETROFIT_AND_PV, SKIN_RETROFIT_AND_SYSTEM_RETROFIT_AND_PV): + self._yearly_capital_costs.loc[0, 'D3020_heat_and_cooling_generating_systems'] = ( + capital_cost_heating_and_cooling_equipment * self._own_capital) + self._yearly_capital_costs.loc[0, 'D3040_distribution_systems'] = ( + capital_cost_distribution_equipment * self._own_capital) + self._yearly_capital_costs.loc[0, 'D3060_storage_systems'] = ( + capital_cost_energy_storage_equipment * self._own_capital) + self._yearly_capital_costs.loc[0, 'D40_dhw'] = ( + capital_cost_domestic_hot_water_equipment * self._own_capital) + capital_cost_hvac = (capital_cost_heating_and_cooling_equipment + capital_cost_distribution_equipment + + capital_cost_energy_storage_equipment + capital_cost_domestic_hot_water_equipment) return (capital_cost_pv, capital_cost_heating_and_cooling_equipment, capital_cost_distribution_equipment, capital_cost_energy_storage_equipment, capital_cost_domestic_hot_water_equipment, capital_cost_lighting, capital_cost_hvac) def yearly_energy_system_costs(self): chapter = self._capital_costs_chapter.chapter('D_services') system_investment_costs = self.energy_system_capital_cost() - system_components = self.system_components()[0] - component_categories = self.system_components()[1] - component_sizes = self.system_components()[2] + system_components, component_categories, component_sizes = self.system_components() + pv = False + for energy_system in self._building.energy_systems: + for generation_system in energy_system.generation_systems: + if generation_system.system_type == cte.PHOTOVOLTAIC: + pv = True for year in range(1, self._configuration.number_of_years): costs_increase = math.pow(1 + self._configuration.consumer_price_index, year) self._yearly_capital_costs.loc[year, 'D2010_photovoltaic_system'] = ( @@ -228,23 +230,7 @@ class CapitalCosts(CostBase): system_investment_costs[4] * self._configuration.percentage_credit ) ) - # self._yearly_capital_costs.loc[year, 'D5020_lighting_and_branch_wiring'] = ( - # -npf.pmt( - # self._configuration.interest_rate, - # self._configuration.credit_years, - # system_investment_costs[5] * self._configuration.percentage_credit - # ) - # ) - # if (year % chapter.item('D5020_lighting_and_branch_wiring').lifetime) == 0: - # reposition_cost_lighting = ( - # self._total_floor_area * chapter.item('D5020_lighting_and_branch_wiring').reposition[0] * costs_increase - # ) - # self._yearly_capital_costs.loc[year, 'D5020_lighting_and_branch_wiring'] += reposition_cost_lighting - if self._configuration.retrofit_scenario in (SYSTEM_RETROFIT_AND_PV, SKIN_RETROFIT_AND_SYSTEM_RETROFIT_AND_PV): - if (year % chapter.item('D2010_photovoltaic_system').lifetime) == 0: - self._yearly_capital_costs.loc[year, 'D2010_photovoltaic_system'] += ( - self._surface_pv * chapter.item('D2010_photovoltaic_system').reposition[0] * costs_increase - ) + if self._configuration.retrofit_scenario not in (SKIN_RETROFIT, PV): for (i, component) in enumerate(system_components): if (year % chapter.item(component).lifetime) == 0 and year != (self._configuration.number_of_years - 1): if component_categories[i] == 'generation': @@ -259,6 +245,17 @@ class CapitalCosts(CostBase): else: reposition_cost_energy_storage_equipment = chapter.item(component).initial_investment[0] * component_sizes[i] * costs_increase self._yearly_capital_costs.loc[year, 'D3060_storage_systems'] += reposition_cost_energy_storage_equipment + if self._configuration.retrofit_scenario == CURRENT_STATUS and pv: + if (year % chapter.item('D2010_photovoltaic_system').lifetime) == 0: + self._yearly_capital_costs.loc[year, 'D2010_photovoltaic_system'] += ( + self._surface_pv * chapter.item('D2010_photovoltaic_system').reposition[0] * costs_increase + ) + elif self._configuration.retrofit_scenario in (PV, SYSTEM_RETROFIT_AND_PV, + SKIN_RETROFIT_AND_SYSTEM_RETROFIT_AND_PV): + if (year % chapter.item('D2010_photovoltaic_system').lifetime) == 0: + self._yearly_capital_costs.loc[year, 'D2010_photovoltaic_system'] += ( + self._surface_pv * chapter.item('D2010_photovoltaic_system').reposition[0] * costs_increase + ) def system_components(self): system_components = [] @@ -308,7 +305,7 @@ class CapitalCosts(CostBase): if distribution_systems is not None: for distribution_system in distribution_systems: component_categories.append('distribution') - sizes.append(self._building.cooling_peak_load[cte.YEAR][0] / 3.6e6) + sizes.append(self._building.cooling_peak_load[cte.YEAR][0] / 1000) system_components.append('D3040_distribution_systems') return system_components, component_categories, sizes diff --git a/scripts/costs/configuration.py b/scripts/costs/configuration.py index f868da79..3d5b9485 100644 --- a/scripts/costs/configuration.py +++ b/scripts/costs/configuration.py @@ -28,7 +28,8 @@ class Configuration: factories_handler, retrofit_scenario, fuel_type, - dictionary + dictionary, + fuel_tariffs ): self._number_of_years = number_of_years self._percentage_credit = percentage_credit @@ -45,6 +46,7 @@ class Configuration: self._retrofit_scenario = retrofit_scenario self._fuel_type = fuel_type self._dictionary = dictionary + self._fuel_tariffs = fuel_tariffs @property def number_of_years(self): @@ -227,3 +229,10 @@ class Configuration: Get hub function to cost function dictionary """ return self._dictionary + + @property + def fuel_tariffs(self): + """ + Get fuel tariffs + """ + return self._fuel_tariffs diff --git a/scripts/costs/constants.py b/scripts/costs/constants.py index cbf11e57..18543adf 100644 --- a/scripts/costs/constants.py +++ b/scripts/costs/constants.py @@ -11,9 +11,11 @@ CURRENT_STATUS = 0 SKIN_RETROFIT = 1 SYSTEM_RETROFIT_AND_PV = 2 SKIN_RETROFIT_AND_SYSTEM_RETROFIT_AND_PV = 3 +PV = 4 RETROFITTING_SCENARIOS = [ CURRENT_STATUS, SKIN_RETROFIT, SYSTEM_RETROFIT_AND_PV, - SKIN_RETROFIT_AND_SYSTEM_RETROFIT_AND_PV + SKIN_RETROFIT_AND_SYSTEM_RETROFIT_AND_PV, + PV ] diff --git a/scripts/costs/cost.py b/scripts/costs/cost.py index 996c4724..2ebd1ffc 100644 --- a/scripts/costs/cost.py +++ b/scripts/costs/cost.py @@ -40,7 +40,10 @@ class Cost: retrofitting_year_construction=2020, factories_handler='montreal_new', retrofit_scenario=CURRENT_STATUS, - dictionary=None): + dictionary=None, + fuel_tariffs=None): + if fuel_tariffs is None: + fuel_tariffs = ['Electricity-D', 'Gas-Energir'] if dictionary is None: dictionary = Dictionaries().hub_function_to_montreal_custom_costs_function self._building = building @@ -57,7 +60,8 @@ class Cost: factories_handler, retrofit_scenario, fuel_type, - dictionary) + dictionary, + fuel_tariffs) @property def building(self) -> Building: @@ -94,7 +98,6 @@ class Cost: global_capital_costs['D3050_other_hvac_ahu'] + global_capital_costs['D3060_storage_systems'] + global_capital_costs['D40_dhw'] + - global_capital_costs['D5020_lighting_and_branch_wiring'] + global_capital_costs['D2010_photovoltaic_system'] ) diff --git a/scripts/costs/peak_load.py b/scripts/costs/peak_load.py index 108f748a..422f563b 100644 --- a/scripts/costs/peak_load.py +++ b/scripts/costs/peak_load.py @@ -45,7 +45,7 @@ class PeakLoad: conditioning_peak[i] = self._building.heating_peak_load[cte.MONTH][i] * heating else: conditioning_peak[i] = self._building.cooling_peak_load[cte.MONTH][i] * cooling - monthly_electricity_peak[i] += 0.8 * conditioning_peak[i] / 3600 + monthly_electricity_peak[i] += 0.8 * conditioning_peak[i] electricity_peak_load_results = pd.DataFrame( monthly_electricity_peak, diff --git a/scripts/costs/total_maintenance_costs.py b/scripts/costs/total_maintenance_costs.py index 81a88de8..0f5b7dfc 100644 --- a/scripts/costs/total_maintenance_costs.py +++ b/scripts/costs/total_maintenance_costs.py @@ -25,6 +25,7 @@ class TotalMaintenanceCosts(CostBase): columns=[ 'Heating_maintenance', 'Cooling_maintenance', + 'DHW_maintenance', 'PV_maintenance' ], dtype='float' @@ -39,15 +40,76 @@ class TotalMaintenanceCosts(CostBase): archetype = self._archetype # todo: change area pv when the variable exists roof_area = 0 - for roof in building.roofs: - roof_area += roof.solid_polygon.area - surface_pv = roof_area * 0.5 + surface_pv = 0 + for roof in self._building.roofs: + if roof.installed_solar_collector_area is not None: + surface_pv += roof.installed_solar_collector_area + else: + surface_pv = roof_area * 0.5 - peak_heating = building.heating_peak_load[cte.YEAR][0] / 3.6e6 - peak_cooling = building.cooling_peak_load[cte.YEAR][0] / 3.6e6 + energy_systems = building.energy_systems + maintenance_heating_0 = 0 + maintenance_cooling_0 = 0 + maintenance_dhw_0 = 0 + heating_equipments = {} + cooling_equipments = {} + dhw_equipments = {} + for energy_system in energy_systems: + if cte.COOLING in energy_system.demand_types: + for generation_system in energy_system.generation_systems: + if generation_system.system_type == cte.HEAT_PUMP and generation_system.source_medium == cte.AIR: + cooling_equipments['air_source_heat_pump'] = generation_system.nominal_cooling_output / 1000 + elif generation_system.system_type == cte.HEAT_PUMP and generation_system.source_medium == cte.GROUND: + cooling_equipments['ground_source_heat_pump'] = generation_system.nominal_cooling_output / 1000 + elif generation_system.system_type == cte.HEAT_PUMP and generation_system.source_medium == cte.WATER: + cooling_equipments['water_source_heat_pump'] = generation_system.nominal_cooling_output / 1000 + else: + cooling_equipments['general_cooling_equipment'] = generation_system.nominal_cooling_output / 1000 + if cte.HEATING in energy_system.demand_types: + for generation_system in energy_system.generation_systems: + if generation_system.system_type == cte.HEAT_PUMP and generation_system.source_medium == cte.AIR: + heating_equipments['air_source_heat_pump'] = generation_system.nominal_heat_output / 1000 + elif generation_system.system_type == cte.HEAT_PUMP and generation_system.source_medium == cte.GROUND: + heating_equipments['ground_source_heat_pump'] = generation_system.nominal_heat_output / 1000 + elif generation_system.system_type == cte.HEAT_PUMP and generation_system.source_medium == cte.WATER: + heating_equipments['water_source_heat_pump'] = generation_system.nominal_heat_output / 1000 + elif generation_system.system_type == cte.BOILER and generation_system.fuel_type == cte.GAS: + heating_equipments['gas_boiler'] = generation_system.nominal_heat_output / 1000 + elif generation_system.system_type == cte.BOILER and generation_system.fuel_type == cte.ELECTRICITY: + heating_equipments['electric_boiler'] = generation_system.nominal_heat_output / 1000 + else: + heating_equipments['general_heating_equipment'] = generation_system.nominal_heat_output / 1000 + if cte.DOMESTIC_HOT_WATER in energy_system.demand_types and cte.HEATING not in energy_system.demand_types: + for generation_system in energy_system.generation_systems: + if generation_system.system_type == cte.HEAT_PUMP and generation_system.source_medium == cte.AIR: + dhw_equipments['air_source_heat_pump'] = generation_system.nominal_heat_output / 1000 + elif generation_system.system_type == cte.HEAT_PUMP and generation_system.source_medium == cte.GROUND: + dhw_equipments['ground_source_heat_pump'] = generation_system.nominal_heat_output / 1000 + elif generation_system.system_type == cte.HEAT_PUMP and generation_system.source_medium == cte.WATER: + dhw_equipments['water_source_heat_pump'] = generation_system.nominal_heat_output / 1000 + elif generation_system.system_type == cte.BOILER and generation_system.fuel_type == cte.GAS: + dhw_equipments['gas_boiler'] = generation_system.nominal_heat_output / 1000 + elif generation_system.system_type == cte.BOILER and generation_system.fuel_type == cte.ELECTRICITY: + dhw_equipments['electric_boiler'] = generation_system.nominal_heat_output / 1000 + else: + dhw_equipments['general_heating_equipment'] = generation_system.nominal_heat_output / 1000 + + print(dhw_equipments) + for heating_equipment in heating_equipments: + component = self.search_hvac_equipment(heating_equipment) + maintenance_cost = component.maintenance[0] + maintenance_heating_0 += (heating_equipments[heating_equipment] * maintenance_cost) + + for cooling_equipment in cooling_equipments: + component = self.search_hvac_equipment(cooling_equipment) + maintenance_cost = component.maintenance[0] + maintenance_cooling_0 += (cooling_equipments[cooling_equipment] * maintenance_cost) + + for dhw_equipment in dhw_equipments: + component = self.search_hvac_equipment(dhw_equipment) + maintenance_cost = component.maintenance[0] + maintenance_dhw_0 += (dhw_equipments[dhw_equipment] * maintenance_cost) - maintenance_heating_0 = peak_heating * archetype.operational_cost.maintenance_heating - maintenance_cooling_0 = peak_cooling * archetype.operational_cost.maintenance_cooling maintenance_pv_0 = surface_pv * archetype.operational_cost.maintenance_pv for year in range(1, self._configuration.number_of_years + 1): @@ -58,8 +120,18 @@ class TotalMaintenanceCosts(CostBase): self._yearly_maintenance_costs.loc[year, 'Cooling_maintenance'] = ( maintenance_cooling_0 * costs_increase ) + self._yearly_maintenance_costs.loc[year, 'DHW_maintenance'] = ( + maintenance_dhw_0 * costs_increase + ) self._yearly_maintenance_costs.loc[year, 'PV_maintenance'] = ( maintenance_pv_0 * costs_increase ) self._yearly_maintenance_costs.fillna(0, inplace=True) return self._yearly_maintenance_costs + + def search_hvac_equipment(self, equipment_type): + for component in self._archetype.operational_cost.maintenance_hvac: + if component.type == equipment_type: + return component + + diff --git a/scripts/costs/total_operational_costs.py b/scripts/costs/total_operational_costs.py index 6b0c1e27..dc672fa1 100644 --- a/scripts/costs/total_operational_costs.py +++ b/scripts/costs/total_operational_costs.py @@ -42,48 +42,68 @@ class TotalOperationalCosts(CostBase): factor = total_floor_area / 80 else: factor = 1 - total_electricity_consumption = sum(self._building.energy_consumption_breakdown[cte.ELECTRICITY].values()) + total_electricity_consumption = sum(self._building.energy_consumption_breakdown[cte.ELECTRICITY].values()) / 3600 peak_electricity_load = PeakLoad(self._building).electricity_peak_load peak_load_value = peak_electricity_load.max(axis=1) peak_electricity_demand = peak_load_value[1] / 1000 # self._peak_electricity_demand adapted to kW - fuels = archetype.operational_cost.fuels - for fuel in fuels: - if fuel.type in fuel_consumption_breakdown.keys(): - if fuel.type == cte.ELECTRICITY: + for system_fuel in self._configuration.fuel_type: + fuel = None + for fuel_tariff in self._configuration.fuel_tariffs: + if system_fuel in fuel_tariff: + fuel = self.search_fuel(system_fuel, fuel_tariff) + if fuel.type == cte.ELECTRICITY: + if fuel.variable.rate_type == 'fixed': variable_electricity_cost_year_0 = ( - total_electricity_consumption * fuel.variable[0] / 1000 + total_electricity_consumption * float(fuel.variable.values[0]) / 1000 ) - peak_electricity_cost_year_0 = peak_electricity_demand * fuel.fixed_power * 12 - monthly_electricity_cost_year_0 = fuel.fixed_monthly * 12 * factor - for year in range(1, self._configuration.number_of_years + 1): - price_increase_electricity = math.pow(1 + self._configuration.electricity_price_index, year) - price_increase_peak_electricity = math.pow(1 + self._configuration.electricity_peak_index, year) - self._yearly_operational_costs.at[year, 'Fixed Costs Electricity Peak'] = ( - peak_electricity_cost_year_0 * price_increase_peak_electricity - ) - self._yearly_operational_costs.at[year, 'Fixed Costs Electricity Monthly'] = ( - monthly_electricity_cost_year_0 * price_increase_peak_electricity - ) - if not isinstance(variable_electricity_cost_year_0, pd.DataFrame): - variable_costs_electricity = variable_electricity_cost_year_0 * price_increase_electricity - else: - variable_costs_electricity = float(variable_electricity_cost_year_0.iloc[0] * price_increase_electricity) - self._yearly_operational_costs.at[year, 'Variable Costs Electricity'] = ( - variable_costs_electricity - ) else: - fuel_fixed_cost = fuel.fixed_monthly * 12 * factor - if fuel.type == cte.BIOMASS: - conversion_factor = 1 + hourly_electricity_consumption = self.hourly_fuel_consumption_profile(fuel.type) + hourly_electricity_price_profile = fuel.variable.values * len(hourly_electricity_consumption) + hourly_electricity_price = [hourly_electricity_consumption[i] / 1000 * hourly_electricity_price_profile[i] + for i in range(len(hourly_electricity_consumption))] + variable_electricity_cost_year_0 = sum(hourly_electricity_price) + peak_electricity_cost_year_0 = peak_electricity_demand * fuel.fixed_power * 12 + monthly_electricity_cost_year_0 = fuel.fixed_monthly * 12 * factor + for year in range(1, self._configuration.number_of_years + 1): + price_increase_electricity = math.pow(1 + self._configuration.electricity_price_index, year) + price_increase_peak_electricity = math.pow(1 + self._configuration.electricity_peak_index, year) + self._yearly_operational_costs.at[year, 'Fixed Costs Electricity Peak'] = ( + peak_electricity_cost_year_0 * price_increase_peak_electricity + ) + self._yearly_operational_costs.at[year, 'Fixed Costs Electricity Monthly'] = ( + monthly_electricity_cost_year_0 * price_increase_peak_electricity + ) + if not isinstance(variable_electricity_cost_year_0, pd.DataFrame): + variable_costs_electricity = variable_electricity_cost_year_0 * price_increase_electricity else: - conversion_factor = fuel.density[0] + variable_costs_electricity = float(variable_electricity_cost_year_0.iloc[0] * price_increase_electricity) + self._yearly_operational_costs.at[year, 'Variable Costs Electricity'] = ( + variable_costs_electricity + ) + else: + fuel_fixed_cost = fuel.fixed_monthly * 12 * factor + if fuel.type == cte.BIOMASS: + conversion_factor = 1 + else: + conversion_factor = fuel.density[0] + if fuel.variable.rate_type == 'fixed': variable_cost_fuel = ( - ((sum(fuel_consumption_breakdown[fuel.type].values()) * 3600)/(1e6*fuel.lower_heating_value[0] * conversion_factor)) * fuel.variable[0]) - for year in range(1, self._configuration.number_of_years + 1): - price_increase_gas = math.pow(1 + self._configuration.gas_price_index, year) - self._yearly_operational_costs.at[year, f'Fixed Costs {fuel.type}'] = fuel_fixed_cost * price_increase_gas - self._yearly_operational_costs.at[year, f'Variable Costs {fuel.type}'] = ( - variable_cost_fuel * price_increase_gas) + (sum(fuel_consumption_breakdown[fuel.type].values()) / ( + 1e6 * fuel.lower_heating_value[0] * conversion_factor)) * fuel.variable.values[0]) + + else: + hourly_fuel_consumption = self.hourly_fuel_consumption_profile(fuel.type) + hourly_fuel_price_profile = fuel.variable.values * len(hourly_fuel_consumption) + hourly_fuel_price = [hourly_fuel_consumption[i] / ( + 1e6 * fuel.lower_heating_value[0] * conversion_factor) * hourly_fuel_price_profile[i] + for i in range(len(hourly_fuel_consumption))] + variable_cost_fuel = sum(hourly_fuel_price) + + for year in range(1, self._configuration.number_of_years + 1): + price_increase_gas = math.pow(1 + self._configuration.gas_price_index, year) + self._yearly_operational_costs.at[year, f'Fixed Costs {fuel.type}'] = fuel_fixed_cost * price_increase_gas + self._yearly_operational_costs.at[year, f'Variable Costs {fuel.type}'] = ( + variable_cost_fuel * price_increase_gas) self._yearly_operational_costs.fillna(0, inplace=True) return self._yearly_operational_costs @@ -102,3 +122,116 @@ class TotalOperationalCosts(CostBase): return columns_list + def search_fuel(self, system_fuel, tariff): + fuels = self._archetype.operational_cost.fuels + for fuel in fuels: + if system_fuel == fuel.type and tariff == fuel.variable.name: + return fuel + raise KeyError(f'fuel {system_fuel} with {tariff} tariff not found') + + + def hourly_fuel_consumption_profile(self, fuel_type): + hourly_fuel_consumption = [] + energy_systems = self._building.energy_systems + if fuel_type == cte.ELECTRICITY: + appliance = self._building.appliances_electrical_demand[cte.HOUR] + lighting = self._building.lighting_electrical_demand[cte.HOUR] + elec_heating = 0 + elec_cooling = 0 + elec_dhw = 0 + if cte.HEATING in self._building.energy_consumption_breakdown[cte.ELECTRICITY]: + elec_heating = 1 + if cte.COOLING in self._building.energy_consumption_breakdown[cte.ELECTRICITY]: + elec_cooling = 1 + if cte.DOMESTIC_HOT_WATER in self._building.energy_consumption_breakdown[cte.ELECTRICITY]: + elec_dhw = 1 + heating = None + cooling = None + dhw = None + + if elec_heating == 1: + for energy_system in energy_systems: + if cte.HEATING in energy_system.demand_types: + for generation_system in energy_system.generation_systems: + if generation_system.fuel_type == cte.ELECTRICITY: + if cte.HEATING in generation_system.energy_consumption: + heating = generation_system.energy_consumption[cte.HEATING][cte.HOUR] + else: + if len(energy_system.generation_systems) > 1: + heating = [x / 2 for x in self._building.heating_consumption[cte.HOUR]] + else: + heating = self._building.heating_consumption[cte.HOUR] + + if elec_dhw == 1: + for energy_system in energy_systems: + if cte.DOMESTIC_HOT_WATER in energy_system.demand_types: + for generation_system in energy_system.generation_systems: + if generation_system.fuel_type == cte.ELECTRICITY: + if cte.DOMESTIC_HOT_WATER in generation_system.energy_consumption: + dhw = generation_system.energy_consumption[cte.DOMESTIC_HOT_WATER][cte.HOUR] + else: + if len(energy_system.generation_systems) > 1: + dhw = [x / 2 for x in self._building.domestic_hot_water_consumption[cte.HOUR]] + else: + dhw = self._building.domestic_hot_water_consumption[cte.HOUR] + + if elec_cooling == 1: + for energy_system in energy_systems: + if cte.COOLING in energy_system.demand_types: + for generation_system in energy_system.generation_systems: + if cte.COOLING in generation_system.energy_consumption: + cooling = generation_system.energy_consumption[cte.COOLING][cte.HOUR] + else: + if len(energy_system.generation_systems) > 1: + cooling = [x / 2 for x in self._building.cooling_consumption[cte.HOUR]] + else: + cooling = self._building.cooling_consumption[cte.HOUR] + + for i in range(len(self._building.heating_demand[cte.HOUR])): + hourly = 0 + hourly += appliance[i] / 3600 + hourly += lighting[i] / 3600 + if heating is not None: + hourly += heating[i] / 3600 + if cooling is not None: + hourly += cooling[i] / 3600 + if dhw is not None: + dhw += dhw[i] / 3600 + hourly_fuel_consumption.append(hourly) + else: + heating = None + dhw = None + if cte.HEATING in self._building.energy_consumption_breakdown[fuel_type]: + for energy_system in energy_systems: + if cte.HEATING in energy_system.demand_types: + for generation_system in energy_system.generation_systems: + if cte.HEATING in generation_system.energy_consumption: + heating = generation_system.energy_consumption[cte.HEATING][cte.HOUR] + else: + if len(energy_system.generation_systems) > 1: + heating = [x / 2 for x in self._building.heating_consumption[cte.HOUR]] + else: + heating = self._building.heating_consumption[cte.HOUR] + if cte.DOMESTIC_HOT_WATER in self._building.energy_consumption_breakdown[fuel_type]: + for energy_system in energy_systems: + if cte.DOMESTIC_HOT_WATER in energy_system.demand_types: + for generation_system in energy_system.generation_systems: + if cte.DOMESTIC_HOT_WATER in generation_system.energy_consumption: + dhw = generation_system.energy_consumption[cte.DOMESTIC_HOT_WATER][cte.HOUR] + else: + if len(energy_system.generation_systems) > 1: + dhw = [x / 2 for x in self._building.domestic_hot_water_consumption[cte.HOUR]] + else: + dhw = self._building.domestic_hot_water_consumption[cte.HOUR] + + for i in range(len(self._building.heating_demand[cte.HOUR])): + hourly = 0 + if heating is not None: + hourly += heating[i] / 3600 + if dhw is not None: + hourly += dhw[i] / 3600 + hourly_fuel_consumption.append(hourly) + return hourly_fuel_consumption + + + diff --git a/scripts/energy_system_sizing.py b/scripts/energy_system_sizing.py index e626bc7d..77f876b4 100644 --- a/scripts/energy_system_sizing.py +++ b/scripts/energy_system_sizing.py @@ -52,17 +52,17 @@ class SystemSizing: if cte.HEATING in demand_types: if len(generation_systems) == 1: for generation in generation_systems: - generation.nominal_heat_output = building.heating_peak_load[cte.YEAR][0] / 3600 + generation.nominal_heat_output = building.heating_peak_load[cte.YEAR][0] else: for generation in generation_systems: - generation.nominal_heat_output = building.heating_peak_load[cte.YEAR][0] / (len(generation_systems) * 3600) + generation.nominal_heat_output = building.heating_peak_load[cte.YEAR][0] / (len(generation_systems)) elif cte.COOLING in demand_types: if len(generation_systems) == 1: for generation in generation_systems: - generation.nominal_cooling_output = building.cooling_peak_load[cte.YEAR][0] / 3600 + generation.nominal_cooling_output = building.cooling_peak_load[cte.YEAR][0] else: for generation in generation_systems: - generation.nominal_heat_output = building.cooling_peak_load[cte.YEAR][0] / (len(generation_systems) * 3600) + generation.nominal_heat_output = building.cooling_peak_load[cte.YEAR][0] / (len(generation_systems)) diff --git a/scripts/random_assignation.py b/scripts/random_assignation.py index 958f269d..f82ab89f 100644 --- a/scripts/random_assignation.py +++ b/scripts/random_assignation.py @@ -15,8 +15,8 @@ from hub.city_model_structure.building import Building energy_systems_format = 'montreal_custom' # parameters: -residential_systems_percentage = {'system 1 gas': 44, - 'system 1 electricity': 6, +residential_systems_percentage = {'system 1 gas': 100, + 'system 1 electricity': 0, 'system 2 gas': 0, 'system 2 electricity': 0, 'system 3 and 4 gas': 0, @@ -25,8 +25,8 @@ residential_systems_percentage = {'system 1 gas': 44, 'system 5 electricity': 0, 'system 6 gas': 0, 'system 6 electricity': 0, - 'system 8 gas': 44, - 'system 8 electricity': 6} + 'system 8 gas': 0, + 'system 8 electricity': 0} residential_new_systems_percentage = {'PV+ASHP+GasBoiler+TES': 0, 'PV+4Pipe+DHW': 100, From ee6dc92b40ee4a0b6efdc5d1be4e37e8c52d36dc Mon Sep 17 00:00:00 2001 From: s_ranjbar Date: Mon, 24 Jun 2024 19:21:28 -0400 Subject: [PATCH 07/15] fix: new archetype to model a system with central heating and decenral cooling and dhw created --- .../montreal_future_systems.xml | 31 +++++++++++++++++++ scripts/random_assignation.py | 1 + tests/test_systems_catalog.py | 6 ++-- 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/hub/data/energy_systems/montreal_future_systems.xml b/hub/data/energy_systems/montreal_future_systems.xml index 455235ae..f9d43be4 100644 --- a/hub/data/energy_systems/montreal_future_systems.xml +++ b/hub/data/energy_systems/montreal_future_systems.xml @@ -1438,6 +1438,29 @@ 27 + + 11 + Central Heating System َASHP Gas-Boiler TES + schemas/ASHP+TES+GasBoiler.jpg + + heating + + + 23 + 16 + + + + 12 + Unitary ASHP Cooling System + schemas/ASHP+TES+GasBoiler.jpg + + cooling + + + 23 + + @@ -1528,6 +1551,14 @@ 10 + + Central Heating+Unitary Cooling+Unitary DHW + + 10 + 11 + 12 + + diff --git a/scripts/random_assignation.py b/scripts/random_assignation.py index f82ab89f..44be581c 100644 --- a/scripts/random_assignation.py +++ b/scripts/random_assignation.py @@ -30,6 +30,7 @@ residential_systems_percentage = {'system 1 gas': 100, residential_new_systems_percentage = {'PV+ASHP+GasBoiler+TES': 0, 'PV+4Pipe+DHW': 100, + 'Central Heating+Unitary Cooling+Unitary DHW': 0, 'PV+ASHP+ElectricBoiler+TES': 0, 'PV+GSHP+GasBoiler+TES': 0, 'PV+GSHP+ElectricBoiler+TES': 0, diff --git a/tests/test_systems_catalog.py b/tests/test_systems_catalog.py index 839107c2..234f54d6 100644 --- a/tests/test_systems_catalog.py +++ b/tests/test_systems_catalog.py @@ -38,10 +38,10 @@ class TestSystemsCatalog(TestCase): catalog = EnergySystemsCatalogFactory('montreal_future').catalog catalog_categories = catalog.names() - archetypes = catalog.names('archetypes') - self.assertEqual(13, len(archetypes['archetypes'])) + archetypes = catalog.names() + self.assertEqual(14, len(archetypes['archetypes'])) systems = catalog.names('systems') - self.assertEqual(10, len(systems['systems'])) + self.assertEqual(12, len(systems['systems'])) generation_equipments = catalog.names('generation_equipments') self.assertEqual(27, len(generation_equipments['generation_equipments'])) with self.assertRaises(ValueError): From 5f95d2a5fb10fbf9764ceb0b11fd733f0df4fc57 Mon Sep 17 00:00:00 2001 From: s_ranjbar Date: Tue, 25 Jun 2024 18:14:12 -0400 Subject: [PATCH 08/15] feat: simulation models of 2 archetypes with central heating and decentral cooling and dhw are added --- hub/city_model_structure/building.py | 48 +-- .../montreal_future_systems.xml | 9 + ...gy_system_sizing_and_simulation_factory.py | 10 + scripts/pv_sizing_and_simulation.py | 15 +- scripts/random_assignation.py | 5 +- .../system_simulation_models/archetype13.py | 12 +- .../archetypes14_15.py | 402 ++++++++++++++++++ tests/test_systems_catalog.py | 2 +- 8 files changed, 445 insertions(+), 58 deletions(-) create mode 100644 scripts/system_simulation_models/archetypes14_15.py diff --git a/hub/city_model_structure/building.py b/hub/city_model_structure/building.py index ac284d29..132e7a5c 100644 --- a/hub/city_model_structure/building.py +++ b/hub/city_model_structure/building.py @@ -484,7 +484,7 @@ class Building(CityObject): monthly_values = PeakLoads().peak_loads_from_hourly(self.domestic_hot_water_heat_demand[cte.HOUR]) if monthly_values is None: return None - results[cte.MONTH] = [x for x in monthly_values] + results[cte.MONTH] = [x / cte.WATTS_HOUR_TO_JULES for x in monthly_values] results[cte.YEAR] = [max(monthly_values) / cte.WATTS_HOUR_TO_JULES] return results @@ -810,39 +810,16 @@ class Building(CityObject): Get total electricity produced onsite in J return: dict """ - orientation_losses_factor = {cte.MONTH: {'north': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - 'east': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - 'south': [2.137931, 1.645503, 1.320946, 1.107817, 0.993213, 0.945175, - 0.967949, 1.065534, 1.24183, 1.486486, 1.918033, 2.210526], - 'west': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]}, - cte.YEAR: {'north': [0], - 'east': [0], - 'south': [1.212544], - 'west': [0]} - } - - # Add other systems whenever new ones appear - if self.energy_systems is None: - return self._onsite_electrical_production - for energy_system in self.energy_systems: - for generation_system in energy_system.generation_systems: - if generation_system.system_type == cte.PHOTOVOLTAIC: - if generation_system.electricity_efficiency is not None: - _efficiency = float(generation_system.electricity_efficiency) - else: - _efficiency = 0 - self._onsite_electrical_production = {} - for _key in self.roofs[0].global_irradiance.keys(): - _results = [0 for _ in range(0, len(self.roofs[0].global_irradiance[_key]))] - for surface in self.roofs: - if _key in orientation_losses_factor: - _results = [x + y * _efficiency * surface.perimeter_area - * surface.solar_collectors_area_reduction_factor * z - for x, y, z in zip(_results, surface.global_irradiance[_key], - orientation_losses_factor[_key]['south'])] - self._onsite_electrical_production[_key] = _results return self._onsite_electrical_production + @onsite_electrical_production.setter + def onsite_electrical_production(self, value): + """ + set onsite electrical production from external pv simulations + :return: + """ + self._onsite_electrical_production = value + @property def lower_corner(self): """ @@ -913,10 +890,3 @@ class Building(CityObject): self._fuel_consumption_breakdown = fuel_breakdown return self._fuel_consumption_breakdown - @property - def pv_generation(self) -> dict: - return self._pv_generation - - @pv_generation.setter - def pv_generation(self, value): - self._pv_generation = value diff --git a/hub/data/energy_systems/montreal_future_systems.xml b/hub/data/energy_systems/montreal_future_systems.xml index f9d43be4..b51c9488 100644 --- a/hub/data/energy_systems/montreal_future_systems.xml +++ b/hub/data/energy_systems/montreal_future_systems.xml @@ -1559,6 +1559,15 @@ 12 + + Central Heating+Unitary Cooling+Unitary DHW+PV + + 7 + 10 + 11 + 12 + + diff --git a/scripts/energy_system_sizing_and_simulation_factory.py b/scripts/energy_system_sizing_and_simulation_factory.py index b0526e2c..9a0e14bb 100644 --- a/scripts/energy_system_sizing_and_simulation_factory.py +++ b/scripts/energy_system_sizing_and_simulation_factory.py @@ -8,6 +8,7 @@ Project Coder Saeed Ranjbar saeed.ranjbar@mail.concordia.ca from scripts.system_simulation_models.archetype13 import Archetype13 from scripts.system_simulation_models.archetype13_stratified_tes import Archetype13Stratified from scripts.system_simulation_models.archetype1 import Archetype1 +from scripts.system_simulation_models.archetypes14_15 import Archetype14_15 class EnergySystemsSimulationFactory: @@ -36,6 +37,15 @@ class EnergySystemsSimulationFactory: self._building.level_of_detail.energy_systems = 2 self._building.level_of_detail.energy_systems = 2 + def _archetype14_15(self): + """ + Enrich the city by using the sizing and simulation model developed for archetype14 and archetype15 of + montreal_future_systems + """ + Archetype14_15(self._building, self._output_path).enrich_buildings() + self._building.level_of_detail.energy_systems = 2 + self._building.level_of_detail.energy_systems = 2 + def enrich(self): """ Enrich the city given to the class using the class given handler diff --git a/scripts/pv_sizing_and_simulation.py b/scripts/pv_sizing_and_simulation.py index 877a1499..6ef1a51d 100644 --- a/scripts/pv_sizing_and_simulation.py +++ b/scripts/pv_sizing_and_simulation.py @@ -49,18 +49,11 @@ class PVSizingSimulation(RadiationTilted): available_roof = self.available_space() inter_row_spacing = self.inter_row_spacing() self.number_of_panels(available_roof, inter_row_spacing) + self.building.roofs[0].installed_solar_collector_area = pv_module_area * self.total_number_of_panels system_efficiency = 0.2 - pv_hourly_production = [x * system_efficiency * self.total_number_of_panels * pv_module_area for x in radiation] + pv_hourly_production = [x * system_efficiency * self.total_number_of_panels * pv_module_area * + cte.WATTS_HOUR_TO_JULES for x in radiation] self.building.onsite_electrical_production[cte.HOUR] = pv_hourly_production self.building.onsite_electrical_production[cte.MONTH] = ( MonthlyValues.get_total_month(self.building.onsite_electrical_production[cte.HOUR])) - self.building.onsite_electrical_production[cte.YEAR] = [sum(self.building.onsite_electrical_production[cte.MONTH])] - - - - - - - - - + self.building.onsite_electrical_production[cte.YEAR] = [sum(self.building.onsite_electrical_production[cte.MONTH])] \ No newline at end of file diff --git a/scripts/random_assignation.py b/scripts/random_assignation.py index 44be581c..ac6eb454 100644 --- a/scripts/random_assignation.py +++ b/scripts/random_assignation.py @@ -29,8 +29,9 @@ residential_systems_percentage = {'system 1 gas': 100, 'system 8 electricity': 0} residential_new_systems_percentage = {'PV+ASHP+GasBoiler+TES': 0, - 'PV+4Pipe+DHW': 100, - 'Central Heating+Unitary Cooling+Unitary DHW': 0, + 'PV+4Pipe+DHW': 0, + 'Central Heating+Unitary Cooling+Unitary DHW': 50, + 'Central Heating+Unitary Cooling+Unitary DHW+PV': 50, 'PV+ASHP+ElectricBoiler+TES': 0, 'PV+GSHP+GasBoiler+TES': 0, 'PV+GSHP+ElectricBoiler+TES': 0, diff --git a/scripts/system_simulation_models/archetype13.py b/scripts/system_simulation_models/archetype13.py index 786115db..5219af2f 100644 --- a/scripts/system_simulation_models/archetype13.py +++ b/scripts/system_simulation_models/archetype13.py @@ -19,7 +19,8 @@ class Archetype13: self._domestic_hot_water_peak_load = building.domestic_hot_water_peak_load[cte.YEAR][0] self._hourly_heating_demand = [demand / cte.HOUR_TO_SECONDS for demand in building.heating_demand[cte.HOUR]] self._hourly_cooling_demand = [demand / cte.HOUR_TO_SECONDS for demand in building.cooling_demand[cte.HOUR]] - self._hourly_dhw_demand = building.domestic_hot_water_heat_demand[cte.HOUR] + self._hourly_dhw_demand = [demand / cte.WATTS_HOUR_TO_JULES for demand in + building.domestic_hot_water_heat_demand[cte.HOUR]] self._output_path = output_path self._t_out = building.external_temperature[cte.HOUR] self.results = {} @@ -30,11 +31,12 @@ class Archetype13: heat_pump = self._hvac_system.generation_systems[1] boiler = self._hvac_system.generation_systems[0] thermal_storage = boiler.energy_storage_systems[0] - heat_pump.nominal_heat_output = round(0.5 * self._heating_peak_load / 3600) - heat_pump.nominal_cooling_output = round(self._cooling_peak_load / 3600) - boiler.nominal_heat_output = round(0.5 * self._heating_peak_load / 3600) + heat_pump.nominal_heat_output = round(0.5 * self._heating_peak_load) + heat_pump.nominal_cooling_output = round(self._cooling_peak_load) + boiler.nominal_heat_output = round(0.5 * self._heating_peak_load) thermal_storage.volume = round( - (self._heating_peak_load * storage_factor) / (cte.WATER_HEAT_CAPACITY * cte.WATER_DENSITY * 25)) + (self._heating_peak_load * storage_factor * cte.WATTS_HOUR_TO_JULES) / + (cte.WATER_HEAT_CAPACITY * cte.WATER_DENSITY * 25)) return heat_pump, boiler, thermal_storage def dhw_sizing(self): diff --git a/scripts/system_simulation_models/archetypes14_15.py b/scripts/system_simulation_models/archetypes14_15.py new file mode 100644 index 00000000..e3cf52d1 --- /dev/null +++ b/scripts/system_simulation_models/archetypes14_15.py @@ -0,0 +1,402 @@ +import math +import hub.helpers.constants as cte +import csv +from hub.helpers.monthly_values import MonthlyValues + + +class Archetype14_15: + def __init__(self, building, output_path): + self._building = building + self._name = building.name + if 'PV' in building.energy_systems_archetype_name: + i = 1 + self._pv_system = building.energy_systems[0] + else: + i = 0 + self._dhw_system = building.energy_systems[i] + self._heating_system = building.energy_systems[i + 1] + self._cooling_system = building.energy_systems[i + 2] + self._dhw_peak_flow_rate = (building.thermal_zones_from_internal_zones[0].total_floor_area * + building.thermal_zones_from_internal_zones[0].domestic_hot_water.peak_flow * + cte.WATER_DENSITY) + self._heating_peak_load = building.heating_peak_load[cte.YEAR][0] + self._cooling_peak_load = building.cooling_peak_load[cte.YEAR][0] + self._domestic_hot_water_peak_load = building.domestic_hot_water_peak_load[cte.YEAR][0] + self._hourly_heating_demand = [demand / cte.WATTS_HOUR_TO_JULES for demand in building.heating_demand[cte.HOUR]] + self._hourly_cooling_demand = [demand / cte.WATTS_HOUR_TO_JULES for demand in building.cooling_demand[cte.HOUR]] + self._hourly_dhw_demand = [demand / cte.WATTS_HOUR_TO_JULES for demand in + building.domestic_hot_water_heat_demand[cte.HOUR]] + self._output_path = output_path + self._t_out = building.external_temperature[cte.HOUR] + self.results = {} + self.dt = 900 + + def heating_system_sizing(self): + storage_factor = 3 + heat_pump = self._heating_system.generation_systems[1] + heat_pump.source_temperature = self._t_out + boiler = self._heating_system.generation_systems[0] + thermal_storage = boiler.energy_storage_systems[0] + heat_pump.nominal_heat_output = round(0.5 * self._heating_peak_load) + boiler.nominal_heat_output = round(0.5 * self._heating_peak_load) + thermal_storage.volume = round( + (self._heating_peak_load * storage_factor * cte.WATTS_HOUR_TO_JULES) / + (cte.WATER_HEAT_CAPACITY * cte.WATER_DENSITY * 25)) + return heat_pump, boiler, thermal_storage + + def cooling_system_sizing(self): + heat_pump = self._cooling_system.generation_systems[0] + heat_pump.nominal_cooling_output = heat_pump.nominal_cooling_output = round(self._cooling_peak_load) + heat_pump.source_temperature = self._t_out + return heat_pump + + + def dhw_system_sizing(self): + storage_factor = 3 + dhw_hp = self._dhw_system.generation_systems[0] + dhw_hp.nominal_heat_output = round(0.7 * self._domestic_hot_water_peak_load) + dhw_hp.source_temperature = self._t_out + dhw_tes = dhw_hp.energy_storage_systems[0] + dhw_tes.volume = round( + (self._domestic_hot_water_peak_load * storage_factor * cte.WATTS_HOUR_TO_JULES) / + (cte.WATER_HEAT_CAPACITY * cte.WATER_DENSITY * 10)) + return dhw_hp, dhw_tes + + def heating_system_simulation(self): + hp, boiler, tes = self.heating_system_sizing() + cop_curve_coefficients = [float(coefficient) for coefficient in hp.heat_efficiency_curve.coefficients] + number_of_ts = int(cte.HOUR_TO_SECONDS / self.dt) + demand = [0] + [x for x in self._hourly_heating_demand for _ in range(number_of_ts)] + t_out = [0] + [x for x in self._t_out for _ in range(number_of_ts)] + hp.source_temperature = self._t_out + variable_names = ["t_sup_hp", "t_tank", "t_ret", "m_ch", "m_dis", "q_hp", "q_boiler", "hp_cop", + "hp_electricity", "boiler_gas_consumption", "t_sup_boiler", "boiler_energy_consumption", + "heating_consumption"] + num_hours = len(demand) + variables = {name: [0] * num_hours for name in variable_names} + (t_sup_hp, t_tank, t_ret, m_ch, m_dis, q_hp, q_boiler, hp_cop, + hp_electricity, boiler_gas_consumption, t_sup_boiler, boiler_energy_consumption, heating_consumption) = \ + [variables[name] for name in variable_names] + t_tank[0] = 55 + hp_heating_cap = hp.nominal_heat_output + boiler_heating_cap = boiler.nominal_heat_output + hp_delta_t = 5 + boiler_efficiency = float(boiler.heat_efficiency) + v, h = float(tes.volume), float(tes.height) + r_tot = sum(float(layer.thickness) / float(layer.material.conductivity) for layer in + tes.layers) + u_tot = 1 / r_tot + d = math.sqrt((4 * v) / (math.pi * h)) + a_side = math.pi * d * h + a_top = math.pi * d ** 2 / 4 + ua = u_tot * (2 * a_top + a_side) + # storage temperature prediction + for i in range(len(demand) - 1): + t_tank[i + 1] = (t_tank[i] + + (m_ch[i] * (t_sup_boiler[i] - t_tank[i]) + + (ua * (t_out[i] - t_tank[i])) / cte.WATER_HEAT_CAPACITY - + m_dis[i] * (t_tank[i] - t_ret[i])) * (self.dt / (cte.WATER_DENSITY * v))) + # hp operation + if t_tank[i + 1] < 40: + q_hp[i + 1] = hp_heating_cap + m_ch[i + 1] = q_hp[i + 1] / (cte.WATER_HEAT_CAPACITY * hp_delta_t) + t_sup_hp[i + 1] = (q_hp[i + 1] / (m_ch[i + 1] * cte.WATER_HEAT_CAPACITY)) + t_tank[i + 1] + elif 40 <= t_tank[i + 1] < 55 and q_hp[i] == 0: + q_hp[i + 1] = 0 + m_ch[i + 1] = 0 + t_sup_hp[i + 1] = t_tank[i + 1] + elif 40 <= t_tank[i + 1] < 55 and q_hp[i] > 0: + q_hp[i + 1] = hp_heating_cap + m_ch[i + 1] = q_hp[i + 1] / (cte.WATER_HEAT_CAPACITY * hp_delta_t) + t_sup_hp[i + 1] = (q_hp[i + 1] / (m_ch[i + 1] * cte.WATER_HEAT_CAPACITY)) + t_tank[i + 1] + else: + q_hp[i + 1], m_ch[i + 1], t_sup_hp[i + 1] = 0, 0, t_tank[i + 1] + t_sup_hp_fahrenheit = 1.8 * t_sup_hp[i + 1] + 32 + t_out_fahrenheit = 1.8 * t_out[i + 1] + 32 + if q_hp[i + 1] > 0: + hp_cop[i + 1] = (cop_curve_coefficients[0] + + cop_curve_coefficients[1] * t_sup_hp_fahrenheit + + cop_curve_coefficients[2] * t_sup_hp_fahrenheit ** 2 + + cop_curve_coefficients[3] * t_out_fahrenheit + + cop_curve_coefficients[4] * t_out_fahrenheit ** 2 + + cop_curve_coefficients[5] * t_sup_hp_fahrenheit * t_out_fahrenheit) + hp_electricity[i + 1] = q_hp[i + 1] / hp_cop[i + 1] + else: + hp_cop[i + 1] = 0 + hp_electricity[i + 1] = 0 + # boiler operation + if q_hp[i + 1] > 0: + if t_sup_hp[i + 1] < 45: + q_boiler[i + 1] = boiler_heating_cap + elif demand[i + 1] > 0.5 * self._heating_peak_load / self.dt: + q_boiler[i + 1] = 0.5 * boiler_heating_cap + boiler_energy_consumption[i + 1] = q_boiler[i + 1] / boiler_efficiency + boiler_gas_consumption[i + 1] = (q_boiler[i + 1] * self.dt) / (boiler_efficiency * cte.NATURAL_GAS_LHV) + t_sup_boiler[i + 1] = t_sup_hp[i + 1] + (q_boiler[i + 1] / (m_ch[i + 1] * cte.WATER_HEAT_CAPACITY)) + # storage discharging + if demand[i + 1] == 0: + m_dis[i + 1] = 0 + t_ret[i + 1] = t_tank[i + 1] + else: + if demand[i + 1] > 0.5 * self._heating_peak_load / cte.HOUR_TO_SECONDS: + factor = 8 + else: + factor = 4 + m_dis[i + 1] = self._heating_peak_load / (cte.WATER_HEAT_CAPACITY * factor * cte.HOUR_TO_SECONDS) + t_ret[i + 1] = t_tank[i + 1] - demand[i + 1] / (m_dis[i + 1] * cte.WATER_HEAT_CAPACITY) + tes.temperature = [] + hp_electricity_j = [(x * cte.WATTS_HOUR_TO_JULES) / number_of_ts for x in hp_electricity] + boiler_consumption_j = [(x * cte.WATTS_HOUR_TO_JULES) / number_of_ts for x in boiler_energy_consumption] + hp_hourly = [] + boiler_hourly = [] + boiler_sum = 0 + hp_sum = 0 + for i in range(1, len(demand)): + hp_sum += hp_electricity_j[i] + boiler_sum += boiler_consumption_j[i] + if (i - 1) % number_of_ts == 0: + tes.temperature.append(t_tank[i]) + hp_hourly.append(hp_sum) + boiler_hourly.append(boiler_sum) + hp_sum = 0 + boiler_sum = 0 + hp.energy_consumption[cte.HEATING] = {} + hp.energy_consumption[cte.HEATING][cte.HOUR] = hp_hourly + hp.energy_consumption[cte.HEATING][cte.MONTH] = MonthlyValues.get_total_month( + hp.energy_consumption[cte.HEATING][cte.HOUR]) + hp.energy_consumption[cte.HEATING][cte.YEAR] = [ + sum(hp.energy_consumption[cte.HEATING][cte.MONTH])] + boiler.energy_consumption[cte.HEATING] = {} + boiler.energy_consumption[cte.HEATING][cte.HOUR] = boiler_hourly + boiler.energy_consumption[cte.HEATING][cte.MONTH] = MonthlyValues.get_total_month( + boiler.energy_consumption[cte.HEATING][cte.HOUR]) + boiler.energy_consumption[cte.HEATING][cte.YEAR] = [ + sum(boiler.energy_consumption[cte.HEATING][cte.MONTH])] + + self.results['Heating Demand (W)'] = demand + self.results['HP Heat Output (W)'] = q_hp + self.results['HP Source Temperature'] = t_out + self.results['HP Supply Temperature'] = t_sup_hp + self.results['HP COP'] = hp_cop + self.results['HP Electricity Consumption (W)'] = hp_electricity + self.results['Boiler Heat Output (W)'] = q_boiler + self.results['Boiler Supply Temperature'] = t_sup_boiler + self.results['Boiler Gas Consumption'] = boiler_gas_consumption + self.results['TES Temperature'] = t_tank + self.results['TES Charging Flow Rate (kg/s)'] = m_ch + self.results['TES Discharge Flow Rate (kg/s)'] = m_dis + self.results['Heating Loop Return Temperature'] = t_ret + return hp_hourly, boiler_hourly + + def cooling_system_simulation(self): + hp = self.cooling_system_sizing() + eer_curve_coefficients = [float(coefficient) for coefficient in hp.cooling_efficiency_curve.coefficients] + cooling_efficiency = float(hp.cooling_efficiency) + number_of_ts = int(cte.HOUR_TO_SECONDS / self.dt) + demand = [0] + [x for x in self._hourly_cooling_demand for _ in range(number_of_ts)] + t_out = [0] + [x for x in self._t_out for _ in range(number_of_ts)] + hp.source_temperature = self._t_out + variable_names = ["t_sup_hp", "t_ret", "m", "q_hp", "hp_electricity", "hp_eer"] + num_hours = len(demand) + variables = {name: [0] * num_hours for name in variable_names} + (t_sup_hp, t_ret, m, q_hp, hp_electricity, hp_eer) = [variables[name] for name in variable_names] + t_ret[0] = 13 + + for i in range(1, len(demand)): + if demand[i] > 0: + m[i] = self._cooling_peak_load / (cte.WATER_HEAT_CAPACITY * 5 * cte.HOUR_TO_SECONDS) + if t_ret[i - 1] >= 13: + if demand[i] < 0.25 * self._cooling_peak_load / cte.HOUR_TO_SECONDS: + q_hp[i] = 0.25 * hp.nominal_cooling_output + elif demand[i] < 0.5 * self._cooling_peak_load / cte.HOUR_TO_SECONDS: + q_hp[i] = 0.5 * hp.nominal_cooling_output + else: + q_hp[i] = hp.nominal_cooling_output + t_sup_hp[i] = t_ret[i - 1] - q_hp[i] / (m[i] * cte.WATER_HEAT_CAPACITY) + else: + q_hp[i] = 0 + t_sup_hp[i] = t_ret[i - 1] + if m[i] == 0: + t_ret[i] = t_sup_hp[i] + else: + t_ret[i] = t_sup_hp[i] + demand[i] / (m[i] * cte.WATER_HEAT_CAPACITY) + else: + m[i] = 0 + q_hp[i] = 0 + t_sup_hp[i] = t_ret[i -1] + t_ret[i] = t_ret[i - 1] + t_sup_hp_fahrenheit = 1.8 * t_sup_hp[i] + 32 + t_out_fahrenheit = 1.8 * t_out[i] + 32 + if q_hp[i] > 0: + hp_eer[i] = (eer_curve_coefficients[0] + + eer_curve_coefficients[1] * t_sup_hp_fahrenheit + + eer_curve_coefficients[2] * t_sup_hp_fahrenheit ** 2 + + eer_curve_coefficients[3] * t_out_fahrenheit + + eer_curve_coefficients[4] * t_out_fahrenheit ** 2 + + eer_curve_coefficients[5] * t_sup_hp_fahrenheit * t_out_fahrenheit) + hp_electricity[i] = q_hp[i] / hp_eer[i] + else: + hp_eer[i] = 0 + hp_electricity[i] = 0 + hp_electricity_j = [(x * cte.WATTS_HOUR_TO_JULES) / number_of_ts for x in hp_electricity] + hp_hourly = [] + hp_sum = 0 + for i in range(1, len(demand)): + hp_sum += hp_electricity_j[i] + if (i - 1) % number_of_ts == 0: + hp_hourly.append(hp_sum) + hp_sum = 0 + hp.energy_consumption[cte.COOLING] = {} + hp.energy_consumption[cte.COOLING][cte.HOUR] = hp_hourly + hp.energy_consumption[cte.COOLING][cte.MONTH] = MonthlyValues.get_total_month( + hp.energy_consumption[cte.COOLING][cte.HOUR]) + hp.energy_consumption[cte.COOLING][cte.YEAR] = [ + sum(hp.energy_consumption[cte.COOLING][cte.MONTH])] + self.results['Cooling Demand (W)'] = demand + self.results['HP Cooling Output (W)'] = q_hp + self.results['HP Cooling Supply Temperature'] = t_sup_hp + self.results['HP Cooling COP'] = hp_eer + self.results['HP Electricity Consumption'] = hp_electricity + self.results['Cooling Loop Flow Rate (kg/s)'] = m + self.results['Cooling Loop Return Temperature'] = t_ret + return hp_hourly + + def dhw_system_simulation(self): + hp, tes = self.dhw_system_sizing() + cop_curve_coefficients = [float(coefficient) for coefficient in hp.heat_efficiency_curve.coefficients] + number_of_ts = int(cte.HOUR_TO_SECONDS / self.dt) + demand = [0] + [x for x in self._hourly_dhw_demand for _ in range(number_of_ts)] + t_out = [0] + [x for x in self._t_out for _ in range(number_of_ts)] + variable_names = ["t_sup_hp", "t_tank", "m_ch", "m_dis", "q_hp", "q_coil", "hp_cop", + "hp_electricity", "available hot water (m3)", "refill flow rate (kg/s)"] + num_hours = len(demand) + variables = {name: [0] * num_hours for name in variable_names} + (t_sup_hp, t_tank, m_ch, m_dis, m_refill, q_hp, q_coil, hp_cop, hp_electricity, v_dhw) = \ + [variables[name] for name in variable_names] + t_tank[0] = 70 + v_dhw[0] = tes.volume + + hp_heating_cap = hp.nominal_heat_output + hp_delta_t = 8 + v, h = float(tes.volume), float(tes.height) + r_tot = sum(float(layer.thickness) / float(layer.material.conductivity) for layer in + tes.layers) + u_tot = 1 / r_tot + d = math.sqrt((4 * v) / (math.pi * h)) + a_side = math.pi * d * h + a_top = math.pi * d ** 2 / 4 + ua = u_tot * (2 * a_top + a_side) + freshwater_temperature = 18 + for i in range(len(demand) - 1): + delta_t_demand = demand[i] * (self.dt / (cte.WATER_DENSITY * cte.WATER_HEAT_CAPACITY * v)) + if t_tank[i] < 65: + q_hp[i] = hp_heating_cap + delta_t_hp = q_hp[i] * (self.dt / (cte.WATER_DENSITY * cte.WATER_HEAT_CAPACITY * v)) + if demand[i] > 0: + dhw_needed = (demand[i] * cte.HOUR_TO_SECONDS) / (cte.WATER_HEAT_CAPACITY * t_tank[i] * cte.WATER_DENSITY) + m_dis[i] = dhw_needed * cte.WATER_DENSITY / cte.HOUR_TO_SECONDS + m_refill[i] = m_dis[i] + delta_t_freshwater = m_refill[i] * (t_tank[i] - freshwater_temperature) * (self.dt / (v * cte.WATER_DENSITY)) + diff = delta_t_freshwater + delta_t_demand - delta_t_hp + if diff > 0: + if diff > 0: + power = diff * (cte.WATER_DENSITY * cte.WATER_HEAT_CAPACITY * v) / self.dt + if power <= float(tes.heating_coil_capacity): + q_coil[i] = power + else: + q_coil[i] = float(tes.heating_coil_capacity) + delta_t_coil = q_coil[i] * (self.dt / (cte.WATER_DENSITY * cte.WATER_HEAT_CAPACITY * v)) + + if q_hp[i] > 0: + m_ch[i] = q_hp[i] / (cte.WATER_HEAT_CAPACITY * hp_delta_t) + t_sup_hp[i] = (q_hp[i] / (m_ch[i] * cte.WATER_HEAT_CAPACITY)) + t_tank[i] + else: + m_ch[i] = 0 + t_sup_hp[i] = t_tank[i] + t_sup_hp_fahrenheit = 1.8 * t_sup_hp[i] + 32 + t_out_fahrenheit = 1.8 * t_out[i] + 32 + if q_hp[i] > 0: + hp_cop[i] = (cop_curve_coefficients[0] + + cop_curve_coefficients[1] * t_sup_hp_fahrenheit + + cop_curve_coefficients[2] * t_sup_hp_fahrenheit ** 2 + + cop_curve_coefficients[3] * t_out_fahrenheit + + cop_curve_coefficients[4] * t_out_fahrenheit ** 2 + + cop_curve_coefficients[5] * t_sup_hp_fahrenheit * t_out_fahrenheit) + hp_electricity[i] = q_hp[i] / hp_cop[i] + else: + hp_cop[i] = 0 + hp_electricity[i] = 0 + + t_tank[i + 1] = t_tank[i] + (delta_t_hp - delta_t_freshwater - delta_t_demand + delta_t_coil) + tes.temperature = [] + hp_electricity_j = [(x * cte.WATTS_HOUR_TO_JULES) / number_of_ts for x in hp_electricity] + heating_coil_j = [(x * cte.WATTS_HOUR_TO_JULES) / number_of_ts for x in q_coil] + hp_hourly = [] + coil_hourly = [] + coil_sum = 0 + hp_sum = 0 + for i in range(1, len(demand)): + hp_sum += hp_electricity_j[i] + coil_sum += heating_coil_j[i] + if (i - 1) % number_of_ts == 0: + tes.temperature.append(t_tank[i]) + hp_hourly.append(hp_sum) + coil_hourly.append(coil_sum) + hp_sum = 0 + coil_sum = 0 + + hp.energy_consumption[cte.DOMESTIC_HOT_WATER] = {} + hp.energy_consumption[cte.DOMESTIC_HOT_WATER][cte.HOUR] = hp_hourly + hp.energy_consumption[cte.DOMESTIC_HOT_WATER][cte.MONTH] = MonthlyValues.get_total_month( + hp.energy_consumption[cte.DOMESTIC_HOT_WATER][cte.HOUR]) + hp.energy_consumption[cte.DOMESTIC_HOT_WATER][cte.YEAR] = [ + sum(hp.energy_consumption[cte.DOMESTIC_HOT_WATER][cte.MONTH])] + tes.heating_coil_energy_consumption = {} + tes.heating_coil_energy_consumption[cte.HOUR] = coil_hourly + tes.heating_coil_energy_consumption[cte.MONTH] = MonthlyValues.get_total_month( + tes.heating_coil_energy_consumption[cte.HOUR]) + tes.heating_coil_energy_consumption[cte.YEAR] = [ + sum(tes.heating_coil_energy_consumption[cte.MONTH])] + tes.temperature = t_tank + + self.results['DHW Demand (W)'] = demand + self.results['DHW HP Heat Output (W)'] = q_hp + self.results['DHW HP Electricity Consumption (W)'] = hp_electricity + self.results['DHW HP Source Temperature'] = t_out + self.results['DHW HP Supply Temperature'] = t_sup_hp + self.results['DHW HP COP'] = hp_cop + self.results['DHW TES Heating Coil Heat Output (W)'] = q_coil + self.results['DHW TES Temperature'] = t_tank + self.results['DHW TES Charging Flow Rate (kg/s)'] = m_ch + self.results['DHW Flow Rate (kg/s)'] = m_dis + self.results['DHW TES Refill Flow Rate (kg/s)'] = m_refill + self.results['Available Water in Tank (m3)'] = v_dhw + return hp_hourly, coil_hourly + + + def enrich_buildings(self): + hp_heating, boiler_consumption = self.heating_system_simulation() + hp_cooling = self.cooling_system_simulation() + hp_dhw, heating_coil = self.dhw_system_simulation() + heating_consumption = [hp_heating[i] + boiler_consumption[i] for i in range(len(hp_heating))] + dhw_consumption = [hp_dhw[i] + heating_coil[i] for i in range(len(hp_dhw))] + self._building.heating_consumption[cte.HOUR] = heating_consumption + self._building.heating_consumption[cte.MONTH] = ( + MonthlyValues.get_total_month(self._building.heating_consumption[cte.HOUR])) + self._building.heating_consumption[cte.YEAR] = [sum(self._building.heating_consumption[cte.MONTH])] + self._building.cooling_consumption[cte.HOUR] = hp_cooling + self._building.cooling_consumption[cte.MONTH] = ( + MonthlyValues.get_total_month(self._building.cooling_consumption[cte.HOUR])) + self._building.cooling_consumption[cte.YEAR] = [sum(self._building.cooling_consumption[cte.MONTH])] + self._building.domestic_hot_water_consumption[cte.HOUR] = dhw_consumption + self._building.domestic_hot_water_consumption[cte.MONTH] = ( + MonthlyValues.get_total_month(self._building.domestic_hot_water_consumption[cte.HOUR])) + self._building.domestic_hot_water_consumption[cte.YEAR] = ( + sum(self._building.domestic_hot_water_consumption[cte.MONTH])) + file_name = f'energy_system_simulation_results_{self._name}.csv' + with open(self._output_path / file_name, 'w', newline='') as csvfile: + output_file = csv.writer(csvfile) + # Write header + output_file.writerow(self.results.keys()) + # Write data + output_file.writerows(zip(*self.results.values())) diff --git a/tests/test_systems_catalog.py b/tests/test_systems_catalog.py index 234f54d6..612a8fe6 100644 --- a/tests/test_systems_catalog.py +++ b/tests/test_systems_catalog.py @@ -39,7 +39,7 @@ class TestSystemsCatalog(TestCase): catalog_categories = catalog.names() archetypes = catalog.names() - self.assertEqual(14, len(archetypes['archetypes'])) + self.assertEqual(15, len(archetypes['archetypes'])) systems = catalog.names('systems') self.assertEqual(12, len(systems['systems'])) generation_equipments = catalog.names('generation_equipments') From 2ef3be7fe354c41fab71f2268e2519fc7d07f4e7 Mon Sep 17 00:00:00 2001 From: s_ranjbar Date: Tue, 25 Jun 2024 19:57:22 -0400 Subject: [PATCH 09/15] fix: cost workflow finalized --- main.py | 68 +++++++++++++ scripts/costs/capital_costs.py | 122 +++++++++++++++++------ scripts/costs/constants.py | 4 +- scripts/costs/cost.py | 10 +- scripts/costs/total_maintenance_costs.py | 17 ++-- scripts/random_assignation.py | 6 +- 6 files changed, 178 insertions(+), 49 deletions(-) diff --git a/main.py b/main.py index e69de29b..a0383a6d 100644 --- a/main.py +++ b/main.py @@ -0,0 +1,68 @@ +from scripts.geojson_creator import process_geojson +from pathlib import Path +import subprocess +from scripts.ep_run_enrich import energy_plus_workflow +from hub.imports.geometry_factory import GeometryFactory +from hub.helpers.dictionaries import Dictionaries +from hub.imports.construction_factory import ConstructionFactory +from hub.imports.usage_factory import UsageFactory +from hub.imports.weather_factory import WeatherFactory +from hub.imports.results_factory import ResultFactory +from scripts import random_assignation +from hub.imports.energy_systems_factory import EnergySystemsFactory +from scripts.energy_system_sizing_and_simulation_factory import EnergySystemsSimulationFactory +from scripts.costs.cost import Cost +from scripts.costs.constants import SKIN_RETROFIT_AND_SYSTEM_RETROFIT_AND_PV, SYSTEM_RETROFIT_AND_PV, SYSTEM_RETROFIT +import hub.helpers.constants as cte +from scripts.solar_angles import CitySolarAngles +from scripts.pv_sizing_and_simulation import PVSizingSimulation +from hub.exports.exports_factory import ExportsFactory +# Specify the GeoJSON file path +geojson_file = process_geojson(x=-73.5681295982132, y=45.49218262677643, diff=0.0001) +file_path = (Path(__file__).parent / 'input_files' / 'output_buildings.geojson') +# Specify the output path for the PDF file +output_path = (Path(__file__).parent / 'out_files').resolve() +# Create city object from GeoJSON file +city = GeometryFactory('geojson', + path=file_path, + height_field='height', + year_of_construction_field='year_of_construction', + function_field='function', + function_to_hub=Dictionaries().montreal_function_to_hub_function).city +# Enrich city data +ConstructionFactory('nrcan', city).enrich() + +UsageFactory('nrcan', city).enrich() +WeatherFactory('epw', city).enrich() +ExportsFactory('sra', city, output_path).export() +sra_path = (output_path / f'{city.name}_sra.xml').resolve() +subprocess.run(['sra', str(sra_path)]) +ResultFactory('sra', city, output_path).enrich() +solar_angles = CitySolarAngles(city.name, + city.latitude, + city.longitude, + tilt_angle=45, + surface_azimuth_angle=180).calculate +energy_plus_workflow(city) +random_assignation.call_random(city.buildings, random_assignation.residential_new_systems_percentage) +EnergySystemsFactory('montreal_future', city).enrich() +for building in city.buildings: + EnergySystemsSimulationFactory('archetype13', building=building, output_path=output_path).enrich() + if 'PV' in building.energy_systems_archetype_name: + ghi = [x / cte.WATTS_HOUR_TO_JULES for x in building.roofs[0].global_irradiance[cte.HOUR]] + pv_sizing_simulation = PVSizingSimulation(building, + solar_angles, + tilt_angle=45, + module_height=1, + module_width=2, + ghi=ghi) + pv_sizing_simulation.pv_output() +for building in city.buildings: + costs = Cost(building=building, retrofit_scenario=SYSTEM_RETROFIT).life_cycle + costs.to_csv(output_path / f'{building.name}_lcc.csv') + (costs.loc['global_operational_costs', f'Scenario {SYSTEM_RETROFIT}']. + to_csv(output_path / f'{building.name}_op.csv')) + costs.loc['global_capital_costs', f'Scenario {SYSTEM_RETROFIT}'].to_csv( + output_path / f'{building.name}_cc.csv') + costs.loc['global_maintenance_costs', f'Scenario {SYSTEM_RETROFIT}'].to_csv( + output_path / f'{building.name}_m.csv') \ No newline at end of file diff --git a/scripts/costs/capital_costs.py b/scripts/costs/capital_costs.py index c87f3eba..832aeb7d 100644 --- a/scripts/costs/capital_costs.py +++ b/scripts/costs/capital_costs.py @@ -13,7 +13,7 @@ from hub.city_model_structure.building import Building import hub.helpers.constants as cte from scripts.costs.configuration import Configuration from scripts.costs.constants import (SKIN_RETROFIT, SKIN_RETROFIT_AND_SYSTEM_RETROFIT_AND_PV, - SYSTEM_RETROFIT_AND_PV, CURRENT_STATUS, PV) + SYSTEM_RETROFIT_AND_PV, CURRENT_STATUS, PV, SYSTEM_RETROFIT) from scripts.costs.cost_base import CostBase @@ -32,10 +32,12 @@ class CapitalCosts(CostBase): 'B3010_opaque_roof', 'B1010_superstructure', 'D2010_photovoltaic_system', - 'D3020_heat_and_cooling_generating_systems', - 'D3040_distribution_systems', - 'D3050_other_hvac_ahu', - 'D3060_storage_systems', + 'D3020_simultaneous_heat_and_cooling_generating_systems', + 'D3030_heating_systems', + 'D3040_cooling_systems', + 'D3050_distribution_systems', + 'D3060_other_hvac_ahu', + 'D3070_storage_systems', 'D40_dhw', ], dtype='float' @@ -45,10 +47,12 @@ class CapitalCosts(CostBase): self._yearly_capital_costs.loc[0, 'B3010_opaque_roof'] = 0 self._yearly_capital_costs.loc[0, 'B1010_superstructure'] = 0 self._yearly_capital_costs.loc[0, 'D2010_photovoltaic_system'] = 0 - self._yearly_capital_costs.loc[0, 'D3020_heat_and_cooling_generating_systems'] = 0 - self._yearly_capital_costs.loc[0, 'D3040_distribution_systems'] = 0 - self._yearly_capital_costs.loc[0, 'D3080_other_hvac_ahu'] = 0 - self._yearly_capital_costs.loc[0, 'D3060_storage_systems'] = 0 + self._yearly_capital_costs.loc[0, 'D3020_simultaneous_heat_and_cooling_generating_systems'] = 0 + self._yearly_capital_costs.loc[0, 'D3030_heating_systems'] = 0 + self._yearly_capital_costs.loc[0, 'D3040_cooling_systems'] = 0 + self._yearly_capital_costs.loc[0, 'D3050_distribution_systems'] = 0 + self._yearly_capital_costs.loc[0, 'D3060_other_hvac_ahu'] = 0 + self._yearly_capital_costs.loc[0, 'D3070_storage_systems'] = 0 self._yearly_capital_costs.loc[0, 'D40_dhw'] = 0 self._yearly_capital_incomes = pd.DataFrame( @@ -107,7 +111,7 @@ class CapitalCosts(CostBase): capital_cost_transparent = surface_transparent * chapter.item('B2020_transparent').refurbishment[0] capital_cost_roof = surface_roof * chapter.item('B3010_opaque_roof').refurbishment[0] capital_cost_ground = surface_ground * chapter.item('B1010_superstructure').refurbishment[0] - if self._configuration.retrofit_scenario not in (SYSTEM_RETROFIT_AND_PV, CURRENT_STATUS, PV): + if self._configuration.retrofit_scenario not in (SYSTEM_RETROFIT_AND_PV, CURRENT_STATUS, PV, SYSTEM_RETROFIT): self._yearly_capital_costs.loc[0, 'B2010_opaque_walls'] = capital_cost_opaque * self._own_capital self._yearly_capital_costs.loc[0, 'B2020_transparent'] = capital_cost_transparent * self._own_capital self._yearly_capital_costs.loc[0, 'B3010_opaque_roof'] = capital_cost_roof * self._own_capital @@ -151,14 +155,20 @@ class CapitalCosts(CostBase): chapter = self._capital_costs_chapter.chapter('D_services') system_components, component_categories, component_sizes = self.system_components() capital_cost_heating_and_cooling_equipment = 0 + capital_cost_heating_equipment = 0 + capital_cost_cooling_equipment = 0 capital_cost_domestic_hot_water_equipment = 0 capital_cost_energy_storage_equipment = 0 capital_cost_distribution_equipment = 0 capital_cost_lighting = 0 capital_cost_pv = self._surface_pv * chapter.item('D2010_photovoltaic_system').initial_investment[0] for (i, component) in enumerate(system_components): - if component_categories[i] == 'generation': + if component_categories[i] == 'multi_generation': capital_cost_heating_and_cooling_equipment += chapter.item(component).initial_investment[0] * component_sizes[i] + elif component_categories[i] == 'heating': + capital_cost_heating_equipment += chapter.item(component).initial_investment[0] * component_sizes[i] + elif component_categories[i] == 'cooling': + capital_cost_cooling_equipment += chapter.item(component).initial_investment[0] * component_sizes[i] elif component_categories[i] == 'dhw': capital_cost_domestic_hot_water_equipment += chapter.item(component).initial_investment[0] * \ component_sizes[i] @@ -170,19 +180,25 @@ class CapitalCosts(CostBase): if self._configuration.retrofit_scenario in (SKIN_RETROFIT_AND_SYSTEM_RETROFIT_AND_PV, SYSTEM_RETROFIT_AND_PV, PV): self._yearly_capital_costs.loc[0, 'D2010_photovoltaic_system'] = capital_cost_pv - if self._configuration.retrofit_scenario in (SYSTEM_RETROFIT_AND_PV, SKIN_RETROFIT_AND_SYSTEM_RETROFIT_AND_PV): - self._yearly_capital_costs.loc[0, 'D3020_heat_and_cooling_generating_systems'] = ( + if (self._configuration.retrofit_scenario in + (SYSTEM_RETROFIT_AND_PV, SKIN_RETROFIT_AND_SYSTEM_RETROFIT_AND_PV, SYSTEM_RETROFIT)): + self._yearly_capital_costs.loc[0, 'D3020_simultaneous_heat_and_cooling_generating_systems'] = ( capital_cost_heating_and_cooling_equipment * self._own_capital) - self._yearly_capital_costs.loc[0, 'D3040_distribution_systems'] = ( + self._yearly_capital_costs.loc[0, 'D3030_heating_systems'] = ( + capital_cost_heating_equipment * self._own_capital) + self._yearly_capital_costs.loc[0, 'D3040_cooling_systems'] = ( + capital_cost_cooling_equipment * self._own_capital) + self._yearly_capital_costs.loc[0, 'D3050_distribution_systems'] = ( capital_cost_distribution_equipment * self._own_capital) - self._yearly_capital_costs.loc[0, 'D3060_storage_systems'] = ( + self._yearly_capital_costs.loc[0, 'D3070_storage_systems'] = ( capital_cost_energy_storage_equipment * self._own_capital) self._yearly_capital_costs.loc[0, 'D40_dhw'] = ( capital_cost_domestic_hot_water_equipment * self._own_capital) capital_cost_hvac = (capital_cost_heating_and_cooling_equipment + capital_cost_distribution_equipment + capital_cost_energy_storage_equipment + capital_cost_domestic_hot_water_equipment) - return (capital_cost_pv, capital_cost_heating_and_cooling_equipment, capital_cost_distribution_equipment, - capital_cost_energy_storage_equipment, capital_cost_domestic_hot_water_equipment, capital_cost_lighting, capital_cost_hvac) + return (capital_cost_pv, capital_cost_heating_and_cooling_equipment, capital_cost_heating_equipment, + capital_cost_distribution_equipment, capital_cost_cooling_equipment, capital_cost_energy_storage_equipment, + capital_cost_domestic_hot_water_equipment, capital_cost_lighting, capital_cost_hvac) def yearly_energy_system_costs(self): chapter = self._capital_costs_chapter.chapter('D_services') @@ -202,49 +218,79 @@ class CapitalCosts(CostBase): system_investment_costs[0] * self._configuration.percentage_credit ) ) - self._yearly_capital_costs.loc[year, 'D3020_heat_and_cooling_generating_systems'] = ( + self._yearly_capital_costs.loc[year, 'D3020_simultaneous_heat_and_cooling_generating_systems'] = ( -npf.pmt( self._configuration.interest_rate, self._configuration.credit_years, system_investment_costs[1] * self._configuration.percentage_credit ) ) - self._yearly_capital_costs.loc[year, 'D3040_distribution_systems'] = ( + self._yearly_capital_costs.loc[year, 'D3030_heating_systems'] = ( -npf.pmt( self._configuration.interest_rate, self._configuration.credit_years, system_investment_costs[2] * self._configuration.percentage_credit ) ) - self._yearly_capital_costs.loc[year, 'D3060_storage_systems'] = ( + self._yearly_capital_costs.loc[year, 'D3040_cooling_systems'] = ( -npf.pmt( self._configuration.interest_rate, self._configuration.credit_years, system_investment_costs[3] * self._configuration.percentage_credit ) ) - self._yearly_capital_costs.loc[year, 'D40_dhw'] = ( + self._yearly_capital_costs.loc[year, 'D3050_distribution_systems'] = ( -npf.pmt( self._configuration.interest_rate, self._configuration.credit_years, system_investment_costs[4] * self._configuration.percentage_credit ) ) + self._yearly_capital_costs.loc[year, 'D3070_storage_systems'] = ( + -npf.pmt( + self._configuration.interest_rate, + self._configuration.credit_years, + system_investment_costs[5] * self._configuration.percentage_credit + ) + ) + self._yearly_capital_costs.loc[year, 'D40_dhw'] = ( + -npf.pmt( + self._configuration.interest_rate, + self._configuration.credit_years, + system_investment_costs[6] * self._configuration.percentage_credit + ) + ) if self._configuration.retrofit_scenario not in (SKIN_RETROFIT, PV): for (i, component) in enumerate(system_components): if (year % chapter.item(component).lifetime) == 0 and year != (self._configuration.number_of_years - 1): - if component_categories[i] == 'generation': - reposition_cost_heating_and_cooling_equipment = chapter.item(component).reposition[0] * component_sizes[i] * costs_increase - self._yearly_capital_costs.loc[year, 'D3020_heat_and_cooling_generating_systems'] += reposition_cost_heating_and_cooling_equipment + if component_categories[i] == 'multi_generation': + reposition_cost_heating_and_cooling_equipment = (chapter.item(component).reposition[0] * + component_sizes[i] * costs_increase) + self._yearly_capital_costs.loc[year, 'D3020_simultaneous_heat_and_cooling_generating_systems'] += ( + reposition_cost_heating_and_cooling_equipment) + elif component_categories[i] == 'heating': + reposition_cost_heating_equipment = (chapter.item(component).reposition[0] * + component_sizes[i] * costs_increase) + self._yearly_capital_costs.loc[year, 'D3030_heating_systems'] += ( + reposition_cost_heating_equipment) + elif component_categories[i] == 'cooling': + reposition_cost_cooling_equipment = (chapter.item(component).reposition[0] * + component_sizes[i] * costs_increase) + self._yearly_capital_costs.loc[year, 'D3040_cooling_systems'] += ( + reposition_cost_cooling_equipment) elif component_categories[i] == 'dhw': - reposition_cost_domestic_hot_water_equipment = chapter.item(component).reposition[0] * component_sizes[i] * costs_increase + reposition_cost_domestic_hot_water_equipment = ( + chapter.item(component).reposition[0] * component_sizes[i] * costs_increase) self._yearly_capital_costs.loc[year, 'D40_dhw'] += reposition_cost_domestic_hot_water_equipment elif component_categories[i] == 'distribution': - reposition_cost_distribution_equipment = chapter.item(component).reposition[0] * component_sizes[i] * costs_increase - self._yearly_capital_costs.loc[year, 'D3040_distribution_systems'] += reposition_cost_distribution_equipment + reposition_cost_distribution_equipment = ( + chapter.item(component).reposition[0] * component_sizes[i] * costs_increase) + self._yearly_capital_costs.loc[year, 'D3050_distribution_systems'] += ( + reposition_cost_distribution_equipment) else: - reposition_cost_energy_storage_equipment = chapter.item(component).initial_investment[0] * component_sizes[i] * costs_increase - self._yearly_capital_costs.loc[year, 'D3060_storage_systems'] += reposition_cost_energy_storage_equipment + reposition_cost_energy_storage_equipment = ( + chapter.item(component).initial_investment[0] * component_sizes[i] * costs_increase) + self._yearly_capital_costs.loc[year, 'D3070_storage_systems'] += reposition_cost_energy_storage_equipment if self._configuration.retrofit_scenario == CURRENT_STATUS and pv: if (year % chapter.item('D2010_photovoltaic_system').lifetime) == 0: self._yearly_capital_costs.loc[year, 'D2010_photovoltaic_system'] += ( @@ -280,8 +326,11 @@ class CapitalCosts(CostBase): system_components.append(self.boiler_type(generation_system)) else: system_components.append('D302010_template_heat') - elif cte.HEATING or cte.COOLING in demand_types: - component_categories.append('generation') + elif cte.HEATING in demand_types: + if cte.COOLING in demand_types and generation_system.fuel_type == cte.ELECTRICITY: + component_categories.append('multi_generation') + else: + component_categories.append('heating') sizes.append(installed_capacity) if generation_system.system_type == cte.HEAT_PUMP: item_type = self.heat_pump_type(generation_system) @@ -290,11 +339,18 @@ class CapitalCosts(CostBase): item_type = self.boiler_type(generation_system) system_components.append(item_type) else: - if cte.COOLING in demand_types and cte.HEATING not in demand_types: + if cooling_capacity > heating_capacity: system_components.append('D302090_template_cooling') else: system_components.append('D302010_template_heat') - + elif cte.COOLING in demand_types: + component_categories.append('cooling') + sizes.append(installed_capacity) + if generation_system.system_type == cte.HEAT_PUMP: + item_type = self.heat_pump_type(generation_system) + system_components.append(item_type) + else: + system_components.append('D302090_template_cooling') if generation_system.energy_storage_systems is not None: energy_storage_systems = generation_system.energy_storage_systems for storage_system in energy_storage_systems: diff --git a/scripts/costs/constants.py b/scripts/costs/constants.py index 18543adf..87372d3d 100644 --- a/scripts/costs/constants.py +++ b/scripts/costs/constants.py @@ -12,10 +12,12 @@ SKIN_RETROFIT = 1 SYSTEM_RETROFIT_AND_PV = 2 SKIN_RETROFIT_AND_SYSTEM_RETROFIT_AND_PV = 3 PV = 4 +SYSTEM_RETROFIT = 5 RETROFITTING_SCENARIOS = [ CURRENT_STATUS, SKIN_RETROFIT, SYSTEM_RETROFIT_AND_PV, SKIN_RETROFIT_AND_SYSTEM_RETROFIT_AND_PV, - PV + PV, + SYSTEM_RETROFIT ] diff --git a/scripts/costs/cost.py b/scripts/costs/cost.py index 2ebd1ffc..91a13034 100644 --- a/scripts/costs/cost.py +++ b/scripts/costs/cost.py @@ -93,10 +93,12 @@ class Cost: global_capital_costs['B1010_superstructure'] ) df_capital_costs_systems = ( - global_capital_costs['D3020_heat_and_cooling_generating_systems'] + - global_capital_costs['D3040_distribution_systems'] + - global_capital_costs['D3050_other_hvac_ahu'] + - global_capital_costs['D3060_storage_systems'] + + global_capital_costs['D3020_simultaneous_heat_and_cooling_generating_systems'] + + global_capital_costs['D3030_heating_systems'] + + global_capital_costs['D3040_cooling_systems'] + + global_capital_costs['D3050_distribution_systems'] + + global_capital_costs['D3060_other_hvac_ahu'] + + global_capital_costs['D3070_storage_systems'] + global_capital_costs['D40_dhw'] + global_capital_costs['D2010_photovoltaic_system'] ) diff --git a/scripts/costs/total_maintenance_costs.py b/scripts/costs/total_maintenance_costs.py index 1e44f3d0..7a11b9b6 100644 --- a/scripts/costs/total_maintenance_costs.py +++ b/scripts/costs/total_maintenance_costs.py @@ -57,14 +57,15 @@ class TotalMaintenanceCosts(CostBase): for energy_system in energy_systems: if cte.COOLING in energy_system.demand_types: for generation_system in energy_system.generation_systems: - if generation_system.system_type == cte.HEAT_PUMP and generation_system.source_medium == cte.AIR: - cooling_equipments['air_source_heat_pump'] = generation_system.nominal_cooling_output / 1000 - elif generation_system.system_type == cte.HEAT_PUMP and generation_system.source_medium == cte.GROUND: - cooling_equipments['ground_source_heat_pump'] = generation_system.nominal_cooling_output / 1000 - elif generation_system.system_type == cte.HEAT_PUMP and generation_system.source_medium == cte.WATER: - cooling_equipments['water_source_heat_pump'] = generation_system.nominal_cooling_output / 1000 - else: - cooling_equipments['general_cooling_equipment'] = generation_system.nominal_cooling_output / 1000 + if generation_system.fuel_type == cte.ELECTRICITY: + if generation_system.system_type == cte.HEAT_PUMP and generation_system.source_medium == cte.AIR: + cooling_equipments['air_source_heat_pump'] = generation_system.nominal_cooling_output / 1000 + elif generation_system.system_type == cte.HEAT_PUMP and generation_system.source_medium == cte.GROUND: + cooling_equipments['ground_source_heat_pump'] = generation_system.nominal_cooling_output / 1000 + elif generation_system.system_type == cte.HEAT_PUMP and generation_system.source_medium == cte.WATER: + cooling_equipments['water_source_heat_pump'] = generation_system.nominal_cooling_output / 1000 + else: + cooling_equipments['general_cooling_equipment'] = generation_system.nominal_cooling_output / 1000 if cte.HEATING in energy_system.demand_types: for generation_system in energy_system.generation_systems: if generation_system.system_type == cte.HEAT_PUMP and generation_system.source_medium == cte.AIR: diff --git a/scripts/random_assignation.py b/scripts/random_assignation.py index ac6eb454..7483cced 100644 --- a/scripts/random_assignation.py +++ b/scripts/random_assignation.py @@ -29,9 +29,9 @@ residential_systems_percentage = {'system 1 gas': 100, 'system 8 electricity': 0} residential_new_systems_percentage = {'PV+ASHP+GasBoiler+TES': 0, - 'PV+4Pipe+DHW': 0, - 'Central Heating+Unitary Cooling+Unitary DHW': 50, - 'Central Heating+Unitary Cooling+Unitary DHW+PV': 50, + 'PV+4Pipe+DHW': 100, + 'Central Heating+Unitary Cooling+Unitary DHW': 0, + 'Central Heating+Unitary Cooling+Unitary DHW+PV': 0, 'PV+ASHP+ElectricBoiler+TES': 0, 'PV+GSHP+GasBoiler+TES': 0, 'PV+GSHP+ElectricBoiler+TES': 0, From 7369bc65a4c7c6de39750499fdb68a849c275540 Mon Sep 17 00:00:00 2001 From: s_ranjbar Date: Fri, 28 Jun 2024 16:40:37 -0400 Subject: [PATCH 10/15] fix: first stages of report creation are started --- main.py | 2 +- report_test.py | 41 +++++ scripts/energy_system_retrofit_report.py | 196 +++++++++++++++++++++++ scripts/report_creation.py | 61 ++++--- 4 files changed, 275 insertions(+), 25 deletions(-) create mode 100644 report_test.py create mode 100644 scripts/energy_system_retrofit_report.py diff --git a/main.py b/main.py index a0383a6d..5140022c 100644 --- a/main.py +++ b/main.py @@ -65,4 +65,4 @@ for building in city.buildings: costs.loc['global_capital_costs', f'Scenario {SYSTEM_RETROFIT}'].to_csv( output_path / f'{building.name}_cc.csv') costs.loc['global_maintenance_costs', f'Scenario {SYSTEM_RETROFIT}'].to_csv( - output_path / f'{building.name}_m.csv') \ No newline at end of file + output_path / f'{building.name}_m.csv') diff --git a/report_test.py b/report_test.py new file mode 100644 index 00000000..638b9213 --- /dev/null +++ b/report_test.py @@ -0,0 +1,41 @@ +from pathlib import Path +import subprocess +from scripts.ep_run_enrich import energy_plus_workflow +from hub.imports.geometry_factory import GeometryFactory +from hub.helpers.dictionaries import Dictionaries +from hub.imports.construction_factory import ConstructionFactory +from hub.imports.usage_factory import UsageFactory +from hub.imports.weather_factory import WeatherFactory +from hub.imports.results_factory import ResultFactory +from scripts.energy_system_retrofit_report import EnergySystemRetrofitReport +from scripts.geojson_creator import process_geojson +from scripts import random_assignation +from hub.imports.energy_systems_factory import EnergySystemsFactory +from scripts.energy_system_sizing import SystemSizing +from scripts.energy_system_retrofit_results import system_results, new_system_results +from scripts.energy_system_sizing_and_simulation_factory import EnergySystemsSimulationFactory +from scripts.costs.cost import Cost +from scripts.costs.constants import SKIN_RETROFIT_AND_SYSTEM_RETROFIT_AND_PV, SYSTEM_RETROFIT_AND_PV +import hub.helpers.constants as cte +from hub.exports.exports_factory import ExportsFactory + +geojson_file = process_geojson(x=-73.5681295982132, y=45.49218262677643, diff=0.0001) +file_path = (Path(__file__).parent / 'input_files' / 'output_buildings.geojson') +output_path = (Path(__file__).parent / 'out_files').resolve() +city = GeometryFactory('geojson', + path=file_path, + height_field='height', + year_of_construction_field='year_of_construction', + function_field='function', + function_to_hub=Dictionaries().montreal_function_to_hub_function).city +ConstructionFactory('nrcan', city).enrich() +UsageFactory('nrcan', city).enrich() +WeatherFactory('epw', city).enrich() +ExportsFactory('sra', city, output_path).export() +sra_path = (output_path / f'{city.name}_sra.xml').resolve() +subprocess.run(['sra', str(sra_path)]) +ResultFactory('sra', city, output_path).enrich() +energy_plus_workflow(city) + +(EnergySystemRetrofitReport(city, output_path, 'PV Implementation and HVAC Retrofit'). + create_report(current_system=None, new_system=None)) diff --git a/scripts/energy_system_retrofit_report.py b/scripts/energy_system_retrofit_report.py new file mode 100644 index 00000000..6209c5bc --- /dev/null +++ b/scripts/energy_system_retrofit_report.py @@ -0,0 +1,196 @@ +import os +import hub.helpers.constants as cte +import matplotlib.pyplot as plt +import random +import matplotlib.colors as mcolors +from matplotlib import cm +from scripts.report_creation import LatexReport +import matplotlib as mpl +from matplotlib.ticker import MaxNLocator +import numpy as np +from pathlib import Path + +class EnergySystemRetrofitReport: + def __init__(self, city, output_path, retrofit_scenario): + self.city = city + self.output_path = output_path + self.content = [] + self.report = LatexReport('energy_system_retrofit_report', + 'Energy System Retrofit Report', retrofit_scenario, output_path) + self.charts_path = Path(output_path) / 'charts' + self.charts_path.mkdir(parents=True, exist_ok=True) + + def building_energy_info(self): + table_data = [ + ["Building Name", "Year of Construction", "function", "Yearly Heating Demand (MWh)", + "Yearly Cooling Demand (MWh)", "Yearly DHW Demand (MWh)", "Yearly Electricity Demand (MWh)"] + ] + intensity_table_data = [["Building Name", "Total Floor Area m2", "Heating Demand Intensity kWh/m2", + "Cooling Demand Intensity kWh/m2", "Electricity Intensity kWh/m2"]] + + for building in self.city.buildings: + total_floor_area = 0 + for zone in building.thermal_zones_from_internal_zones: + total_floor_area += zone.total_floor_area + building_data = [ + building.name, + str(building.year_of_construction), + building.function, + str(format(building.heating_demand[cte.YEAR][0] / 3.6e9, '.2f')), + str(format(building.cooling_demand[cte.YEAR][0] / 3.6e9, '.2f')), + str(format(building.domestic_hot_water_heat_demand[cte.YEAR][0] / 3.6e9, '.2f')), + str(format( + (building.lighting_electrical_demand[cte.YEAR][0] + building.appliances_electrical_demand[cte.YEAR][0]) + / 3.6e9, '.2f')), + ] + intensity_data = [ + building.name, + str(format(total_floor_area, '.2f')), + str(format(building.heating_demand[cte.YEAR][0] / (3.6e6 * total_floor_area), '.2f')), + str(format(building.cooling_demand[cte.YEAR][0] / (3.6e6 * total_floor_area), '.2f')), + str(format( + (building.lighting_electrical_demand[cte.YEAR][0] + building.appliances_electrical_demand[cte.YEAR][0]) / + (3.6e6 * total_floor_area), '.2f')) + ] + table_data.append(building_data) + intensity_table_data.append(intensity_data) + + self.report.add_table(table_data, caption='Buildings Energy Consumption Data') + self.report.add_table(intensity_table_data, caption='Buildings Energy Use Intensity Data') + + def monthly_demands(self): + heating = [] + cooling = [] + dhw = [] + lighting_appliance = [] + for i in range(12): + heating_demand = 0 + cooling_demand = 0 + dhw_demand = 0 + lighting_appliance_demand = 0 + for building in self.city.buildings: + heating_demand += building.heating_demand[cte.MONTH][i] / 3.6e6 + cooling_demand += building.cooling_demand[cte.MONTH][i] / 3.6e6 + dhw_demand += building.domestic_hot_water_heat_demand[cte.MONTH][i] / 3.6e6 + lighting_appliance_demand += building.lighting_electrical_demand[cte.MONTH][i] / 3.6e6 + heating.append(heating_demand) + cooling.append(cooling_demand) + dhw.append(dhw_demand) + lighting_appliance.append(lighting_appliance_demand) + + monthly_demands = {'heating': heating, + 'cooling': cooling, + 'dhw': dhw, + 'lighting_appliance': lighting_appliance} + return monthly_demands + + def plot_monthly_energy_demands(self, demands, file_name): + # Data preparation + months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] + heating = demands['heating'] + cooling = demands['cooling'] + dhw = demands['dhw'] + electricity = demands['lighting_appliance'] + + # Plotting + fig, axs = plt.subplots(2, 2, figsize=(15, 10), dpi=96) + fig.suptitle('Monthly Energy Demands', fontsize=16, weight='bold', alpha=.8) + + # Heating bar chart + axs[0, 0].bar(months, heating, color='#2196f3', width=0.6, zorder=2) + axs[0, 0].grid(which="major", axis='x', color='#DAD8D7', alpha=0.5, zorder=1) + axs[0, 0].grid(which="major", axis='y', color='#DAD8D7', alpha=0.5, zorder=1) + axs[0, 0].set_xlabel('Month', fontsize=12, labelpad=10) + axs[0, 0].set_ylabel('Heating Demand (kWh)', fontsize=12, labelpad=10) + axs[0, 0].set_title('Monthly Heating Demands', fontsize=14, weight='bold', alpha=.8) + axs[0, 0].xaxis.set_major_locator(MaxNLocator(integer=True)) + axs[0, 0].yaxis.set_major_locator(MaxNLocator(integer=True)) + axs[0, 0].set_xticks(np.arange(len(months))) + axs[0, 0].set_xticklabels(months, rotation=45, ha='right') + axs[0, 0].bar_label(axs[0, 0].containers[0], padding=3, color='black', fontsize=8) + axs[0, 0].spines[['top', 'left', 'bottom']].set_visible(False) + axs[0, 0].spines['right'].set_linewidth(1.1) + average_heating = np.mean(heating) + axs[0, 0].axhline(y=average_heating, color='grey', linewidth=2, linestyle='--') + axs[0, 0].text(len(months)-1, average_heating, f'Average = {average_heating:.1f} kWh', ha='right', va='bottom', color='grey') + + # Cooling bar chart + axs[0, 1].bar(months, cooling, color='#ff5a5f', width=0.6, zorder=2) + axs[0, 1].grid(which="major", axis='x', color='#DAD8D7', alpha=0.5, zorder=1) + axs[0, 1].grid(which="major", axis='y', color='#DAD8D7', alpha=0.5, zorder=1) + axs[0, 1].set_xlabel('Month', fontsize=12, labelpad=10) + axs[0, 1].set_ylabel('Cooling Demand (kWh)', fontsize=12, labelpad=10) + axs[0, 1].set_title('Monthly Cooling Demands', fontsize=14, weight='bold', alpha=.8) + axs[0, 1].xaxis.set_major_locator(MaxNLocator(integer=True)) + axs[0, 1].yaxis.set_major_locator(MaxNLocator(integer=True)) + axs[0, 1].set_xticks(np.arange(len(months))) + axs[0, 1].set_xticklabels(months, rotation=45, ha='right') + axs[0, 1].bar_label(axs[0, 1].containers[0], padding=3, color='black', fontsize=8) + axs[0, 1].spines[['top', 'left', 'bottom']].set_visible(False) + axs[0, 1].spines['right'].set_linewidth(1.1) + average_cooling = np.mean(cooling) + axs[0, 1].axhline(y=average_cooling, color='grey', linewidth=2, linestyle='--') + axs[0, 1].text(len(months)-1, average_cooling, f'Average = {average_cooling:.1f} kWh', ha='right', va='bottom', color='grey') + + # DHW bar chart + axs[1, 0].bar(months, dhw, color='#4caf50', width=0.6, zorder=2) + axs[1, 0].grid(which="major", axis='x', color='#DAD8D7', alpha=0.5, zorder=1) + axs[1, 0].grid(which="major", axis='y', color='#DAD8D7', alpha=0.5, zorder=1) + axs[1, 0].set_xlabel('Month', fontsize=12, labelpad=10) + axs[1, 0].set_ylabel('DHW Demand (kWh)', fontsize=12, labelpad=10) + axs[1, 0].set_title('Monthly DHW Demands', fontsize=14, weight='bold', alpha=.8) + axs[1, 0].xaxis.set_major_locator(MaxNLocator(integer=True)) + axs[1, 0].yaxis.set_major_locator(MaxNLocator(integer=True)) + axs[1, 0].set_xticks(np.arange(len(months))) + axs[1, 0].set_xticklabels(months, rotation=45, ha='right') + axs[1, 0].bar_label(axs[1, 0].containers[0], padding=3, color='black', fontsize=8) + axs[1, 0].spines[['top', 'left', 'bottom']].set_visible(False) + axs[1, 0].spines['right'].set_linewidth(1.1) + average_dhw = np.mean(dhw) + axs[1, 0].axhline(y=average_dhw, color='grey', linewidth=2, linestyle='--') + axs[1, 0].text(len(months)-1, average_dhw, f'Average = {average_dhw:.1f} kWh', ha='right', va='bottom', color='grey') + + # Electricity bar chart + axs[1, 1].bar(months, electricity, color='#ffc107', width=0.6, zorder=2) + axs[1, 1].grid(which="major", axis='x', color='#DAD8D7', alpha=0.5, zorder=1) + axs[1, 1].grid(which="major", axis='y', color='#DAD8D7', alpha=0.5, zorder=1) + axs[1, 1].set_xlabel('Month', fontsize=12, labelpad=10) + axs[1, 1].set_ylabel('Electricity Demand (kWh)', fontsize=12, labelpad=10) + axs[1, 1].set_title('Monthly Electricity Demands', fontsize=14, weight='bold', alpha=.8) + axs[1, 1].xaxis.set_major_locator(MaxNLocator(integer=True)) + axs[1, 1].yaxis.set_major_locator(MaxNLocator(integer=True)) + axs[1, 1].set_xticks(np.arange(len(months))) + axs[1, 1].set_xticklabels(months, rotation=45, ha='right') + axs[1, 1].bar_label(axs[1, 1].containers[0], padding=3, color='black', fontsize=8) + axs[1, 1].spines[['top', 'left', 'bottom']].set_visible(False) + axs[1, 1].spines['right'].set_linewidth(1.1) + average_electricity = np.mean(electricity) + axs[1, 1].axhline(y=average_electricity, color='grey', linewidth=2, linestyle='--') + axs[1, 1].text(len(months)-1, average_electricity * 0.95, f'Average = {average_electricity:.1f} kWh', ha='right', va='top', color='grey') + + # Set a white background + fig.patch.set_facecolor('white') + + # Adjust the margins around the plot area + plt.subplots_adjust(left=0.05, right=0.95, top=0.9, bottom=0.1, wspace=0.3, hspace=0.5) + + + # Save the plot + chart_path = self.charts_path / f'{file_name}.png' + plt.savefig(chart_path, bbox_inches='tight') + plt.close() + + return chart_path + + def create_report(self, current_system, new_system): + os.chdir(self.charts_path) + self.report.add_section('Current Status') + self.report.add_subsection('City Buildings Characteristics') + self.building_energy_info() + monthly_demands = self.monthly_demands() + monthly_demands_path = str(Path(self.charts_path / 'monthly_demands.png')) + self.plot_monthly_energy_demands(demands=monthly_demands, + file_name='monthly_demands') + self.report.add_image('monthly_demands.png', 'Total Monthly Energy Demands in City' ) + self.report.save_report() + self.report.compile_to_pdf() diff --git a/scripts/report_creation.py b/scripts/report_creation.py index cca587f4..e1e88927 100644 --- a/scripts/report_creation.py +++ b/scripts/report_creation.py @@ -1,38 +1,50 @@ import subprocess import datetime +import os +from pathlib import Path + class LatexReport: - def __init__(self, file_name): - self.file_name = file_name - self.content = [] - self.content.append(r'\documentclass{article}') - self.content.append(r'\usepackage[margin=2.5cm]{geometry}') # Adjust page margins - self.content.append(r'\usepackage{graphicx}') - self.content.append(r'\usepackage{tabularx}') - self.content.append(r'\begin{document}') - # Get current date and time - current_datetime = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - self.content.append(r'\title{Energy System Analysis Report - ' + current_datetime + r'}') - self.content.append(r'\author{Next-Generation Cities Institute}') - self.content.append(r'\date{}') # Remove the date field, as it's included in the title now - self.content.append(r'\maketitle') + def __init__(self, file_name, title, subtitle, output_path): + self.file_name = file_name + self.output_path = Path(output_path) / 'report' + self.output_path.mkdir(parents=True, exist_ok=True) + self.file_path = self.output_path / f"{file_name}.tex" + self.content = [] + self.content.append(r'\documentclass{article}') + self.content.append(r'\usepackage[margin=2.5cm]{geometry}') + self.content.append(r'\usepackage{graphicx}') + self.content.append(r'\usepackage{tabularx}') + self.content.append(r'\begin{document}') + + current_datetime = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + self.content.append(r'\title{' + title + '}') + self.content.append(r'\author{Next-Generation Cities Institute}') + self.content.append(r'\date{}') + self.content.append(r'\maketitle') + + self.content.append(r'\begin{center}') + self.content.append(r'\large ' + subtitle + r'\\') + self.content.append(r'\large ' + current_datetime) + self.content.append(r'\end{center}') def add_section(self, section_title): - self.content.append(r'\section{' + section_title + r'}') + self.content.append(r'\section{' + section_title + r'}') def add_subsection(self, subsection_title): - self.content.append(r'\subsection{' + subsection_title + r'}') + self.content.append(r'\subsection{' + subsection_title + r'}') def add_text(self, text): - self.content.append(text) + self.content.append(text) def add_table(self, table_data, caption=None, first_column_width=None): num_columns = len(table_data[0]) - total_width = 0.9 # Default total width + total_width = 0.9 if first_column_width is not None: first_column_width_str = str(first_column_width) + 'cm' - total_width -= first_column_width / 16.0 # Adjust total width for the first column + total_width -= first_column_width / 16.0 if caption: self.content.append(r'\begin{table}[htbp]') @@ -55,19 +67,20 @@ class LatexReport: if caption: self.content.append(r'\begin{figure}[htbp]') self.content.append(r'\centering') - self.content.append(r'\includegraphics[width=0.8\textwidth]{' + image_path + r'}') + self.content.append(r'\includegraphics[width=\textwidth]{' + image_path + r'}') self.content.append(r'\caption{' + caption + r'}') self.content.append(r'\end{figure}') else: self.content.append(r'\begin{figure}[htbp]') self.content.append(r'\centering') - self.content.append(r'\includegraphics[width=0.8\textwidth]{' + image_path + r'}') + self.content.append(r'\includegraphics[width=\textwidth]{' + image_path + r'}') self.content.append(r'\end{figure}') def save_report(self): - self.content.append(r'\end{document}') # Add this line to close the document - with open(self.file_name, 'w') as f: + self.content.append(r'\end{document}') + with open(self.file_path, 'w') as f: f.write('\n'.join(self.content)) def compile_to_pdf(self): - subprocess.run(['pdflatex', self.file_name]) + subprocess.run(['pdflatex', '-output-directory', str(self.output_path), str(self.file_path)]) + From c4f98a30c1f81525eec086669ec45d3c91f114e6 Mon Sep 17 00:00:00 2001 From: s_ranjbar Date: Mon, 15 Jul 2024 08:51:15 -0400 Subject: [PATCH 11/15] fix: current and retrofitted status energy consumption analysis and system schematic added to the report --- .../energy_systems/emission_system.py | 2 +- .../energy_systems/montreal_custom_catalog.py | 2 +- .../montreal_future_system_catalogue.py | 2 +- .../energy_systems/emission_system.py | 2 +- .../energy_systems/schemas/PV+4Pipe+DHW.jpg | Bin 0 -> 79991 bytes ...ontreal_custom_energy_system_parameters.py | 10 +- ...ntreal_future_energy_systems_parameters.py | 10 +- input_files/output_buildings_expanded.geojson | 863 ++++++++++++++++++ report_test.py | 38 +- scripts/energy_system_retrofit_report.py | 535 ++++++++--- scripts/energy_system_retrofit_results.py | 123 ++- scripts/geojson_creator.py | 7 +- scripts/pv_feasibility.py | 34 + scripts/random_assignation.py | 8 +- scripts/report_creation.py | 51 +- .../system_simulation_models/archetype13.py | 18 +- 16 files changed, 1486 insertions(+), 219 deletions(-) create mode 100644 hub/data/energy_systems/schemas/PV+4Pipe+DHW.jpg create mode 100644 input_files/output_buildings_expanded.geojson create mode 100644 scripts/pv_feasibility.py diff --git a/hub/catalog_factories/data_models/energy_systems/emission_system.py b/hub/catalog_factories/data_models/energy_systems/emission_system.py index a8ac91b6..538954d3 100644 --- a/hub/catalog_factories/data_models/energy_systems/emission_system.py +++ b/hub/catalog_factories/data_models/energy_systems/emission_system.py @@ -10,7 +10,7 @@ class EmissionSystem: """ Emission system class """ - def __init__(self, system_id, model_name=None, system_type=None, parasitic_energy_consumption=None): + def __init__(self, system_id, model_name=None, system_type=None, parasitic_energy_consumption=0): self._system_id = system_id self._model_name = model_name diff --git a/hub/catalog_factories/energy_systems/montreal_custom_catalog.py b/hub/catalog_factories/energy_systems/montreal_custom_catalog.py index d3e37e36..cace9278 100644 --- a/hub/catalog_factories/energy_systems/montreal_custom_catalog.py +++ b/hub/catalog_factories/energy_systems/montreal_custom_catalog.py @@ -135,7 +135,7 @@ class MontrealCustomCatalog(Catalog): equipment_id = float(equipment['@id']) equipment_type = equipment['@type'] model_name = equipment['name'] - parasitic_consumption = None + parasitic_consumption = 0 if 'parasitic_consumption' in equipment: parasitic_consumption = float(equipment['parasitic_consumption']['#text']) / 100 diff --git a/hub/catalog_factories/energy_systems/montreal_future_system_catalogue.py b/hub/catalog_factories/energy_systems/montreal_future_system_catalogue.py index 625e362c..a4477ba2 100644 --- a/hub/catalog_factories/energy_systems/montreal_future_system_catalogue.py +++ b/hub/catalog_factories/energy_systems/montreal_future_system_catalogue.py @@ -262,7 +262,7 @@ class MontrealFutureSystemCatalogue(Catalog): system_id = None model_name = None system_type = None - parasitic_energy_consumption = None + parasitic_energy_consumption = 0 emission_system = EmissionSystem(system_id=system_id, model_name=model_name, system_type=system_type, diff --git a/hub/city_model_structure/energy_systems/emission_system.py b/hub/city_model_structure/energy_systems/emission_system.py index 32bf7c17..e8773013 100644 --- a/hub/city_model_structure/energy_systems/emission_system.py +++ b/hub/city_model_structure/energy_systems/emission_system.py @@ -13,7 +13,7 @@ class EmissionSystem: def __init__(self): self._model_name = None self._type = None - self._parasitic_energy_consumption = None + self._parasitic_energy_consumption = 0 @property def model_name(self): diff --git a/hub/data/energy_systems/schemas/PV+4Pipe+DHW.jpg b/hub/data/energy_systems/schemas/PV+4Pipe+DHW.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7daad9875de9fc55791c58b97f36bbcef48aa9a8 GIT binary patch literal 79991 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%0AYW(ILm zpcoz=0lBQ5_|gg{M|nF##`vf(-1r^sgkR&u0(W5jJnPUO&ec>Ing?mOz&NT`0_7b8 zR9}=q@MqG}hCVdKO9cWl@^8rP1LI0LZ#ra5Cd?WSlh#kwuCM%3yw@k49v6qNusQ_E zx4d|xuy9i#2u#BNzFTtUbX7pl8C@Qn5;K#*K^6!Gp_3%5&a#Ks=XwQHa*l0Jy!BE$ zDPRpf?8y*QBn!<7Q2F}mgN3Slp7rg$oQmz5KjZ8$BsF)vL^tdW=5I%>uMtlmS>mk8^~8C@V73^PEee7O!?RvwTnJ4J8#_Td5pW8;q@;$05zK3^2*5A3> z>vwVc_9r5G!XZbpr+wK_M%2qZM&1XQGDQSRdp`mUQqDq4>Y8x0iel#Z$>;YZ6epECcddPcCoaz@u^}a@ z1w+#lm=zm?)!0zoG@7Lk)}RI@junZP0^-mY*aqAC2FuL;X($@|&$G^hS^UFO4gc5# zq1^eeI^K-=J34NS42?)e1WQesxd!j0?oA@wsk=m!`K5hhT)a`8Yjykb*j29B?2O9K zVwo*(2#6ah47FsXivo6~8L)v{o&Q47A_xNh$0T*2D81e zKq<`ROMAc)oFgJte>{iig0aXV-~_q`8|uiaV7XfLS6{7J1J7gX=TXCwP z`*e4OyT*e=-b8KMS@B0nR}EUuaIA*Y%$V_NaGd=POcJT=CYWZUQ4@!&( zhNmST@s4g4el2<@JoVc)t)u(hbro5?=!*TK4DDBO!-fq*A-E#HvkE^YJLNPC=zfpm zH(j}4_HNO-Mw0Bu5W$l%k;^;#g17yYYHe;8JN^A?^GftyyG#CB7559Rh_eOnF|NBK z&O&uEkdiZJI>odR!;4dUNte7`y+0ed*1dV(l=^-f<)Guc-mj>2{Z%%$T94z!i)TMH za#=bGPuVL{^=L}SHfor35^}Ou#u@MSP(4*&r{Dsu`Or?e;f zSe9$4x7mCx78(D19Nl|t?}3Ch;l_0bc5!w*P1A~YNL94Da#v3w`q;&D2YBS4=k-lU z&M-&kx3KDf*>|XYfIi}pzY|m=HQf{*CW_kLQ0afk&nXgYzUM=N+?iI#%L}@0OSk?< z6cOnH0UQdMS1HnjVV}Bq;B+p<1x&;{XUa zLP!VFGX}k&hYJFl3Y5ZhSw81na*b6UM-7$ITB@0eI7M>Mwe|)I_Gx+gl#0}rd8z&r z5<2uyo`Lq$XSut-Sb(R@kbqI<+Ec5IyFqyn;)J+g&2B8VlfG7)WZ`cuW6*vlavT0_ zVs=sR9pDP!=yZ?ZFMgO;=muq!lr=U0CDP_ysF$05q&T7KIrdp{@Nm()g_3w!9Cw%E z$>J9+#GOMbhKU=DDGjY>;+ix+W!~?LDXdO_G9BaJoagy!Ff%HDc={+R?~s|j3LO*N z;fE^auKQ-uN~_vqzcuZ{_#lUaT74+yQ_6X*hS&QaFFg@*$s#IiZv&mq@2xf)`6O!K z8KEvQ{hV~n`I?=3|C4+ACjATkE#9%r^H0_hH={4ZtEc>80iB8?Xr`bm6HjOn>c_Co zWsDH%yhswrT~7-^JAn+Df2ddrDPZxv2eUCnZl_Dl5|)n?4X z9yiqTga#|b?c8%W+AET?W=gektxHi(g5=h#v3E9@*4&U){L#t~#n1*2RSGxlCd`kl zw7~tEQ@zE-%>ZF+A;g)V+d0Tpn~%~Lv{3>2URfA?1> zZHq*P0P`M839CW#Vm{YaEd`VKaB-ssLTqW9R&Dfj^?$g^W8zoTFe0RU{L93ok z?i5j$>|Js{gTT{9Ew(aOd0=egUF=Wtz%jGim~-u=z#g+%Sa>+$9{eV_T|HC&B_d2& zBQ?+7_`WOd*ees`S@$Z>G9C-;os}Cmd6z$7S7x;6%2R0WT5e=3@|N5l<&zY%hC3hk z&3H!L8_ehV6{d!GZcjs9i`TxQ)m^gF_V|Bz@-nr38z**9POpCg-L{QuExr0@t6bcG+6dPd^=T z?8H~^8vpT17*R@^@unovVCG|n>Ij|_E6)BklY{gP$nyFf;UkOYbvTi&>rm9((Q`iws1P_63P~hR8Se-e9$ngy%0;bQg^nKXE~jYu{mffc5jTXk9+v6LIm;r|kd;%H?mRfnQKyY63XZXOhuVz(u>%9aGd8^d zgH2gx{;38k*q=3Y5sZr@(Q|&Fk!tSRUssS%E)ftpEag6dKVyfb^_GB-M{xLqY{!Z1z>z~ZCNZ=@wf8;@! zkEaCo8cWCYcu!I9c<=bZ9@Ddilvj@nl*Tq48#~Lb|G&X?xSIj4OoIBc>9i9)+Qz&0 z1XSE!mR6=q=Wf2MJrq5g!RZUdgbROd$$Lq7#@c{XO#`ELJqv|oA#*joVB6N8=N~&9 zFmabjSSulf0`2q)UlxqdMI=un8adzgtbNYcw$6NY_YL}5dHh_YhHRRV`WxA!(K44q z39pHc^Q&t3aG18!nY5-S00k8N9}i_+(15Aa8uSFlbIuXd1W@d*%7xWv>dV}Oc$14T zBXHc4N3*yL&7H=cN6zp?^0n{ek^f=qRj<}yI0EY3=0{(;ECul&NAz<}>z(k-j`3KB zGTW81xU8?%1lQ~06h~D>aMH{yQS!6wXGq|POs)JTm%-kr9x^8?_e{NY+r&55x_;fU zdP2A7x`F8`zpy1Oj2LFTeyl;krgQn3XNwX&E$-ae1 zDeYx-hl=}Cx48W4^B%*I>(5#}iq&?7pvR-)hV{gf`B`bOR;-X@U<$n!U_&uwZ_5u@ ze=~Y+-@g8hf{aA!u`j~i&k3C*5v-pH6^>e#6|yRgGwbq|_E+(k-MPgm%XjyOpy-{Y z!LkfZ%0vUqJ;josWTptlJ?U3gsd>C*clyIpnG%J2P4|rEkD@pM?sir+%x&Tk15Ncb ziK&cM*m*OYaEyO~>abh{yc)n&D$-~AvMGHn?a(yL@B6J@&P8SH-hk2JuOB|V+P(5l z0E4sJLvY_+H!bsnCt<*k>-vq8a|bbxKv8p4Sg-=(M-%92Z;|ygr255WQytG){gm{;3H@Eut@4Tvky}R-d2qH zt{0B?eb_^s4IPOQ zVQ7(w8!*QgE&Gj2c9@MfPWCM6jcU&iUb^=|_@rZlM3~mSqGClF#lUxp%7gP;|;O`Js1%q*b17;PoB@GUq6IvK;HlVgV>+x%`(9--sJB!bOvN-x6ZVyhf&?PM|gyH3< zI55(!X1OR<>cF&q2Q`X5R(`Q|(``nIQ32?9iuJRrWflP=3@56SnK0^p?Dj>??5rm? z#Bzg|@8#XEM_kd2teeI7vp(rcBX}}8;#L6MOcL%#vRjn3i*~RwADZsM-! z2P9(}%%v;9INqu4LVhcIO~u>rVY};!RJzgTE4lvXj*t_A%#WhD;Cd(G*q1}5bm{9r zX{OE*m57d*RZU_zk#^Vxy=Em2A4Qi*&dd+9p+Tp5b(R>lS|fQ%Vx{vZ&ufbf+>3W* z#pXDlDR}6njAU|wk+x;rv^lMxSO5KQb%Q!C(KdD!&g%H@fd)Y=u%iiX#boAV1)q*1&o#A^8 zj%-<1+xmZ-p$@!-Va?-z*pgN#1U+01FSzLfOQ+;>9~(O9S@4q>=TAj72}RnA*tID6 z8fhQHic!ZbpE$Nx4Ca{(mz)SgKlI}bP>rM$t4qG(g>jh zA5}qH8?~a;bf;Snb{BSoi9DG=d*CIN@#b^Sla1&><3^%LHRp(`D9(Zsm9S)|CYa!s zGdQ^x8RqGfcKJ}IN%a9=65}YZmP&iZv0aZ+^Ic0@znM8ece8n87a7-?w%#l)>8Zs_a>=In{31RrGpwrEf83 zlm6SId~b7Zir@Sw=D2WDc6#X*Q6+dU?qC<|AYhlXYJ~M}-rudKtSjzLe7ZE5Oi0O6 zXe+CF6<>BqCVx0H!!7IaiN#23V2pUW=f-B(-;Q5t*}JzrM-6FOhkvosX?uz2Q~l>?luBu^oHG*M;o){mma7-y-1_-kEQ@48;i z_+DCCOLXv%~FYKGl`Pdd$(A<)kP(V|F7CngWW$-IS#}a&Jon zhVpfmDv7M~isANsvXKD7TJ^`5i5&fi2tPhtSAjxm$k;A`K2AkN`%YVwC!eWu6jr?- z6&1r5pZuPC^Z7E3N$r~oorQN_RbS^+R(iK}X=~qSHdK~cLnb8PUlNkE^@NjI!n2qL zNvZPm?+sVHUW*>uXjUmub2bTgrK){^+lol#o++o~o$cY*S^6$razKkY(X^Wr^&Day z;ut0U(F?(aQo~v2%qiTg<{Gd>^^Gu?4i5cbumBpLp?|)Bf4pTw-!6c7!@r)G{|Iu} z0^(dC3~&H_l+{cou1T|@AAI=n;M-=HOb}(7N?Ln?U-e)^E9R)bIk9YkJJ77t;dDu0 zk4$>9q5HexM{C0W?&B`}4UU!S z(b^7)1U+ooH0SgAR0bg8-u{k0-(fh~UJ6Nh+FFRUu_^64G%@YKKv#%odBy*V=Wcgr`X$P}b0nGs;%AEJ97uaz zzz=*Z!*rCto!72dny%Pb!TI@)?|^poQ`_aEGMj=a#jeAyy0@fL#oL65YYF!d_zDrGL-FF+ifv-C! z$76U(_O$zDJI(7J-La2*t%Gmh zSp~jZ<+m`K5wMWnF?hGMGt3@n1J^|MS}hy8x-m!i4t8r_b?|9_?io7W3U`3{?2zTW zw&=*Ny=okr?@gR|yIuBp-xqC#u@LO{w|8g`_Vt+Uj9Z|wYdB@3yig+sr|t<>z{ewx z)L{O*V(v=qpD7#n-Za{IP+G*_!QM)MI7_4}zuOvGMjAxbcZ|uOZgO?bcx7Z2hc6rn zLF+ja!VX~&Wv57n)^q(_oGo|>3OZ7%;6|I5t5eL$pJ$e1PY21n((u3_sy_j?VK&`- zT>=&>OE>f+_gu+rbZNP9n)+mRIu=^fj2$i`CXqI(eabUIlE^AY56oj;O{c#45o9nR zB;GNAOg9GH-tLCk7BK>euO$AF9Y)Z8`~WP@A|H{2i=Y$(Ay8py-HBI_LOZTKct*joh1dZ>MgI z&RXvaqK%Eu46kskaRmjr=MJ>{`+JG!=I}VC`hQjMdcI}PM=T^bUMqzxf(E4Ay_-}h z?X>G%m=Vt=GA>VVulV-B!@K&f2*p^PuU7!i5S;*=G%M;WmrplhT>YZD z=h5x(275^_*+Sgdx?6>(3?Qv5S72g`+YBKb12H)0XvCW?T4%qdv_l2ul6mq%^~xS@ zfjx!B@BGo&$4>D8=g??H_vbJ;nGq~iWTmwV&j8TO=4yRr#?HJV<83MK=F2t)&R4E& zQF!cu+c*rZON5W1|JH5y$1}t}uh2DUJCSg|(vNiQ!P9RKCJg!79E_(;VbmFlfVo`Y zh7QDHn6w3MXk~o_OFGV=w1?Hqj2dU+XE!O-+JoUNo_$Mw>-_m(XxE$R9)d~kDZIuf z)sJhZ3pi6o$Vv{>+WNdK+RcolN^EWsCpEFJVuUKccD@~?ihQn|aOOYz`QtgYi?gQWF+C25v z$HYvwoi#mvYgBic()w2d3?xQ1sUkbKP*5S-CL`v^9p4R|Wo<<#tP;4}4zBJ$lxaU- zmhtxW=;6cddS-jh?AIq=_N4pEZpI}=DOwA&ddA;c2TH}R%?3^dkE=37lSU;YYS%Lk zBU434srl;?-hB190x^$*yS+W+J(rChmwGe|`zSbt34mPW=J_FGTGgvcYG0#cr#8p1 z18^R%+F*DZ?Owdo3JD>NBjZJsye5?3@-ct!XF9ES_G-UA9J4Qb zzeC88t8Gf{8_McNl=~8X|N2EM!#&2EUQ}*5($0y`(B(p2pa&NLg7zF|PI1p@3g)C_ zCet?bWFFjH4Xccv4*&Ks{;2j$yl7$yP+45OfuvBlyszFg$vX0pR{>Vlczfg7eb2y) zlMcHaX}zYTboKLr&XF3u;dlV`meQ7yZR7!61#GMdXS0+_RuJJlWl6YEeyHxq^+MMi zNb})9G?W?ALl{aQor|Lz1FKBkUE43~L~qG=H+3&NtCKfr6+ANv(V2*L_}5K=z4#cS z3n_#R@j4W-SMa$`dR$mGXJ;)C6cLF?w=jA@`TDQ`igeYyJKA33%dx%{_EDEMH z6LX(hPhDJDk;G4{M>o7H*?jEM&|zq0+J;yPYz~^~zyui`&JrWxw)K|lcp8QlMXkAn zS+#vwSoiXwu@EudyQ>P1DCy*=<6pru6C*rVEX&)B3b%)!HH*nME%6*g6>DsNt)uUY zr<`lE^dTnTONCR6yy!>EPxwjz5l$D~ohPR#TJ}M< zA_8GQ{qb8JlT>HuRo>mX5@?Fmk1yp%e1_1WYqQ_81?LZR37F#YE9S3q)f#Axd0C@h z3#8P75DC{IW_r1fx>(5!}omNi+8@AOAY=&fw@JUapg$H_OP#4Tml$HVjF_ z{BZphUHPfB6H0k5eSWMV`YnCh*S0oGg;tJ~V(!8%J9 zXX_KCA?`6Yv%9fob@bV#Pf%detSVg*Ng6erRiPM;>`Zy#LJKerBj~qed-~X#Y%hGj zZz1Fyua^3P!NIh(tlC}7m%%#N33BT!Oh1AcHdc+L`9)RhJueI>x@VB<(K9C>U2yMU zNTF9^R2tp>pHeseYf~p`vCLP*>n0jBElPWoZ-#NBpS^;~wFTo7vAsVo?@uwI$6P%^ zcsjjk!Q%G^ufHLX^O%vv=%u4i1iOb-!Hbj{NwUClcBs3E7xLBFIbmC=J{72*EfXWm z(|+$M2imW%CTJGZ4w6z^S*FHh3+C-10CE^(I+h`9)T}?<%rf{rntz7j^n6l7cYD~x zg*ao0)Y8H|h19mKK_7YQ7NT2gaIF*&p5`YLVlkrCio^Z7(m$(ktgSunKF{ZJzGV4E za^96s7~ePRotONw-kSS&G*as}@T2KO%9pBHCC{iaRunoJKJjfqFSBrc|` zTli1cae#Eg|2XYp0`g3I`M}?i1~PB%gX*tMG*16Cr)DdF`xm z!*@=}?_1*)<8b=EJKq+?PqbCxx!-ZqU$$%yg$Hh1y#FIM+q1&NaRA9v0LZnXR4@~7 z5!dvBj-uec2^3>onE{~B8ccS|Dv0eKi3Kk(T2i$L7G>GP5s!OVtuN4qAc62N=(kWB zdN_x);0TKmr@H_<8*e3eA~s-gv^Jn<+@%10RkHO2@0&~oU})aV)F_eDO71@4 zTCz&LGyJeWK0Mw##Pl4;6#lhFJ-qp6T3@*C0i1G}7BNPb^PYj-ZiB(9&-krPG*B=V z$OVX~?q7`sZz#UbEYRf-?#1)_0C|cKMr2p&-(Tt}@~Ac=XZV>s@|fFtJM~`M4QOL+ zgN!31Txqa|ZF{pI!ey?%q5e z>bLD59;sAFvS*o!C`+=ZFqJGNCWIoUl08(m!I-gx5JD(olC82#mLZX`ODfqJ>zHKE z3>w2MJ!jwNzOL)LT=#ul_w&1--}CxCfAq@oG4uJHbDYO{9Pi`(J`PYt!VWJ_Ssu*~ zrc|`*$-6#ns}7LjiKXN!TkXE`+Nb~tAj11tK^)Bc#jX0?D8XLuwdH#riYoNnc1v5` z65P<#HG`~-H`|_6aeVQGgw|-RXGi1=BOUd7D?>E5;d?6?u1iYK9Fki1MH)a)L2wU1 zRVBQbx%Z1N<8cn9E533;QYlUR9-L3n+*alIrQ3%%cItApdW>QkE%Zc&hdTnm*(lxU zu}{n)P)FTL2-0z*?E*)F&nW@aH2}_~{<4QMZ(HzbS=bEzpe#2g?(slWaV>e02=N)- zks1hY)Gwh@cA*f|P4MC! zD1elgsm=gbx1jOM^r$Ip%R>}7c1tIgB~Uy#vG!|wa~RovshrsBwld^S3@Lr-sJz#9 zyrBYsLRQyK|8aasxW`L`bMwq+gmXPeTo790C297R`Z4i$k8h@oay^=~Yc$H(`?%6G{IXL?0 zB3BCyMln_%{@!yu%}$x$;gPm8*5(@G>Exo7)Tra;pc{!mmo-`(A-&Xj&z5L?VS**R zbhioMoGZ&8*>tYYdmefmx{V2;J5jz(oy2mZbSdr8-^tTnl}iVAI7_i_6f6(a&i~&t z4Ea(4lNY$X8yEr1cL1&ynwujlP~%cZxt6BkFA z%m%z`Oc;6xx_jSDn2w^3+f*h(P~pmK0YbT}qgmp|$x9X2?%s)IDN>G>6fM+qfB6C3 z;|3c#G2Q>v65$qZE;sCaa~pZ|#FDlc?b!}JC{qiNfpNTz&yf13%P8ODcOFt)JiuQ- z?oZHm)*bqq@r9}txrW*2YLju4P+;qE!eC1DyP|i_l^dyWXX~x|`hejeDP2r~1sWKQ z9Fse^HW6iexzF*T36z+hF(o=A5ng7zB-%K)LUphQA4=dwLfN~O7aWEa*C3KyANy%p zeaQx%YRw{9@;OJO{FkpC7h6g}WWuTg64?h(hOAKhn8_48%&3`l7bfH6^YU%L9oSf`k*8$bch5^G2_D8cHYJm9D61Q;}QNhKE1}v@as{R$DC2wjG zSuPRd0_saApb7QDi93*p3mL+x#=`Z@IAy&y&W7}Lqo>5{gAR0l&L2irlm zFjC!AuU=0-qsTr)oXa-A-t+HdC-J2oUe%*5zx^Bp>Sd4+*d$@&!g>N`@oFNdV|u!K z4-K*{EE#4$A#W!(%_UJ4q@R#P7@*yGWBHNegQOjA=-4~?iXsktErd7s?3$j#-_?GX z*FsEz)2jm0uAHzBx|BpUX_11`&%RxLfbXyZNg1CAvh8ufO~nA=joYWhZiQt)HuY)X zsZwaHM}$Q=K6bi3;`xt_Fz}fg4E~tlLHAEolva@5K^Xxp{z|k=Ubz%nQRLhEc~^Z_ zk8vj2GOB5w)aAr_8I&@90I2;Gk$f%XSI@`i#c-qN;2p9%58XW}kgYq*+4lnQNH+%m z4W@3<93TB~0P~4=D}u3InVlG0?+rY<3?RwzQrB4z96@XU8_)Q--^Qn++5)aZ6UBd<91X(X&9zZP65wts|i2k|javi?^yTibk zZ!q%P?fJ}@Yb&zbb$34F1bgzZ(!L4Zs4RTWBfwAqQXGdqW+l3}EpcFS*Jh2@ui)1us@9C+;it4{y zAja1}@i;V&^(s@WFfPlvTfH@Mv{{C;-5tWPs3Xvl>72RjUf3=TJ`Rt!@wC)}aro9{ zpTb+Kk6QQN-u~lyxRH*{3!6;#7lf3KE<=;4_KrFj>xt(s+4tG?TC(m@nY56e;|2%p zzc|XcWI#3qnlThZOca=yE1X_@nv40e<;YB(O3bC7Uqf%K~u-*{bb z2R+51*Eh~yye-ZT<0DyYdNk|So|A8^Wa3i7l26)fqAXWS=k(xgnq$_BCmCG0$>c5ZMIjG>&5aOIvPWTCN z#fI8)MX{x35tN4>*7dU0eX>=ItHLAK?CjnQubUn`(rhBypuO?-n#66;fEzZMgIzm= zhJugP7skZn2(!-s*Mf8x)2I)dy~hNZNW&~|U6^zWOtaae@`iVRgQzrD zso`zC&fjLtoLnAW@fLR{0&S94{eF6Nn@G3Ad{Ap^_!R}w?$j+!rIQkBH^0v$Y*&b3 z<-%_8dXPn0B`w?7{B&F}Wq$oCWrH$MeJevJ=w3k8&E$(-Nj!mETw=4wjI<#u@CaD> z=NiU2bUSu0K9L`p@OiKmwy$(QRhFUPZ(_1NX21mbA^CE6N7O>7p{gM~euCWxN zv!E{}?`*#{RlD!6Q>pnv^;9t*Zq{!FDeV(*l0Ns#V6Xx8sRL}?WV!%2TAjMsF#{gU zoBt3S;8K{T3~yw_6?zA7$XKrWJrgLKI!c_PEsHW(Qu$aZUnWAr*~wcyytMFp*87Gn z+vj`f42GLMYA4;4vf5@t*p255oO)-iB=5;(^-fspgvZ;O8()p~qeMSL+D>yF4Aske zUS~uvl~tsBCgty}Lyy@JWdpYfjmkC0OjM5y7p;HZjW0>0N97j~ACLOOoc*r-3~@v(eq5cjg>A5!o0D z@Ut5nOhCOWD+az*36b;YH-~1^3Z>W(Hq=0L&fo>W+^i|WWAHCOJ0%4kb=xrzbSSW} zgoSzVrpEi;cF~V?!zix_PT+6(+JTD4vnT~D=3`7pD!Xgq#}5#TG@)>ZH)>*^CDj%*8EhB9XGGGVRc zC0Z8q;Rad)*kL^wBJnL$pb@k3Wcke=s*2>;*Y{K+puDLE@i->kj(u)A=8bf0+GXdB!mv3UoKH7)6P!Jql~D>b=2g zI3h8(61nN#hMBF&$P2E6$@Ve1y{!|O?=(FdlQhYtcU?KD?d+XoFpW7MU}jC8U_Tzh z%?1(lU@l#Bnbg7t#Dj5RDib3pJAge?qa_XZ+;K|wgQ_EjDDFMpHKwbuCKaj&s0sfH zLt_oH%3)4#2p+gdYX|PkmPC?MRew&u9x?6AY?*O>qI2^>M?EheENSi&s)CZj-ZA4u z!A%uLFr5iPr5c(P8qVWYob)rrGd^33_Ja-O>RB`LV3MqRm~+j){segc3QWf%Fmb49 zABO!XKO_zubT;TNH^xP#XkR_FqqI7DFKuev=Y>)s@%)AbSKFt#r)04%K@KK`DGLZ! zFDEA$yjL)@Yq+^hf@<9n5dK+|QwXu@AvaMDZ)#5Xebu@lK3$9&{7_d#ahuB zUpW2(#i6?f{~Cpyf>x~kPFE=SZ+wmVmEQtjAjOw5!;1Hx*fa`67@u*FL~ zKujgS(f+UHRTHrF%Rv7!o+v}Hua2vnLzzpqRK*uWAB3OKP?8Ch-j;g$ z(CM!2wYX|KOjiqHedyWF*%#I;V~;s>Zf1ubfBE8{bx-D;Z!d7>N!qsd+c-XrT5 zSqJyePL98e6ZJS*&tNx|V}Rd6A3&~>af=M{k1x0%hrjk~OzfIBSFjrwnM~LY z+?H@Bz>g03ASweE!N?aPhLktXMYS3)bj&yLSCy5cYOzV!P2WnK6)T1CbpXx#>y*#M%<-sm%vS=#zeC`! zKSoz0DFy}82`!o#d;0bn;6)C_JbuQ*I1_I6<+R?Dk|mMWeMa}!f+cM(^p$v5j?C`; zMw}PZmnnN2Wn($I=QK`{Uf!ywjZ%}pjVZ4?{T|F$)a-9#f5ue6o8I|=B4*YZ5bmB3 z8e1BSUUbe5yw#s=(XSJk?Q+#d5cq^c7~ zm#e;~gCKW>5h|@CC*guXpDi*~L=bp2yx?%fdw)cl#9gh!3UBVJB_Zq=y1#kinJ1Q_c#*5L`!ZXAnh;6;w0*>uGLCa9ofDB(ahO{z z?uM0$ofDJFdi4!gd>M(kP}ApUG7@qUUZpRyZevkwem1vAO$EXSP}_U8u5+bi`1AXkuai-F z5&m2dn<2Cqt5S~(+1%1WVo6V7qDz0cVC0YM^}aS9ZtL^sxSaCkh&GNN95?SU9ZwO! zw%tK|a>gCP{0U)|)bC_EQTQ65Q9;WI_41e|N0O(7hf|L)M%9_Ytx@KC+z;XjPfxTR zVU@DCFz?bsrV{T7kL%Du1hX^sd)X_+eUj~&Co#?!9QGXA=;QA^K*pxM>-?ACS0Bf! z+!6y&L zQ34oh#1cl}=vsv?Hm+_MAZetP0zC)+?~GUEu*8h|=l_VOfZsV!OrLUL7RZshSaCA| zdin0kIsJ}n654Kp8yReI;KI`hG)$~8(W1+b%%_Q&X!0ao7xw>75)(;EOva%gTiDa^f+2Wj{FJ#8eH`8R>T0RG|?|x z&VYz^w%}50Gy0}2$N`?S(Y%s3paoQuycd^pkseprja121aZi|ar}D7ZzWM#h%({30 zFZ9$evND)}^BaZb_Ed+*NT~x--Fi!Rmtta(=XFNat-RpJ+i&gu>hjbdv}N$dZos^U zP1sGhK}pb8V^jGz;2sJBjYfUK*G@F>i9;%wHI&sVhRH9-Hug>=EIL9rO@u-Y0QU{R zl|Qua`2^~JXR#Xx>h(aF`~Wkjc!^_E!@-8SPRpQU7^<_I)`eu2Maa=E%dC_vh_Cj>QkS=ZC%%A&Fi%mVqDk>0ty+Wo+VH`*htD6z;{F zs9rzq%qbV<@ay2p22Xym8@A{Pc7BiDTV??(3G;QH4HjgGln0jb<|Pi6oDCl}ovam! z=AiFhZqrkHmk$4;_>CU`8I|Nmtv)zW@I!`XS)E7dCr z5UEPY{8yAY)w{8tyI%N2dyG26)F6slq zH;nXYwN-wg-ruJ#>CkTb+Pl`Ta5RsKrSILVVEv|xA0_t%|H%L!te(u)E5F5-Cy8$a{BvlGsw|(rG(Svu*p;%U;Ul zzEr3`Xy-M3;PEky;Y%9V3vd4sqgv^j0`R-*Z+(s6KbA$|1r8o6Zsv+5jyE_mOo@97 z#b$HS-*i(iPm+ZOqo2C973VEJx@^Q}BVwJpOQkMfCK+aV7}rinfL43A;2vOI%c?;c z;GR))xf7>i!(g_mJ8jo*|nItFK%yI{~D*rdz^xF;A0WixW$Ui;t5LuQ%96nQzZt@FtF++32}B zhgU^haN@)mo^8}%!V#08HoqJ8?%P-VU~91LdKJstTkY+0f-aCPF8+l4I1fr}V3EFq zE=+&;b`|*2W0)>FY(=k`b$SZE34B1u9Jtu%Kd5|sE`SUS;M8>B#P1IJP+;s`DgXt{ zoGOfFih*EtodIRo+QcnQj5uIYtF%yyj1%7(j~UG+mwXguzUGR}GqD?@Hj@ean=P_} z-gp=usoEetKfmneFO$+lG>XSB^SHicXL=XSX+cHm&Nx-nno?`*PSPxl%D%lDVT;+2 zxL5*MKFs9aK`a9OQA({J<;IgpUI>9URgKFNs35{pl#;ZUsflv^2y+dTQi|xhg;kK@ zxie6TbZH-#qj)baP{l8^2i{OMYe*$Fw)m>rfityv*6S8Brdnw)-^iT4I=SW4!e@_O zJ<%!lQIsD2fH;Eg(yX^P6=mck&pm?Mfz-wqye0>vYaT>)KY@>Zrf5TM?jtd!r*Ohd z;Y8CxCbZN5tnJpT1aP1AgCLrZP?Ts_-2ep!W1kW9Wt2n$u{)Dji zP7HUFozvKRsl17?btbsWV;*W(hUlJCC;AufDOy)=={dbOE?hS0sEmc9X346M;_kN0 zK=O9@eY6Ob9kV^NfZp4**%hlHQnMrdi$g;ib!W?++G63^>mUyQC!z2E?P?+D}yagjO*G3 z<=3p3;x%o75uuL42?xsGWEp+W8+)=Wx7Ns2ii~4Rb^U}~!u^CCRc24)^h&Qs{*hN_RPsm53B{U{$ z2Ge_(wzLwmT3e;Px%b^JtLwJA^0j=s5fHr`oV7@}< z`SxPID6`>kfHWvFL0bQ>>-|6GvDn{eG(t_xn{(;*Gxk);WIv*Aj zUWg=bsd&I&p>vU|t{cc2v{~=aP?SC$f1R>o&)256XHdhH=qZd4a`krHjuYi+Te|~h1gIsG@GNBPBTwABS!a4wf2-)OH{gi0D0^?zL%s$|0|)b@-^AwBSwSXdiMFV zV~O!>$gjv#<5zIs4>rLbBzmsJU* z5E0wwc4A;<8cx;2sdc?^uUE}Cp3d&pq6S}S0#oSctsU-fRGZXL5lh;Wgs2H2)B(Lj z67PA15DSrBzc+{aa#TJ}aYn^Yo{S!Db`jkuOHeaMDboW=ncL`L6l|jf^VDFSQGqF` zlM$S0f2x^gn9;8!thQ@MvmqqeLa_Gs_H%6>b3v<#@o4S=9fSPY&GpDM5o&bgvf;}D z{gWGa#+dOEG*BmNsyO>S?5)Br$8`^4?44MPfRL__RDnW^${9n6Bu~8f_=in}clK%N z^Qbw-Iok!aW)E5h`C-+nXQ_Z57;-2o`q`(vogfNQnmlYhZSN(WHeFz#&wW(y^s2-l z?<>?LdUFv{ud7`S9(}b(htowiKL=T4**jrY(VsY6SK~Dkd*#Bn8;Kfq7oSJE$*f+f zinLwF@FTUF5O{xVAQ(K>9N>D6{T*KSs(D$6BIS?LhhUIY2*L!$3xw)?sh zruZUkwZeQ~h__AfqZ02CC6{Sg)zLU|ERp~4!%q^Hqb|ObcDYXy#~rLV1H(?e((Z!w zc!`{5;d!bwS8VkIbb7~#O_5J*L!`97-yojAst$ZU1L<&6>n{SShwC!^@On&Th%FER z>g}X_8jLJ08EVzcnNqN?jt#LGuD?W6uHDb-c35SoGPU~fd?FqQ0X)gS{h~)$URC}r z&r&bz0K*KL@lILD9B<{ZmiD{)UitNIqI`Dr!ryFM9s3$dXr{e=ZKc55d9?FBY~g89 zltW;{aoxsJMbIMYy1T$MX3!zJ?>76SV#AHptG&ESISbX;qdjY0?O~WkEtFF!eVqi4 z^GoEPU(XmQ)Vlw5!Y^3On9k6s7Zm6=x^`UhRdky&{cu|rC3?!4Fu763>-VxF@AH0- z%06oqS#!Gs7hIshgZN-@wlj3yuoa2XexJAu`;P+J@K1=Pry;cJV9a%1!8;qbIz#Cz ztF_#Nz|4CJzN{@gvAt5sxN_6$%D9jW>zq*hiQ|#cks5kel-6qI9MyEP3w-XkUTS8z ztI{n}3O#F&W)$5*fk2P%S`TLSsRL$m7)I}hQfRz#?3mUWHZM5$;%!Dj!j~`yTjK^% zVS_ISiC`p2aJdKwoI>Z=LNJ>)3^+BJsp1%q+k>oK6YSmC4JZ$$c}N|*DJO!ZYr@$Y z2uGA8Jqg-65f#Ms;6RzDs;({P38GH8zLaG8wooxfebzz_h)4s@0gS~S%Le6KtD$QN z#6z*71BVAA&t8OVa@Y$V8N8QSWceiMGPUA@zh!K`=rmo+@Z3Bruh7r4cyKDPYiTF- zIW!c;v#fLdPX4*#c5j_y;>I(3(pDvo1_&<#KHU@+O zFMbg4Z#>wM-XKJPGZP;ZTnHmm&E)l#P8in+U311Td1_~&$X!HnBdSs<{<+Ga6#j>w zWmWg0;`H8;p^?-3Cv|SW!hYOUW8dd!!H>V{Yc45Qq_ zGTrOvGxd8*o!p#zmzojb{QOZ-MsC5DQ-qUSF@J+|R< zkLaI{PpOz)^7I_4K}e!*0C@0$+r$pEtOuJfvtiKoX~u=A?X{)`j(eSi$_3l*tiKDk z8NwTS@|o47=4Zn3X@=8TBnx<7lD!ql`p)@B8;JZaZiSY~V|XXUX1=B8194B9J~PJ9 zMool2`kDWhJzPcn&Fer-StZSgH3ba)1m-IWq;7vSs(4Dk)LD!OT5MM8SX67}`yZ}6 z3$80Wd652Bg&{8AHv>wee{W|t*TYY`rX6;?oa-ROe~NQ%64n8zC%_B(xK z5ZgPQkq?Ar-)3)#E{m*}a#8XdSYFXPK?ix!HqqYtJQdJPrkT z+Lz=r{(KsNhTjq3qK(9C=MM>GJ`lZ^y5Gmk)C3dACngx+>;Ilt!!u z$y^Yk7iw|!PbZQ?!~+&qKK-31lQBf6TQ@7Z;8PWPz7Y;hG-IMOLn;X$>3>=C|-+XV8zz2|Ef~`JRnE|DU@drW*@*CpRTa zS}@&_ia7nn^D#1_O{?7D@&fl^^MZ;aR~H+{8&vjxy}QkVmEnor>bm5Zi!!Bq7NgEm zweD+dtA0C3{PETGYli77$FTEHw=`4;D+cQIFN03;uzQ=(w&e&yB|PE>z50rK1~U!o zYPH=Cl`Dkon6(PMzV@O{fP@$kr)lET8^%Q`dNEXZ^TqdMqXC)w=V|X-o`%a9o&M}! ze`;oH>>KmewsRlwgIZdB+&U!J+$JpEa~jp#67)|HFLS38=}0w{|BodV6G?9A!P zq<0qc4AOG!TG!HH^@SLosBUJJugp(K;03SRx2aBD2de-bZP1p`u1t)ZTwkPU1jh?B z1r3XA7B%>1CO-MBEBfl}$Z4`q#E{K1bQ`7|*22p-)kdHWlGAc|*IP<%$cB%=tNnam z$AoenLSOWMVPN&%s!JyO1OoJIJw3?Q#WX3CanUErT@iwHQ01%wxJB0apvo=AGqjo3U~<&1I`K(Qm&STE%C^uf9ZRju>8t0Z51n(qD`R1# zZS`ITcb*CpLOBkil|d9D^9^j5*l!*z{pNaws%~qVzvAkdu$vDmr1Gs#Jz1`L7uN#H z7PV+?q}L4T-Arg@;qn}Ww9FQ7qWmQVcYk;ustTJdfUW?1ah(-%88gRea|LXFP;y9) zWE{f`pPs9=QmyGr$Wh#v15~hLKhf?u8Ny3IJE_q)wzIMdp&c5gJpVY*-vpT|7 zl0s)7&Q(nNDFiW*Rz;0&PK<+5USg{1w4a$sevdAgqm<9}Jf7ZKA)x$t7})^RGH3u0 zkrv|?=7SNzX<~DMkDZ!zN4#M_dqwj7gC7)VqmBqR+uD3JHwAz&{Co72)v5r;2CMG8{?^gLIRXQKz zx41YVU*YGN-10O7vi_^g@WG|d2avzMVm9?X5LQeTR&E_HRh&H2;vH=(^~BF}3deo_ z^UIJ!uVex)AJDVm^0}ob@{MyOmN`K8Ed9{q*gJp^kR=3E?ZZ6+;m$Ky8T%dM8WPr&p%+m)_g0Xo z%dEcJ@gna*xq{K_Z9{z8KX63~(ey%UZ3A|zFGGA;5}DoJteM_Zso9HAu}W>|Fb@sn z%z!XitjFN>9!Y7@MW{24<2rqH9wgTJcKuq97*Ed|Pfhl#yuEI6Sgdn5e$&rEZE|8n zda|%ZSzwMWNttNz6tNl9*r`)wCmtx;p>XXf5AK;91?T*X? zQ1mtF3~}c*E!EiO|KPd4x|3mp*||s1hQW5|R`B)@^>pY6YGT8<3g84sdn+a;;?)#X3n43K zRG7Tr2#Nyp?Qmg0DX~2>XBIRa)YZ4?D|M`>sEjh4Q13Z*$zUTWZ9iWLB-jt>y_7@Y zv|<(1bIgx(bmLFz9^9nO5BR;U-+jYe@YUBn&z?S1ahSe3*aDAn5T(OOflQ+*mh;G| zFO$9rb_?ZaT|YNo+RHI-EXc9pGTB9!hPFU?=*?#aWU1>>1<`Oq_!2omJpNdV$In7UAfcq=eE3?3f;xL#)<^M=aF%c zO1{S42mCedA{zRyt(YM!?Em_Bhkqv9`wXL4!vQ&YH)d$i6R_g~Qr7|&2Y+>Irc|(7 zO~Gv$PnCjz=WTQU({8rrUMeZxq{>FJYf9g|5DjNlbzd&@?f*q)@Z z@ef~f_y2@AYrA-#^5}Pc#wQ47>vZj4Uh2h^S4yHKm8XQU`}-0IBpsZI{&%#Rww>rPU4S)D7sYP;kqm|t+F4ib<|Ijb;ym5C? zfW)cbTL!Ieb-FtlMg{-5)X?EKso~>aoUBcKa`d7cWsodKR4!R0koc*BPrR3u4r*LV zdgl~z4;?7u={Qz2`{6K7UeK*K<&m`Wx^A(VV_cg>p+3L)XZ7XIfMAEgigBWnrakau zC5gxXMj%yasypY4__%X>Ix z24Lk;nIWa^Q;FP6!+rr%Q$3>OmgCI0_v&EUg?~mrIbFOUW@|+o|IoK1PqeNb)lQ7d z=8rJI_p6Jc;IJ0uIJQb~aPYem<69F<;!nsbDmF>;7Y;eaq**IoaWJzVz9Tf)$T~Ci z?HovboogkbAGMK{FsNoGclOivTV**+12?2H2^OcG7$e2gucJjtF`!=&#`+v|-^?m~ zCp6rrz9IPJ+LOKeSGAKIeFMCc(zP3}ozk6|1dvECrJ2Oe$}#oT;5{pL6fZt)8a}_` zw(rU`XZaJ!Dx4GXq>alTx&6Nh_$XX$@Qn-U4OoW2QOufI1p+hJ02PBxt0J~w7BAEQ z6y(IG5RlLRCJ#gjFpd;sDb!_R64|u%I-XPKD7gZXJ>1AFT6RCoN!4n{ifCYCfMf`* zFpRRLXHhM>$cO6IZxfknU!G+Qo@X39SQdQW;$Zxi`02P4SuxnmlZnAVS$H9irV88+ zt8mqwpn8xZv8|WKwm6c0yUnb(t zQa&*U*!z1(JwH5jVQ8fva)Z?4^~eY0EOxR}P~4+98_#SlREWWbjWP~1O% z=iX7t$3J`0H_PW#g7T4DN2Tqb9lYilA3klUcU5pcsy^&LXv!Nl|B6MXF^!;H!fdF( zVhHOzKOw#O`9ZkFtY3Y|LHoj%Pk>EqeEk`p)ql85PV!?tUV8oM7w4wH(R%`xz_j#x zVG4R=f2CSn868zQ>9}(B?4fX(7{{~sH3gM@lC8;;S<_?jOZdUX{=#`aKQ9_!)bud} zXbbnPqO|lCXO{$a@3^@?oI_G!(e^NBBK>qBvdEb3NUBv9w{OKv6$Ps&UVIa|9{uo6 z@{o9@=x77vQ!<20Xz1UdeEvJM(BEGp^moE`TJ9T5nZ5EWSB#;lkYCN}{!GAfqArmA zaTG6dqq&>ImI0KP&go*K0<+EY$%_1h>Eg$h?Sq^V$;WY>fcOok4Rm9!m-tLo-(%h( z@dlA?2vmu5J*&5Q&Uk`J(bW(Y)bUrsU1l{k zm-ANa-)~FWliYJHD_7E3_XX!7f#^bur5j8_cP-TOQwc4uI!c&VL#D2dy#>lIPL&_9 zpM4>g6nZQCRH*_@3v?6&5FS@Effpr5^94NY}c|^U8MTZ(<;bAo|U0pJC8$b z_&`|PsEXnvVR%Q7nN#d+z5T$ATyk)iyPQ-|jZbPLi=Me7%|pssP5HuM4{Kkh%W8oN zS8Tl)*l71`PT@hr>Gv1kMZJ7}BPpOlBA3 z#K9d6kH0}`?OfT4s4)+38i4%6Bj{w3TmzH~X=Fm*HU$?Na`W#JwEdKh{W`{}P&GZ` z)eI?4in1RB|pekOEEe91(=Dmd`FQUf1K< z{a&KpCWxudvu5u@N3VVr-nitqpA|q!Wbdq@D3CBwlV!COr^E+%(}*Qo!u?zk)z9?l zaIFr}&$k89awQN{C#j3B*oGsEe=Gl@x82=#dMfAP$lTkwQDZ^B8!x@gbQj9HL45iG z^r=yDXzPIw$F<3)3*=qsj_HJaAC0GR`YU-2QOsd9x!vK@1%}=@^Rn;tKN^a?rPe!C&blpmsZD+!fy0sG&H?dpFg%3 z`*Byep?^}_9b%~3uXkpv)>8PEt}sp_pLZH!?ceoWp3u*0#A?ow-^ScCsT8QW-wydP zhww*#LWubx5~19lI#-J6%9a$#wcgI1J$|Q}E=XT9){@UEth(e2vt@QuwU+d+Gj6j- zCMw2rCb8TL){e~?dGJ8Vp4_B8H3>H9UuEQ9<;Y~P!vCiemrn?~(hT4tM`3CE-hi`g z3*meb8K^nr7O`Y+BB4=gcMnSFXsXaa%8O5axBcwWBG0Sa8TPyw)_%tK0sa@78Z(oX zf|*PQ?e*$+vAZ3(sD%4oDAi5P_@F@%1*aG1WLhtn5#%3-$|;_VW!-$n7fzV0m{@V& zC&!Af!fcuZARbMWD>JhYjS~zBbu3FUmf(Szd=9O^#KGL*VHpAUP~x&#!bRStBlBj& zyd@q73;h$CVpa)@AL<+j>LuyizllKmDI?_Q=4Ijebq66&g)@VTekbwOeNKTFE{@71 z@L$+3Of2M-&R*kSaQ&0!AN8#`EXJ!V3@yXT#klBtG_VDOMSm1^)}G7tG<1%YEc;kG ze683g^bTl)&GAcWth=oDSIi@nF* zrp5D7B~`WrBb=G%?hcXvFkf!$rTMh(2;a8LFGt>w0It0^c|rs=ev}@g`NVz97rT{s z6f1`K-r;v7%^$YZ@&)Q{dRd@`i0qt#MzSS`@@Hao^b1^tS0u~obA2Cflnh@~*)~Mk zQhH7Xq7VmOuD=MGJ57;mW(BnDm^?E2l&zd~+o_54z^%dKbO zyUF1k>rVevF>E754JIo`u(#5WF1I>p4lJ#i9vr=~~mT$&x8iC;m-a6bAHPT?z zkF6>bvuugp%7uGgMKYRYZ4GRB4-z~AXr_Vu&5;(_gI1>xnqW`URVEjr^aQ?q50$%^ zbrq@-Z77~(Yba)6?kFB!1I_FEy~z0gtFQkDXAN{Ao2TR=(UK!}qsXEbA}q@DyF@$B z+g7G^i~>jUtDDz$az5QP>+#nfRD89S&_paxon_I8o2_z-_s;pv{N7vPZOvEl*@1Rj zN&Ghz+acLp?#l`rfOr&+{Kb4kfuQVlCT2FqV^iuG4?+mp+dRIt*YYCu=(_@BP}8}y zJN;gb8+iyApFY58+#r15GFiJpFZJ^r7OE$@V4GTsEkqeoQdz9NqHXU=N^3l1DvVpU zowa(HcEcFLk!*^J|95%Q*6>&z`y3?HIK_%8g)f}lclFkbbz**>{}%<7rtt4)!r`3R z9=3l~!$E>sxFu;y^5!>%U`(t7_nS9QTJE%p-1ih4i)pyH^@&XOF@^4;jj+ECCV&Y2 z6ZyQ&BI~cGO&@5T`=@RDFSj)qNck@zDBM#FXf3?rl`R3v52e6G8!s!&E)V0t$S{%% zD>lQ1oz!Z-3KW{rMcTPcw;-uR13?>(p5G@*&h&`p44XLhCzYQ(%<%ob8)e^Y$Iq4< z4rz03_2YUN6t1ce3w$+VikEAeQkaN8$?XtVxbRxG#9I_#yifDnSwGZvpRmd8zLYl* z?xqF?WlPX;unt(Fj)GtavAszx4;z+Zo`j(i%L-AFuhTYp^;`E<<&)#jjJ3R$^dvJh z>LMOei7hcZuAvtmxm*qTeomJqxbgMqC2*IgPyas^-u+ivAS#AvM>D1eFszWC080^b zbMjNT>GXO~(Kvzj@hJrtQKNd-Jj`-XtWm1l0D`_dJj@K#1f{uGNwgbgdRB#TZQI4N94x;MO|`22=aLMt`lciJCgh zQi+ftJjI_ycVeorp@Ed>CFw$B?D0*bDGz7ohr91EhP<@3rI&Y)CNw~wflkDLvR23x z)H>zJMT3RvoMXb=CU*If{dL~guOn*dyB}ei097|U%8tFj7L0CY;FPCV*&{nvRoa#} zcI|!_wENvQz%evOfPx;Sj4+v#QN0W!SuNIs*xUFq8lc3F9BVIE9<2*k836C+4kjW1^OROn&PW(W6#JGA12 z?dnfVjqCPQPvTc@5-4U^(2=swhzh4+sj`Iygv}*6ql8OQW@ENiryC@1=)%c!2wu!& z8nj}fxgg3Tk$Vi;L_4_S4fW81+0OMy&9luzInVgi2MLq;?4$IK7h^~a&4aFAf+VhK zJwoXhpSe*MMyPwg19EqVYA5t<|3nmcPcTmbWlhbhZh;fi+xW<=ARS#?Wbs#pg5d`d zWwgkxz8~+rdMW+QjYDJIEGEaTUaSXnF@(bLJ(VL)!`reRaPz1rKYt!77|dbwVoN=? zkT|EQNwe1cGCVxeR;KUmEt+sV$xz732m(0xz@4};1(M7_4d%K$=F2#4r$c54^Er7?}byG zj_q1HHuC-QjI~d_%~{claJf0edGk}C`2%3#KOt0RN0XO%=4~c;nQ;obX2hw$UsLvm z_nj|p-|{sjln>%e2zc@QKdpI)fvR7yLcAP%m&=r=`{uZvu9Uq?i*Wb6$PSCKX}1D{ z2gYcN*7}Pz9Xfum9ysY9ER{PXIm}Qo5WMu@Z&>xe^Z#GRQPwEm!%!>}BgZ-D=92^- z_kDbE1%z&26D7?l6OWVDF?ms2<70(uw!W0VZOkPZ8PYY?#Yy*RC*BIe#H{fuxA^ma zkTbEj{c!R{+9@&p$RuMP&SRg|qV({9z0|DnYfANVn@Jo~?iNtvjRiE}m2bZ6i}Tmc zH$#2D?E(rgG&qGrXq|me|a4!^<5cashb>&y%Qowfs0OMU^!H03Dp zd^tW~3O6U9HFj971u)eR;O-BhL5$zz4=#%b$jYDJ$%lkVVv#!SHj=>PPu&zS@tQju z*jb@@xMInU*DxGl5GSaEAMYc|C`p-7ele;y9Zh% zwUcn-bq~(GDv8XI$qwbi+jL|9e7~3V^jO(wp}FjBRLN;r$t;ZrSmb--avl>7zi0`kNirvEEOljs$?X@Cx1P`fbWpx?Q_nhEk@j>Pv7NMoK2gu zMc%a?^}UrI9)VM%`F6MNhecUEG2`2xG+yIq8>w;BxVCcO+vluv8Xsu^C*8q-ZgTOz zv*G_wxMl-KW73nTaFTZ;x2_9mTh`KgtlgFHapOf_92lLZW}tHS#?X*>8Z1-dCeJDY zlsYIxXtfKAyB_dungG&Rvp`FZg)lYfPZ$R1VSrw67b9UUKuoRsJ)%hetNlp(WLiv? z{`LmH&|}j^`d4ucFerKeT45OW(zPBOBC%`lPF{Q~lFr{XIp@V~@>=$E^8dx&o5w@l zw*BL}REi|BWSz>IwPfF>4M~zc#8i?kNp^;*B-uj{)oBE_4&;Cne#l4^Elqe`*0C?xu-?AsL510cc%$m`~tCSSQdI&if6u`O-cHh;@HI7;04$w|Wlo>@C z!@*(w(??gLA7e@~adA_Kh*bJ+iq+etZzKTaTNah}3n6Qfb`j3jw&uFJ)69=gK^3%A z2AA|W)-yl;*O=#Dea+DUzY6TTE9m>YcMRxmB;?0`g8SQOWK6(VhlU$^M@JpX&S>&% zdfC4g6PS*+=|4GyHI@8rN74h6;wH7>!*=a zt@`q=ge9vUTsbKztN8wYd-UjC#*d9{S0p|r&zRlo;Fv-bq8j}XQ*(hO{<;ma=Y=~; zKsphtKseCUCB^$vbtb~k)+MCAi^;;+@j>{<{s?>V=pj(zcC)GWe+V4>cfi8`z3+o< z+6)O#J%!hAM$nV&biPC~LyXCy!)Z?Lk7%~NAncZveS27YVEURoCabzAT7MNt>P5e18$nT6I`=d5eg z0Z!Fi)b+ncun;>6j*J!rGLo+{@-q)|7?7}PA{3U zJc<*}i!Y0)vU*QSHZ6?tx6xgrwabpG>|L9dl{V(%J0qI3`DoPmw8z+$gp#D*4!#=| zvM4<*^)$Yy~8!NbpwBM zS$M&HWSxe}H#I*S`MKbd%kq}?ch}y>jK*-_40O zr{sUFXWj~TrpSkNliJJZ|=Ns;U4wM#%K#Fqq((sq&A-O#1 z9?PZnh;W+mnG-&|j!&Bz-%tE@O=*8bP0zbs&DP)-v@Ybvb?92yN5q;s1W$krtv zNi0-|rum#OId;jWmH*7Zm;9nv!VC@)T->LexWH*A&iIQ(5$COJkt2JRSEQ=i3v&D1 zdzg(j^yZkWSjC;f$JX-#^@?gSZ`EtIoi^ozc?Klz8Y2;%=38ga2}Enc$yo@{uG=m` z^y@lJ(q3lf4Wvjdc5Y!N-437IMe<=;9sysVu08-HB+#_8=O93JR$Ibugq5Si~o^R9uiW{URp==Jr`}1;|ZF3!YHQF7!Kg+H@ z3VX`_B!2HBryNui!U40b;_@821wz4|Ih2ufT|KvXZ?c2=7q#f-&^&SWZS0s#6`2tj zXUXMXzREu{Z2#TgW~)UIzz)EuPalAjPIpijP^Y_q>kr_lPkIsyD3gzekR z^J}7U(MI~6Dd*+G%^<^ErsUg}PIgSCr%d!a1m8rb`r;)PSx8=>hcIxZb_=8=EWlP3 zGJAul1?qa!V`|y}y0z+}MGp-QmyCqwnaaO^mGb49quj*8hp8xFNX9V0kQ@cY!+DMn zO0+gbweojsg!n-hmj|p-j6|P?48b4y38T`SZwvT%|=}T{cRN(>JT8FVp_sNy9_!;kL{WzC@@O{z$~?L zY+k>AJdmO&?(eq^*HDZ%oP~> zpTAzQUv2_VA-ljizU>NjlTHoRxdc}WRrE;~TiMh`%ynh7M32h68qQrRNa5GA5M~hf z7EPPJN!X&W$xJFd$+l0DdA`ZQ<1~J}|4@%`b=nlKu7tPq)b+b@GkOa5j@gCK9e8w? zFWg`=6w^r~3j96?I|)BODLbOR5)F*FD8P^{fE7Pk%?~<#yha~h4%kn*Nl0m+iS~Lx z8E!1bfD_4%z+ati>nER)bGf0QR|<10(L#al(5w5WQ6R0A00B)m4xv}ToicSRGzu)Y zG2`_^Ml_Qxh#Wu-x}FK?1{5cNfy-Dl=Q6%cli|ZTwOfCS5BgVDTh>X;UZW6!w8W z-5>}$>Td`1qc<)g)_VI9q)oSb+(Aml;UdY7;yW6HRQ=pGZ!_N4w@t;v7e2ODipr%% z4(BKPH~&qSh&hN1DuBCLfeMqp34pcx5P!Uz4|3SpPj|(xi1vOKw!W4?I~4MF9hX0R zDg^%>R^mcsg;0t&VKjAZVw^w^`86U-Ie|jS0A-9P1D+XkmX&T15~VUjr{q1utF9f} zZ)lPrSbqGeDk}_$`Q!C4v3XdrXafe+tgx${&!69k!vuTeX&a}`8Ft4jkd@1QZnW}_ zw5DEULo0TEIh!<>lWh25uwt2#>-!zTa)#8>cdeNHU2*ZN*v-F0OL%Y6ys`RF_k zB^1e9v1v7`Pz_czSfjgylX{(NEkrOFx{p;-7f^}|&IEO{`sQ3jXKG1++-ZaW+_Qk> znyJ(Ey-Fk)cZ7VDE*QE+&Dmf(W>fZD>0QK~Pc+a{<~L?Yz_CXUU!Sh z63^;NJ4Qt10g-&VUg#~@(2~wtIOJfJ8tuk$|7_0kPJejQeVTHfKIsnfF>>QF90IWJ z*XX)0`Jr@#pa1wtewVtqypVgz203XO#q(y}Nr*Uy^qw}Nacr2d?*X($=tK;N>AWo8BReq`1|)(ZCF+K9*6{;mamx^EEx&J zX2bGmOn7WQIUPw+7O;>6Hhhx;p%wO_;EzIokeo1`H0v0d=gb%_<1ClIb; zOcd;ZniLi%!c|&0DA@qa(#sWlp+kl@LRJeed35=IetVes2OmHpmJ~-Yssj)-MWdoQ z)I+luml5ZjH+5s&DeQCdO8s6jruGxHb#FzV`qH1{x8;~L3h4i~;5m1FfW}U_3ciov zSDdkyRo~KMj=l7VxqLfS_tkY};k09`p+uQOq+}gJLh!PD2a`crf3Y9QbZUUub6&_u ztNuh>u)#SV_rKBiIS8vbfYyrY;hSlbKj?ODZW1}*e!G2HT6=fv=*tnb1a<$z+O!Q| z$Yfg9)*%xplWpW}&=_I8`LDre1F=ion=-$VB0C2}QfIMxpF_n4b1E;@p4$=%9e>>Kam6VwDn+N{@%>B9 zyv9dyNFKD!4>~T&B~k`ikraESU+!|Q22U|`xL{mQR`(%mUhC<7A%T$5vcqN{Q7Ey$ zM=pON&mEtn&+7exa)l5TlqXm{DENc|ootzlwz)XXCj9a&yj<}RHuXT6#daoEl=4A$ zLZ2Fv0NoKaN8L)+46A{%%j+{yl#vDn=*l81+3_B%QAK4*NvaoFvwt;KGWW&t;5I>f z-qaZ*(#PL2!>TU0pl$Rct@)$s6|Etqc=WRpe<3|rFXpzfuNCfjXBom@-cY^WN|UYs z2-*iqLxFTPH)0gHeb*8^#%bTIIeICqWNwP_x8m|)RbCnTCDsqb(UpM4Gj^Ud(R;#) zQ9=dkOHYBxzli|t8)U)8M$%HG=jrPaeP}^pK(Q@)^(@`dncf{;pThIGPSR_?noINRQE z@hr+!m^~9l2?boJyP(NbdJU|Y80B=QEZW;;>X1&+b$lLq+buU+!)^bvPu5Fcc$)^^ zf&d7$TOc*H3WkE-4?@S-`Omjz0NSN&auYw3^9=dD@q7e$mZyf)ULJ5R?`6DQ82p+Vkrpobd@R)h%#_eB&0Q+~NW+dt{r@c}i%9;xEQd7m{K$QEhy z<9liZ!T~kxG)r0~M8Ksc0m?f-(yu#y`{ib&-V+=3Z8s>14n&Rn4p6^P6fi7RCisL& zcMLw+JIAzlCC?+QLs+jdKk?2<8&BQ*i|AK;{GW|^6JJOKf*dYhHwx6!J%??9fU-AZ z>^zob$Y?Tixt)9*ZeNI(2~IDpK-Q|%SuGCg&B3hp*q&*KN%fpd2`f22EtY?j$Ogtg zA@c20nq>*MrsQ3W73JbKfo+BC~`7Lgr6h&rQ!RmLjGv%PW1cc>IhX9>;= z`}lEkVotFz)VaDsIDZ)FX#i6PMq{Yj-bi2i~ z25O!>tdEExBSqTRc!Sn+_3XQE=rkwmwz28>mgYtkNnoLo*3y0MU7-jrxP#+yq!pes zD&1a#gDZOblf#xpZl&Fq@7^-OkA34_ptR(BDTjgKc?P1bOrk#&7E3$g?~kv(mM*3c zQ%2@GM(58b^R~lht2uO%9+>-(LXg-R1CZ~L_ugcQ*e(@4_P{(lgWvuk%yL&J9@)dFj z>(X0$9S)m94-0s&+%4{_mOi)gLEWg4&^o!g*RoWr*CkVCM=LGx<)O;B^7_b;RP#l! zqkv~?ZkI!Pw>pUltq#PD+y&w@r=q` znuYA4Nc3V!pf@LqR1M>%=;4E&`KHFc9koaitsgc|QWkvbq^tCN;QFQLM4h-nA^7{L zFQ^(&Qo(B)b)~yXUjRAT1?k8PBX=EY{@Cm$d6}>2?uFCVQtkCNmNR zDhl)fHJa)gW~#BE#lF|^O9$y58?s4H%VW(#TuLF+Zx7!K?DLn=^qf3nRNF^ee}(I1su&@!vjJsrs))-naI?Fu-k|whYa=4@k!tu8OZi`t zeGWp4ni}&Vm72vq&C59i7>x9J0tYiB9D1qa=8PbEcT`46+$(*Cu`jHvc#O8>K|e0A1TOPJu*a7VwQ0_)6wC z=OD$Il;q4tD49bZMiq=N&Lp4=ZSh3_K^MY+H(u=W7C=PRqpCtStriEn5pbbFTem3C z)(uG&EJBqy!C{2CMca!cSAeK~X0H1$k;b6o(5fEX&m4#9Cvh*L} zgUz|F6|EmAE795;`0RkylkMao)2emiTkv_la23#?%|DY=**H8HDu=t$el_Y!q!ioY zu-H`{$-?I+b$t%wdZEfqbl5D7^8kM{CY|^}cTvBAg>vjW0Jr$*{7uL=z@T#}z!@kw zYYrZUJE!eud}j?sdX!nqvcnT_95K<<{Dbc0(h?vZqW8H&hejqfg791QUKT+sVd>kn zOFQpFgzJ5ZKz-2NOGeM*9@jlpe zl^=fjw}}V7UJcCqZ?PzbXkAS@A8@GJxdf%Ejl(tZ zb9EJ8BLWE6$;R<{j>YZ_h*FfxCPXPzQVn9%P=q{dFjQ z?Uf-GDC~p8a;O;noE|;xAO`elzn@l2GD+_!%OpN48u#5?e-GbWwXU6nKBFDLb7DwZ z@LiqT!_EuT*o<1IC^GSBMaA#VHhmk~2?AP%g(Ha~PSBl;^6+l*K{y?rs~P%Te`1v) zvcp2e5Mjh-Fzvn4QWTV;arrv5|8inie2QDRYk1NQj0n!-SkO2PfZ6we^?ZNUEbeEY znC+RPw0TZ?I?Ic^XCHHJ&rZr0g*oIW$>$IAD^gCg)WCLFp=$*Q(BF0*I4pa13nMRx z(oAJQXha2PuN&2f?x(6|T31G5~yC*m#opy|2 zLWX((xqSl-5W39`VRrw&U3~n%yB2kZ3N{DmkIULe;j(kWXV#StYlqzJyJPO+B5(O2 zRKf~1%gJ_mn|o->xGd9`d?hqw;^BH&)Je1D=-%g3;Y&;w^6}!2X1t0sMUdImAGbv( zyeLPLYoLb%x;~Oh(38r8N0|#3)#F(e$)~4RH@-d#q;}u5YgWB+9|d(_^7BrAdH_W^ z9NowkWNe^DN5??Na)tb%nwPWDXTW!r49@w!I}vH0i=Mo(xgREEcgQzK+O5Nd zG}p*Oj3u0%td98LAmR7kaZnL6T-NcRi>NeJ;uRjucfvs--}0jnW9B66x8_MiBu$OL z=)?)k+dc#0gD0;9axfer69?7IGAS1b;~_Z?pBg8XY)il3an#&X?9P1M$YRS+MllPr zs_}x+Dm$jL$&>23*GPeRPICYp+yz;zD9dnWv1q`8eLvp3KPuZl7I(d*LKz{ukbeIRH}`c0jt#3{pc}tLburV1 z^1e`~-}zK3kAj4zaDY$s=_BA%b^RrLsz0G=xWSG>0lfxAt5rno2roH88>6z%X5pb1 zYk$!FN~+^p2765i+W;kULU%+|c09k_T%u`l_#-a4&H#errww3it1f;6KmYm`LjUYN ze@1JpqpV%YS3ql2I6zVgi~u&@qwI7A!Lum>W7BH24VM z&Ac4_2U{W+!R}pu5q~Y2nub~=`pQSU@^tvAG~~~{lh*&*To@kv-3*+MOodFV$(Qh6 z;U4qQxha1Bydz}P@Gu*8>^wt3qeXZ|_J!HY8@>{tzRT?ksw<1c&{Sbt<^!@E@#_#` z=YY<8=u@5rxFCV4K0qzf?d=lh@Uzkesg6R0k+6H}oHs04c>PoYFFuPe<&w421jPyL z0pQX8nbT1MJeNa<0Hz@xcDPt~wp`_229`!voJlT?G@l^XFw4<7k}LO6dNO&KTkfkv9t42na~^m%p*Pqfn^ucJd3ABsM==Zog=FRo!xU=VMwWNd3` zSqy(~mi>*P2J$DzIqETVA|12ljwwJ;Bb`BY%CL$5-?eW3q`Fq%pYdTfVL+Ne&kidJ z8pC(dUUuv@hTmHbPQ0TeThNysN>#D?#?Wv4U-VZvW~wErDS&z`5yh*AaLr2hu5gz2 zzu1z(E_g$wSit%^ZtvT%H*;_9e7ORA`uzy{`2+!_Vgfu~9IokfTRM>g?|De-eYB8w zOXmR@9sw;arA4PsAUYo+aF%3{{@pwI!l&L<_?cUU9r->hVQ$6jYd!pHCn-a&$k%hX z7#}%3rlxABz;#Kr*j8*jMjOGnu&$<(eheEq#s4iKxP8d&1iaw7&bjvtbAs%9R(&z` zWM;Q!0dB(01havi$Uy$S(~Zs=Ez*MJnr1q&4Bq=#6VeW)zqu=rTGa&%hJaku3&@cY z#37Pn(~@~n_YCLG^U4R^$*MkI@6$}Z9^5;|`0&79?7$b<|1b}KMq{d0r)DCi7pji| zzB3dwtxT;B7W_fyi+sMiNpB_D{n&{0IpSK7k}Cg| zQWl%1jx*=|{r4x^xcYdA*E+lK>Pkj*v!0jK|3ufXwRN*HWO(=usvZ)=!%(L#vZROK z@M0ag*+mT5*YM#Y+TQvgE{Nm%0LwGD$6Ie=lT#Oq<4ecVZ>=*r!k>jCCiwUw%LUi- z76*#6+tNKy9RuWOz4Mylem)=wyTJo~{D%b7zvH)0GY;%mkp+T2&|xzJ6|XZW?LP@* zGN2K@u`)qOCE$nt3U?FCeg(!Yr*_-M8;L{eUx186%LBmHqT^^M{h({?A(`nFh1QM9 zc#N4YxKpKjw8Ud>$`;MEOlyVk#VGUPB0e2FE&%E-ngH7q!L7k-4Z!y1@aV-P_q!Wh zF`DcJ5LBFtI|CVW?g8Vfp>08k%yQ90hvm(LhMl3lRk(+1pxaag)Tp9Yx|NY#^4_rV z+7x5Zs}yk*pS>E%X{s~31^uY2G0C9$nIReg))!*Y} z`k0zvnV=b?ldBJh-JrhXxQP{CJBB%St3Grq#P+g{kB~>C+`h!`6Jq>28gX+xnV?d- z_iA-a9hA%5$>NP2%LkIj-Tqbf9bP}p_^V{+*=NK3`^hgU?4<5=$~D?xkF%O)Iw`@Z z`)Yt^+Pl$aNv`Xy<=WD$QSsZ_W-rzvgCla^?*w^!1yZcI8d!#Tc&X5O-_9V;l*#1x z)yX#sIAd;7-z^Rj87Kg$kM$qHHMkT1~M`6ko>v)%7^o}Z= zpM9px-C;*Z9K942c$^#DUUPDHCth9PVLKGoG~q9SAo`QyuwzX18v81c^}g!prxcH= zFFLsz-F_z*KaGra`1!FHHf49)-SO`}L9VZMD*Yu$aYu3l@F)ZVPHSMzu%(X2gj2ho zrFM@^TuP?y{hSdAWufAiYTa3P0_<)kiTU4K9a$S``$3mAaS!<&9!+Z{Rt*jgKe8CV z`9=RY^>g(Z%kSR40Ip;^pL_n@LhSdhJVzCezU#S7F!`y1GsO??nRANR!J;2#XVC6Q z%=)Vmdq_gV(5I*kG@~WyXmgTQIzP2D>#cozW^&H4*Plb>9+$lyw0{Q?xnRI!dhgbe zGdn2kR_&6^aPnl|xCEZ}2I)D!pN>M0!&bthq4zOOg<+PD7ruIN_UMThKl&|~yZdQj zmVj~wumkGOd2m(5;FBc%^o5=Ld^TIcEdqagecJm16q4(uFte+q%w#fxv0MR4qMK=Cc(LQ%E?Q>v?@2*+H0_z4?~e}u9Kq2g0Hg5Wzs4@ml&yO;FxJ(|E}swqrqTYwyN(9P4I5=-VLiDj-Hp zmxiBpo@5cN7vMS_He+QvWq2Z#^GI%?v{qTidx)Vcp4ILL>(L@R-TwL zbLdJ)rB|%zSf|2zcsMO(a8?xe1TRzbgKiISx4MQzMrwvdiF_(u5qJDH$v3wIT%VJ# ztwq{;jg)o9u3%L^ckYK?Tgr|qouO``X8Zy~pClse$OzKaIie@%ef6ela0$O~>s{zN za##>6T((Tc!y2JSq>G12UTj#pN8i77D?%MB{e5|$XdwM9w)OS_#_^2uEde}avgAaF* zk*gU6pS(SngQCr5V}4yc7&hlQj)`ciZCARqvnCU2c$6{<)~%H%ZrV7gjlpX==>AN z+5z8E1*nMuJm?5DJ?8$-R2k=GQ(pCI8#PpTM8hHR=Wbn^@(Z8z_`ja%I2v*?`eHgt zYwulFSSqYobC@#tn zgAHLO&Jm>gH&TTOdT+a>?l0%wt4MXqQ9J+eYeANHw(;3B6Z>1Sa(gyyIk3;BL=?A`7LgK|M3KKhrwJljl~z~~lV!LX_Ea+ZA{ z>0iUcLPQM-jUPvKKh6_TA`Pi4n&EX2y3aw6&NZ34CDBm@0S5L9q~ndE9QA6nfCeHa zkRXXsBIRaS+Oup#)<0`?`N&afrdLgu(Xz5U4ptHcTJARKlaICsG)i zk$8QuOOw1_$mKYj0v?*Q8fa^b` z;Su#1kQCHE!*I@;2Vl9b9vY2=l@DK>Xgo`kid1IA_O(i|hfS~$cJyuYv~^S!!$+T` zcEsI9X?lObgft>cl&Ab|0;vUk5It`U@SO+@h%r!k4U$nqM$@xP_+hGs1dX%Ttv-XA z-laP7VXMq4kA!J~PUcr1MSL3sNtq=yOjM}Yj&O~n?|BoBmq~k?Fbq@UMz}=oI#e9SV zteNSi9Q1Cgq!weAvb#ZlLik+L0^Dv&7>@|t9P^nv`yt`B`ue5AOfS;yB#{2)`A#*+ z-_)4#UFNYS`0-$BGD1k|L9c(lj~lmm!|kzP6KmI02_|AjB4z>+mTP2~tmTv^>#o~B zR50SgIVbA1iteYRL(kMQa7^C`K;fW6KA#a_Vi-IiPVM9+SR0X|5A9bwHPM833EJ!m z{kAx+O5h6P7Yglx6nF8o-F8BXj1MRrF9m$ z0wvlm`^+(YDcj&)?WscP<6#Y71RIfz9?)Y>#A*TjY;u;i!-sLpG1Wq?lNHy^t=ecr zj!4;#PU!;^dH&s*n8vK3W;ce!8J(2s`+B?&UE4nGbzhNq^?f^x$W|RjhBWc0XAWsb zMtgAN>i#COp_(Zfqr{sS41ve&msoV1EJg+f@O=w7j`6O)qMLWt5G_7fI^f03pV6Cp z?~KsPsJJ($$YYQ8;H*06e)m!vyo)9IEIMIgTl2@@yJg@|!dHm!z(S#GxkKRs9 zhNum?+{}DYeahRiEu+&=sjwz4 z6~yV^!;;%Azb=C8&7AyS63hJmi#?eq8XVL$1>RfP@QST-zTG)U>B5IWt-OZ0p{}}()4ovapxk-ui-q( z=`NG*w~j#!MqMr0jjj#9Rby&`u?I+cvlv>5uh;W%cSi%{<>2(Xm7dE9?0#J)d#g#M z&RHKrH5BtYoearNo50ejt6zd{R+48rDccQKFDxo~@`QF&=)=l&^`1)_*bG09M;am| z=meC<%0H(8bj@0+bbGTsoEq!$5TLz2-;#0~SFYvQg!Kc}KCy4OrVOl-AYrd-$}DKh zRyF+QO8v`P7a{L&k?D6n@}J{K%b;BDEnyPdKn5l8S@cP)eMoa=7;@LWP5nWqCT1Q? zR+wzXs_q$8D26>jFjp!1mL=)eV;C2_oVFfF7K36Oh?epG%78bF0gEHo``>H@!Ua+} zN-AQUKJ;xyT!0lXs=Ru^nlD^q5Y+lT^sV@DQO75($~N5wniriHn+vun2OV@n z#%zA(51^FQcl5Rwjos}VIbgSIMJ^(`MC-9oO@GK!2JoL-*-l%9Lrs;b* zeB8E?`8amzmiK#~cDg)nQ`UF!cN(TPF-^J<*5MADd|Ee8C?7P%>z{kgtm`RBku;%*dyXfs$a@&7JaEGaX#;CTgKC^!wC4(xYq}n^dEnP zrHp(&{+oX_I4ak-XpAYdB*kV}k$xl7N0He2)WZaqlq_9#+p15!=AECLujajf?EA)S zyh6I<_LTa`c9Wjf6Mb?c-wmgRSBC1FGvzySelJfGDHe~ZM{}vMJ<&LhL+@78DCi2cA?bbaj8)H{GnJ?;BhP*8=sR-9$@BUOTn03td_{;OC z@M>_l)!wLPB|XO#M?r?1V2p4bCLb?Om6BH>q*BM6Q)-soa$*aQ*#EkL>{8d)Ytn?q)zU?8c!%V*I7mZYt zaKfVYEzx+ZuMb!>KHGuUgh#|QwP4cC>Jj)W$o)%YP{rDcV7Q9gZJlOSMI$o+b|uw0-7;`j*|)jTuY$!^a8y4?7ho)0WzZp%!y{Q}!lmAl(m6b}lK) zM6Z-Dy*QG{0)(2-;Xn_!!Yj-}2QH6injCx)AVG?LF8GmkIE z*~jW+;yfuZjiL%3Ty%6PA;iAoL!e)}dD<-jzns?s$I{P_&TR3yo7jBg0Z`skXOh!( zJaSnVU(sw-=46>AC5mNIKAG0!HdmkQJfkyAhdeKQJ^!#zkp#yIHHee`}4IgoTQ6n145i#(r;xpR+X82xohYby7vawk1A!q6?RU zSFBG^VR+hmCxbghOL*VdiM8i&VaMmE&hx z$9f~Ehi!v(%yp=9a}dgXUJ791scZlJ1uXrQ;SPqX8Z`>NP2Ji;zCjQ^xvT?n_$K7< zF9ISBw82ta7!8;pLNExQ-BXZvKR5;PQ3n5b1ch>>@(DhfH~&ym1iK2`l4h@=Q>&;VTs1bJZXK4lrka1S(iA(H_)v$ zbFPM&77P(ZjjKiKqq1mCkTp}N!zx*f4LstDrNwMI(w-{dtnEA0eL!$>wH{Eee4x_Z zu8cmTHeQqf?I_sfj%PZJOO(QXmi9s)yiYkC{DUsOWQz(E&=@Gv`Xn(s>JcRP8;Ktz zMx`8V2n-Z<8B1z6zN>zfWu@MyLsGwtnkpJQ*jomJp+CN5d5oCUpB>PhsDHptE-yBw zN9+x~3s<6Kpv|1<9xcSPP1HcewpkNWk;0xnS*{j@Km5Lp<<0|RHYg)q7s(A^sQEw! z3bJi@)7yHraWERoC2$h$Q?wZhl@=sr_+Ola?js>;eAO)UpBJmE*6-YeJ?hdei5%wG zIY}pR??0{c->vk|zyCylU^^WHF{G?|+hb0pHB_zitYxssYYD$V{MPmhZ8 zaKRvI_1)QBDp~4sLX4=`?0wJ-u!v+A1qAvnX^h$#PJvgeRKzLL@f{^603LVmXv+1| zFIpoiiIfqyH*TSHJC6U%y#L?l$Mvh*=my^Gad97c+cdn!Q1s~+mapkMep^S1Y&wTX;k|*a^1WP|7acoQ>SAzq-RUI8+lqkqwZe~4X zDxKlzr{H$m;Y!4T-uQW7tkf5cG8=Cm1Hw9rrj`HH>QESBZ%RD#jMuZ zb+Rcm8YS77a*lrBvC0*R7C4k_20uFhF)Zt=(qsuNu~L{@F7wq69dd8~{<Xaijyw9-jzB8g*;oKou-5tYP@Z^`@xx( zyEi{7Dc#YRUz{obcx$O5PR2<-GfAny-PSrO-0y?u1`syUw*M=r*~VOh-yBL6xe^R> zIR^;Eg)L*O>}(BWI%fSFkRy_FeQFF-x|a4yeu*0+uiu59iTAXBy^sF0P=B4cp@F|n ztb;-jJ}R=9Mc{jf&m_)}*>sp&Ela3OCx||EH3G0{zz%i#8aiZP#k*FgVv4MII&w<;X;$Hj<41hHprI0gY8)}Q%mv*eQ4ct zn5F{?7h&KCs#~JI1Kt{l2HVsy6f3{E8st&$_Qq7^@+v}SEHxrmYN;U}ma5zylnW^Guh@^Na?;%0Q1^~ViIiKES2J0Czl;79A1O3C*MTx{h^ zJ;_Ylv3rHN?5C%(PhgUWpFU8FYmX5(m)I5Yv9Ykckcj=SV!f90{ev{&psIrjMGON_ zKJfg+))55vpD*=7+%W?!qF(|%kLV6+n*O%!76d$$BndJ#*BoKgWWk?WHi0gf-{Shz z=;KO_6Zd(TuxG>-e(2De)1u@0nH$Oz)9K2h0iAPJJcRt@P{*Y8}MUa`JhP^&_7O4DXjqoLKiS!PXwVjMntNM?TRI zAtZ#U@w)n8Q$)$UH!KH3q-bBi-8=Haq_jXZ>w$kx4xy2jl|fRxS=B97J(O!Xq$ib9 zT}QiHdr)&&Sy^eqEW+?v)_}XE*_X23RaLCP6@hLk=+OtEq%9g3495i$F%X);?5+X$ zWsF2bfy58Gt`@|^eELMp8s*^Woj%0H*8ZDR_bt7C@$T<%*PU7ME_%T)c@z|_!pOaYe>7}qyt%}Q_APc@^GXyM@H+hlEo;kcLi#$ zh-`-d=0O=bU6)cPPNI>noy#Y5;^_+po{`tCDRHbd|8b~&w;ytxiGK9#UEbLDitn$# zIp`<`w9S1t22zsv3V@TMcaDI3S(G9u+|>T#m4jYHVZ`<}3*{gmW|$qsNibI;2gh31 z6Ph&KyI;*VZD=Wgi7XK1>UYR20WxHmnQ-tSe;HRzO_xaZ2h>@Pz|_N6&c=8xuXuE$ zcoxb(kBT6gHVNJg(!wm4*;eBC=&_;@!cPm+LFuimEAHg z2#M|Ne=aO#($OgwZttSnGOkqR9z;P&&=>C*%yP&6+XsRyo!ETi=Y}iLe6YnNSfEFH z2ipiPsqxFJo}rOtnc1sHURkBb#zyY*?iag~Vy9W+H{!3cUMOTB|R$*WaEA&03!533$+g%QG>EQi}};uKqJ44f%OCtkM5i8PI>M3?ES( zpi2rtZ~UM`jhzMMu?qdzU$0D?96Q>Ggj63`0%E6A75L|Q9=@IqW1f$V3>9EfCADshRVH zh&;K8C5zXGM_WZTs9V-x7%5yi-l&FwiU-MQ3lFyk6=m#$?OW*RxP-%wygmH>i8VdJ zi0IWDz)ccuNL8yQD4?fUxJI3Jth4-3dcFf%N~W`&+zipqGxeX&^WJ?W^mIrbSgd2k z$x+;JQ+(O8O&FV#03W_OEyH^Bf}B(I{jD;Bns&R7Ld=D_yV2Kg@#kLiG>h>PI7KiW zyh4~wkqIy9+DmG$O+B1Cb|$Y@AufC2)5T%G2syPH?Z8BC<4<(c`LYT?EPbf~Ll+=o zNl_xWFS)+8YTe7(8Ph7sIqt_^^10ZD`@N<4U4e@umC9v_@4h!Fn+ZViZuQWk0b=&P z)q#_KVJW&bX1>n`eH+U{M2kO_Rz;eb-xq)uUAlLq-Fc?fO`uQq>=ACSRx=Gj?^r8> zR~<`mMI>^J7_+`nuS4buy1$+`1z zm9;Zw0X0j6A)}{Fzh-r3XFV#T28b@$?pk6wujbVqVIWWA0_4vgMUm`Rqc9t~z=&b` zprjdFYH$e69+Y-mM&o$Yk)wcbj}oA!(gXoDaf*QmFdtD3a1uRpSq43&Ar##eDL9U1 zmq;s##8nz)>Wyx4EX!l>i0Lw$^dy^El~{G|m~hyiYQD$5YcRVp5cGTkaCFcg5rj7A zWO_9e(JcccVi4B>86XaD7;aA?lAt6uC1|ld5#jc?^Z0or2_)w(p#h{s1)fhNzzmuB z31nIbf@Zk`PHDb$GRsj4I8-QI z=ojzbI2c>hzV?Hz|MWId&QW?y$)t$wkZV_*>Yrd0b=_pZxTuMH0X z(Jl;Fa9SE5(1-zt{HFn#75(E|o1k^44^h7Yj{w*t9#!lX6`*UBvP~A6vUNRKouW+u z;UEhRKxEh8;K>hQhBUf}P*TFE#r6oL>nzoYf5bEMTJq;b2_$PAK>0lE#D+X$?hnK( zq&R%kR7DGZxBuNo`DF+SN9oPDJC=t{OIJpc1{*H2JaFIqhimrJ*Y)$jqKE>b+v&Rw zO#)df%5Dq5_PYneA6JHdkhP}_y%|G0jt8hFlu?xXFR~ue+&|K=~@fj%!3n+S^JQ9Kr z^oG&c^h>DeI3z))ykgDMCjhqdf!FInz!y)#x50?uG)X;^E6i3~-Ta%5|7ThT#nVoF zlfsNL^pN^izu}|(S$SZKME;gZwf#F#`K*3z^6tDVFE5wdx)wh&TJZN1;W1qVYSbgs3gRBQXYZv?doeM0p+O1Y3DR%5vswxlT(muXF!yn|ideBk29OfX;sr-(= zWp@+ox%KMl)1UrlSe3ZF?D^~K`Letf-_fFcSHaKF8ei2gq2BqGD!(N|k6m7D&Z%Dd z1lkKib;@G2tQISOqhkKE{(mez`(Y&i@3w2Dxy>esAu9jQB3<+JZTy_Sy*?}HhWEWKEiM$_{ z#vNh#OC-F%?!tV*KlQ+QlHbh#1oF9oT~p6*`?u6?{-^p~JkIaD{^|b=A&2wcp1;2C zo|V+c4PR6{SFLnhz3-RuzO}Ayd__0q4ud<5_=79%JiKIT#$7U1^S}B2Q688$1?;y2 zW7G1({y)m~DZlM+0IRI{!tCeypKZ>6T~nTWYpzw^^A+oA+(P4dJ+IDLoO;W1b5z8w z(q#)p-u>xMT!fk3V{Z8H#-y8X&WyUfJ1;w4kjHlCSJe0@9Q2dF%->ZYJ72n@-g1yBLJk8PmAsf(JD)X~{ zyWh44`l|HFn@@lCfAukYcjs@_+s6yn18XXbI>Ap~YklXERfhsMzgL-`thM7-N!mk| zcYFFbte)~w{HQ*6h5wORWsR$|KHkihYCFH_)|Nb<-FFr}XjMpUl$rGtJ?+M%{<#la z*;AA8!M~%%^MS3m&u`YZb97bL^_Plh3f+0H_w;+R^lWv7gz$y#%F~T2I+u#fyA;0P zdD^n1HB&Wq-(Qt}Z%d$S&BiyaM2&(E;I0p-`y}V7f4DdQu{`gqSz>GR^tSBvy5()X zBD(kEuBnOh?g*c)d0+qNFZ-dYy15@#Ki>K(%3C`B!~33f>SiCaJQ?Hl9>i#}UHhbP zLSEHTy=Ws}NUeFC_+$RodO`6=$2)AIAI^TykyUnU-~9aL-1(xBcTRl^I6mov>r;z( zJ$Jk0l$33ZCqfUf%c>vT-}sO1L+siWKc<#0s>sc?J6f~x$6CSC>UbkhAnU%X+3=c&S^kNaaf0nN0^}*Dx^i&M|s7 z<-PRMnaU4Tyyw=cKiGeymN`Uf+VqG1O;UM__#RefK72i68T<9B4VHhh_x}+80Gw`P z*zvI!SaX`ns(Jo#UfJuBS;ZrCJULUbH!R%WbmsO`kE_2(Z*n+os35moCEFyYZ`F$f ztF?KW_yb%E{WkE%f|^%xC{K^+$S|8D{S3hbh% literal 0 HcmV?d00001 diff --git a/hub/imports/energy_systems/montreal_custom_energy_system_parameters.py b/hub/imports/energy_systems/montreal_custom_energy_system_parameters.py index 15833c9b..eb1b9e92 100644 --- a/hub/imports/energy_systems/montreal_custom_energy_system_parameters.py +++ b/hub/imports/energy_systems/montreal_custom_energy_system_parameters.py @@ -136,10 +136,14 @@ class MontrealCustomEnergySystemParameters: _distribution_system.distribution_consumption_variable_flow = \ archetype_distribution_system.distribution_consumption_variable_flow _distribution_system.heat_losses = archetype_distribution_system.heat_losses - _emission_system = None + _generic_emission_system = None if archetype_distribution_system.emission_systems is not None: - _emission_system = EmissionSystem() - _distribution_system.emission_systems = [_emission_system] + _emission_systems = [] + for emission_system in archetype_distribution_system.emission_systems: + _generic_emission_system = EmissionSystem() + _generic_emission_system.parasitic_energy_consumption = emission_system.parasitic_energy_consumption + _emission_systems.append(_generic_emission_system) + _distribution_system.emission_systems = _emission_systems _distribution_systems.append(_distribution_system) return _distribution_systems diff --git a/hub/imports/energy_systems/montreal_future_energy_systems_parameters.py b/hub/imports/energy_systems/montreal_future_energy_systems_parameters.py index 1bcde834..b5628d9f 100644 --- a/hub/imports/energy_systems/montreal_future_energy_systems_parameters.py +++ b/hub/imports/energy_systems/montreal_future_energy_systems_parameters.py @@ -185,10 +185,14 @@ class MontrealFutureEnergySystemParameters: _distribution_system.distribution_consumption_variable_flow = \ archetype_distribution_system.distribution_consumption_variable_flow _distribution_system.heat_losses = archetype_distribution_system.heat_losses - _emission_system = None + _generic_emission_system = None if archetype_distribution_system.emission_systems is not None: - _emission_system = EmissionSystem() - _distribution_system.emission_systems = [_emission_system] + _emission_systems = [] + for emission_system in archetype_distribution_system.emission_systems: + _generic_emission_system = EmissionSystem() + _generic_emission_system.parasitic_energy_consumption = emission_system.parasitic_energy_consumption + _emission_systems.append(_generic_emission_system) + _distribution_system.emission_systems = _emission_systems _distribution_systems.append(_distribution_system) return _distribution_systems diff --git a/input_files/output_buildings_expanded.geojson b/input_files/output_buildings_expanded.geojson new file mode 100644 index 00000000..43fd4d3f --- /dev/null +++ b/input_files/output_buildings_expanded.geojson @@ -0,0 +1,863 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.56769087843276, + 45.49251875903776 + ], + [ + -73.56765050367694, + 45.492560280202284 + ], + [ + -73.5677794213865, + 45.49262188364245 + ], + [ + -73.56781916241786, + 45.49258006136105 + ], + [ + -73.56769087843276, + 45.49251875903776 + ] + ] + ] + }, + "id": 173347, + "properties": { + "name": "01044617", + "address": "rue Victor-Hugo (MTL) 1666", + "function": "1000", + "height": 9, + "year_of_construction": 1986 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.56765050367694, + 45.492560280202284 + ], + [ + -73.56761436875776, + 45.49259744179384 + ], + [ + -73.5676075694645, + 45.49260454199484 + ], + [ + -73.56773226889548, + 45.49266394156485 + ], + [ + -73.56773726906921, + 45.49266624130272 + ], + [ + -73.5677794213865, + 45.49262188364245 + ], + [ + -73.56765050367694, + 45.492560280202284 + ] + ] + ] + }, + "id": 173348, + "properties": { + "name": "01044619", + "address": "rue Victor-Hugo (MTL) 1670", + "function": "1000", + "height": 9, + "year_of_construction": 1986 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.56829026835214, + 45.492524742569145 + ], + [ + -73.56849646900322, + 45.49262354174874 + ], + [ + -73.56861067001111, + 45.492505541343576 + ], + [ + -73.56864076915663, + 45.492519941474434 + ], + [ + -73.56866246900178, + 45.49249754209202 + ], + [ + -73.56867696946317, + 45.49250454136644 + ], + [ + -73.56867726964143, + 45.49250414255471 + ], + [ + -73.56881486931461, + 45.492362042624144 + ], + [ + -73.56881686903772, + 45.492359941181455 + ], + [ + -73.5688004699483, + 45.49235084193039 + ], + [ + -73.56882097012145, + 45.4923320417195 + ], + [ + -73.56879846891101, + 45.49232034109352 + ], + [ + -73.56883736970825, + 45.492284841271946 + ], + [ + -73.56886806888434, + 45.492256240993704 + ], + [ + -73.56885337003277, + 45.49224914198001 + ], + [ + -73.56890226932418, + 45.49219894164121 + ], + [ + -73.56851866897392, + 45.49201434154299 + ], + [ + -73.56837326884313, + 45.492163841620254 + ], + [ + -73.56864696910176, + 45.49229554163243 + ], + [ + -73.5685268682051, + 45.49241904187041 + ], + [ + -73.56825396962694, + 45.49228824183907 + ], + [ + -73.56810906858335, + 45.49243794104013 + ], + [ + -73.56829026835214, + 45.492524742569145 + ] + ] + ] + }, + "id": 173403, + "properties": { + "name": "01044334", + "address": "rue Saint-Jacques (MTL) 1460", + "function": "1000", + "height": 15, + "year_of_construction": 1985 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.5683896684674, + 45.491800342137736 + ], + [ + -73.56838616878639, + 45.49180414157881 + ], + [ + -73.56850686988925, + 45.49185994152571 + ], + [ + -73.56851286844197, + 45.4918626410622 + ], + [ + -73.56855549071014, + 45.49181750806087 + ], + [ + -73.56842962331187, + 45.49175738300567 + ], + [ + -73.5683896684674, + 45.491800342137736 + ] + ] + ] + }, + "id": 174898, + "properties": { + "name": "01044590", + "address": "rue Victor-Hugo (MTL) 1600", + "function": "1000", + "height": 9, + "year_of_construction": 1986 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.5680637695714, + 45.49212884162544 + ], + [ + -73.56802228176146, + 45.49217205619571 + ], + [ + -73.56815668696326, + 45.49223626189717 + ], + [ + -73.56815766959974, + 45.49223524178655 + ], + [ + -73.56818746886172, + 45.49224944155107 + ], + [ + -73.56822816806918, + 45.49220694186927 + ], + [ + -73.5680637695714, + 45.49212884162544 + ] + ] + ] + }, + "id": 175785, + "properties": { + "name": "01044602", + "address": "rue Victor-Hugo (MTL) 1630", + "function": "1000", + "height": 12, + "year_of_construction": 1986 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.56850793693103, + 45.49167318076048 + ], + [ + -73.56846877951091, + 45.4917152818903 + ], + [ + -73.56859506290321, + 45.491775605518725 + ], + [ + -73.56863463503653, + 45.491733702062774 + ], + [ + -73.56850793693103, + 45.49167318076048 + ] + ] + ] + }, + "id": 175910, + "properties": { + "name": "01044586", + "address": "rue Victor-Hugo (MTL) 1590", + "function": "1000", + "height": 9, + "year_of_construction": 1986 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.56817543449134, + 45.49201384773851 + ], + [ + -73.56813497596143, + 45.49205532773507 + ], + [ + -73.56826745951075, + 45.492118613912375 + ], + [ + -73.56830763251781, + 45.49207699906335 + ], + [ + -73.56817543449134, + 45.49201384773851 + ] + ] + ] + }, + "id": 176056, + "properties": { + "name": "01044599", + "address": "rue Victor-Hugo (MTL) 1620", + "function": "1000", + "height": 8, + "year_of_construction": 1986 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.56772876855176, + 45.49247194194522 + ], + [ + -73.56773406949068, + 45.492474341387755 + ], + [ + -73.56773125185198, + 45.492477239659124 + ], + [ + -73.56785890467093, + 45.492538239964624 + ], + [ + -73.56789966910456, + 45.49249534173201 + ], + [ + -73.56776616865103, + 45.49243264153464 + ], + [ + -73.56772876855176, + 45.49247194194522 + ] + ] + ] + }, + "id": 176261, + "properties": { + "name": "01044613", + "address": "rue Victor-Hugo (MTL) 1656", + "function": "1000", + "height": 10, + "year_of_construction": 1986 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.56802228176146, + 45.49217205619571 + ], + [ + -73.56798225825526, + 45.492213743742184 + ], + [ + -73.56811660206223, + 45.49227791893211 + ], + [ + -73.56815668696326, + 45.49223626189717 + ], + [ + -73.56802228176146, + 45.49217205619571 + ] + ] + ] + }, + "id": 176293, + "properties": { + "name": "01044604", + "address": "rue Victor-Hugo (MTL) 1636", + "function": "1000", + "height": 12, + "year_of_construction": 1986 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.56790222258577, + 45.49229712328457 + ], + [ + -73.56785996900595, + 45.49234104192853 + ], + [ + -73.56799446861396, + 45.49240484193282 + ], + [ + -73.56803643080562, + 45.49236123475947 + ], + [ + -73.56790222258577, + 45.49229712328457 + ] + ] + ] + }, + "id": 176296, + "properties": { + "name": "01044611", + "address": "rue Victor-Hugo (MTL) 1650", + "function": "1000", + "height": 10, + "year_of_construction": 1986 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.56798225825526, + 45.492213743742184 + ], + [ + -73.56794223597048, + 45.4922554321734 + ], + [ + -73.56807651582375, + 45.49231957685336 + ], + [ + -73.56811660206223, + 45.49227791893211 + ], + [ + -73.56798225825526, + 45.492213743742184 + ] + ] + ] + }, + "id": 176298, + "properties": { + "name": "01044607", + "address": "rue Victor-Hugo (MTL) 1640", + "function": "1000", + "height": 12, + "year_of_construction": 1986 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.56742736898599, + 45.49184704208998 + ], + [ + -73.56761256873325, + 45.491896142437554 + ], + [ + -73.56766926915839, + 45.4917902412014 + ], + [ + -73.56766956853903, + 45.49179024192391 + ], + [ + -73.56792966911675, + 45.49183254222432 + ], + [ + -73.56793006788594, + 45.491831141828406 + ], + [ + -73.56794526884076, + 45.49174634219527 + ], + [ + -73.56794516904765, + 45.49174634225465 + ], + [ + -73.56753896905731, + 45.491638642248425 + ], + [ + -73.56742736898599, + 45.49184704208998 + ] + ] + ] + }, + "id": 176918, + "properties": { + "name": "01097185", + "address": "rue Victor-Hugo (MTL) 1591", + "function": "1000", + "height": 10, + "year_of_construction": 1987 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.56773125185198, + 45.492477239659124 + ], + [ + -73.56769087843276, + 45.49251875903776 + ], + [ + -73.56781916241786, + 45.49258006136105 + ], + [ + -73.56785890467093, + 45.492538239964624 + ], + [ + -73.56773125185198, + 45.492477239659124 + ] + ] + ] + }, + "id": 178164, + "properties": { + "name": "01044615", + "address": "rue Victor-Hugo (MTL) 1660", + "function": "1000", + "height": 9, + "year_of_construction": 1986 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.56846877951091, + 45.4917152818903 + ], + [ + -73.56842962331187, + 45.49175738300567 + ], + [ + -73.56855549071014, + 45.49181750806087 + ], + [ + -73.56859506290321, + 45.491775605518725 + ], + [ + -73.56846877951091, + 45.4917152818903 + ] + ] + ] + }, + "id": 179679, + "properties": { + "name": "01044588", + "address": "rue Victor-Hugo (MTL) 1596", + "function": "1000", + "height": 9, + "year_of_construction": 1986 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.56825635009473, + 45.49193088860213 + ], + [ + -73.56821589168355, + 45.491972368627906 + ], + [ + -73.5683477837006, + 45.4920353716151 + ], + [ + -73.56838787594006, + 45.49199371809223 + ], + [ + -73.56825635009473, + 45.49193088860213 + ] + ] + ] + }, + "id": 179789, + "properties": { + "name": "01044595", + "address": "rue Victor-Hugo (MTL) 1610", + "function": "1000", + "height": 8, + "year_of_construction": 1986 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.56821589168355, + 45.491972368627906 + ], + [ + -73.56817543449134, + 45.49201384773851 + ], + [ + -73.56830763251781, + 45.49207699906335 + ], + [ + -73.5683477837006, + 45.4920353716151 + ], + [ + -73.56821589168355, + 45.491972368627906 + ] + ] + ] + }, + "id": 181310, + "properties": { + "name": "01044597", + "address": "rue Victor-Hugo (MTL) 1616", + "function": "1000", + "height": 8, + "year_of_construction": 1986 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.56809506939487, + 45.49209624228538 + ], + [ + -73.56809246893268, + 45.4920988416879 + ], + [ + -73.56821287000538, + 45.49216124158406 + ], + [ + -73.56822186852654, + 45.49216584161625 + ], + [ + -73.56826745951075, + 45.492118613912375 + ], + [ + -73.56813497596143, + 45.49205532773507 + ], + [ + -73.56809506939487, + 45.49209624228538 + ] + ] + ] + }, + "id": 182393, + "properties": { + "name": "01044601", + "address": "rue Victor-Hugo (MTL) 1626", + "function": "1000", + "height": 8, + "year_of_construction": 1986 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.56790756893894, + 45.492291541967774 + ], + [ + -73.56790222258577, + 45.49229712328457 + ], + [ + -73.56803643080562, + 45.49236123475947 + ], + [ + -73.56807651582375, + 45.49231957685336 + ], + [ + -73.56794223597048, + 45.4922554321734 + ], + [ + -73.56790756893894, + 45.492291541967774 + ] + ] + ] + }, + "id": 182442, + "properties": { + "name": "01044609", + "address": "rue Victor-Hugo (MTL) 1646", + "function": "1000", + "height": 11, + "year_of_construction": 1986 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.56829706912258, + 45.49188914205178 + ], + [ + -73.56825635009473, + 45.49193088860213 + ], + [ + -73.56838787594006, + 45.49199371809223 + ], + [ + -73.56842846901456, + 45.49195154234486 + ], + [ + -73.56829706912258, + 45.49188914205178 + ] + ] + ] + }, + "id": 182546, + "properties": { + "name": "01044592", + "address": "rue Victor-Hugo (MTL) 1606", + "function": "1000", + "height": 8, + "year_of_construction": 1986 + } + } + ] +} \ No newline at end of file diff --git a/report_test.py b/report_test.py index 638b9213..aa67926b 100644 --- a/report_test.py +++ b/report_test.py @@ -12,17 +12,22 @@ from scripts.geojson_creator import process_geojson from scripts import random_assignation from hub.imports.energy_systems_factory import EnergySystemsFactory from scripts.energy_system_sizing import SystemSizing -from scripts.energy_system_retrofit_results import system_results, new_system_results +from scripts.solar_angles import CitySolarAngles +from scripts.pv_sizing_and_simulation import PVSizingSimulation +from scripts.energy_system_retrofit_results import consumption_data from scripts.energy_system_sizing_and_simulation_factory import EnergySystemsSimulationFactory from scripts.costs.cost import Cost from scripts.costs.constants import SKIN_RETROFIT_AND_SYSTEM_RETROFIT_AND_PV, SYSTEM_RETROFIT_AND_PV import hub.helpers.constants as cte from hub.exports.exports_factory import ExportsFactory +from scripts.pv_feasibility import pv_feasibility geojson_file = process_geojson(x=-73.5681295982132, y=45.49218262677643, diff=0.0001) file_path = (Path(__file__).parent / 'input_files' / 'output_buildings.geojson') output_path = (Path(__file__).parent / 'out_files').resolve() -city = GeometryFactory('geojson', +simulation_results_path = (Path(__file__).parent / 'out_files' / 'simulation_results').resolve() +simulation_results_path.mkdir(parents=True, exist_ok=True) +city = GeometryFactory(file_type='geojson', path=file_path, height_field='height', year_of_construction_field='year_of_construction', @@ -35,7 +40,32 @@ ExportsFactory('sra', city, output_path).export() sra_path = (output_path / f'{city.name}_sra.xml').resolve() subprocess.run(['sra', str(sra_path)]) ResultFactory('sra', city, output_path).enrich() +pv_feasibility(-73.5681295982132, 45.49218262677643, 0.0001, selected_buildings=city.buildings) energy_plus_workflow(city) +solar_angles = CitySolarAngles(city.name, + city.latitude, + city.longitude, + tilt_angle=45, + surface_azimuth_angle=180).calculate +random_assignation.call_random(city.buildings, random_assignation.residential_systems_percentage) +EnergySystemsFactory('montreal_custom', city).enrich() +SystemSizing(city.buildings).montreal_custom() +current_status_energy_consumption = consumption_data(city) +random_assignation.call_random(city.buildings, random_assignation.residential_new_systems_percentage) +EnergySystemsFactory('montreal_future', city).enrich() +for building in city.buildings: + if 'PV' in building.energy_systems_archetype_name: + ghi = [x / cte.WATTS_HOUR_TO_JULES for x in building.roofs[0].global_irradiance[cte.HOUR]] + pv_sizing_simulation = PVSizingSimulation(building, + solar_angles, + tilt_angle=45, + module_height=1, + module_width=2, + ghi=ghi) + pv_sizing_simulation.pv_output() + if building.energy_systems_archetype_name == 'PV+4Pipe+DHW': + EnergySystemsSimulationFactory('archetype13', building=building, output_path=simulation_results_path).enrich() +retrofitted_energy_consumption = consumption_data(city) +(EnergySystemRetrofitReport(city, output_path, 'PV Implementation and System Retrofit', + current_status_energy_consumption, retrofitted_energy_consumption).create_report()) -(EnergySystemRetrofitReport(city, output_path, 'PV Implementation and HVAC Retrofit'). - create_report(current_system=None, new_system=None)) diff --git a/scripts/energy_system_retrofit_report.py b/scripts/energy_system_retrofit_report.py index 6209c5bc..60e82ece 100644 --- a/scripts/energy_system_retrofit_report.py +++ b/scripts/energy_system_retrofit_report.py @@ -9,24 +9,36 @@ import matplotlib as mpl from matplotlib.ticker import MaxNLocator import numpy as np from pathlib import Path +import glob + class EnergySystemRetrofitReport: - def __init__(self, city, output_path, retrofit_scenario): + def __init__(self, city, output_path, retrofit_scenario, current_status_energy_consumption_data, + retrofitted_energy_consumption_data): self.city = city + self.current_status_data = current_status_energy_consumption_data + self.retrofitted_data = retrofitted_energy_consumption_data self.output_path = output_path self.content = [] + self.retrofit_scenario = retrofit_scenario self.report = LatexReport('energy_system_retrofit_report', - 'Energy System Retrofit Report', retrofit_scenario, output_path) + 'Energy System Retrofit Report', self.retrofit_scenario, output_path) + self.system_schemas_path = (Path(__file__).parent.parent / 'hub' / 'data' / 'energy_systems' / 'schemas') self.charts_path = Path(output_path) / 'charts' self.charts_path.mkdir(parents=True, exist_ok=True) + files = glob.glob(f'{self.charts_path}/*') + for file in files: + os.remove(file) def building_energy_info(self): table_data = [ ["Building Name", "Year of Construction", "function", "Yearly Heating Demand (MWh)", "Yearly Cooling Demand (MWh)", "Yearly DHW Demand (MWh)", "Yearly Electricity Demand (MWh)"] ] - intensity_table_data = [["Building Name", "Total Floor Area m2", "Heating Demand Intensity kWh/m2", - "Cooling Demand Intensity kWh/m2", "Electricity Intensity kWh/m2"]] + intensity_table_data = [["Building Name", "Total Floor Area $m^2$", "Heating Demand Intensity kWh/ $m^2$", + "Cooling Demand Intensity kWh/ $m^2$", "Electricity Intensity kWh/ $m^2$"]] + peak_load_data = [["Building Name", "Heating Peak Load (kW)", "Cooling Peak Load (kW)", + "Domestic Hot Water Peak Load (kW)"]] for building in self.city.buildings: total_floor_area = 0 @@ -52,128 +64,68 @@ class EnergySystemRetrofitReport: (building.lighting_electrical_demand[cte.YEAR][0] + building.appliances_electrical_demand[cte.YEAR][0]) / (3.6e6 * total_floor_area), '.2f')) ] + peak_data = [ + building.name, + str(format(building.heating_peak_load[cte.YEAR][0] / 1000, '.2f')), + str(format(building.cooling_peak_load[cte.YEAR][0] / 1000, '.2f')), + str(format( + (building.lighting_electrical_demand[cte.YEAR][0] + building.appliances_electrical_demand[cte.YEAR][0]) / + (3.6e6 * total_floor_area), '.2f')) + ] table_data.append(building_data) intensity_table_data.append(intensity_data) + peak_load_data.append(peak_data) self.report.add_table(table_data, caption='Buildings Energy Consumption Data') self.report.add_table(intensity_table_data, caption='Buildings Energy Use Intensity Data') + self.report.add_table(peak_load_data, caption='Buildings Peak Load Data') - def monthly_demands(self): - heating = [] - cooling = [] - dhw = [] - lighting_appliance = [] - for i in range(12): - heating_demand = 0 - cooling_demand = 0 - dhw_demand = 0 - lighting_appliance_demand = 0 - for building in self.city.buildings: - heating_demand += building.heating_demand[cte.MONTH][i] / 3.6e6 - cooling_demand += building.cooling_demand[cte.MONTH][i] / 3.6e6 - dhw_demand += building.domestic_hot_water_heat_demand[cte.MONTH][i] / 3.6e6 - lighting_appliance_demand += building.lighting_electrical_demand[cte.MONTH][i] / 3.6e6 - heating.append(heating_demand) - cooling.append(cooling_demand) - dhw.append(dhw_demand) - lighting_appliance.append(lighting_appliance_demand) - - monthly_demands = {'heating': heating, - 'cooling': cooling, - 'dhw': dhw, - 'lighting_appliance': lighting_appliance} - return monthly_demands - - def plot_monthly_energy_demands(self, demands, file_name): + def plot_monthly_energy_demands(self, data, file_name, title): # Data preparation months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] - heating = demands['heating'] - cooling = demands['cooling'] - dhw = demands['dhw'] - electricity = demands['lighting_appliance'] + demands = { + 'Heating': ('heating', '#2196f3'), + 'Cooling': ('cooling', '#ff5a5f'), + 'DHW': ('dhw', '#4caf50'), + 'Electricity': ('lighting_appliance', '#ffc107') + } + + # Helper function for plotting + def plot_bar_chart(ax, demand_type, color, ylabel, title): + values = data[demand_type] + ax.bar(months, values, color=color, width=0.6, zorder=2) + ax.grid(which="major", axis='x', color='#DAD8D7', alpha=0.5, zorder=1) + ax.grid(which="major", axis='y', color='#DAD8D7', alpha=0.5, zorder=1) + ax.set_xlabel('Month', fontsize=12, labelpad=10) + ax.set_ylabel(ylabel, fontsize=14, labelpad=10) + ax.set_title(title, fontsize=14, weight='bold', alpha=.8, pad=40) + ax.xaxis.set_major_locator(MaxNLocator(integer=True)) + ax.yaxis.set_major_locator(MaxNLocator(integer=True)) + ax.set_xticks(np.arange(len(months))) + ax.set_xticklabels(months, rotation=45, ha='right') + ax.bar_label(ax.containers[0], padding=3, color='black', fontsize=12, rotation=90) + ax.spines[['top', 'left', 'bottom']].set_visible(False) + ax.spines['right'].set_linewidth(1.1) + average_value = np.mean(values) + ax.axhline(y=average_value, color='grey', linewidth=2, linestyle='--') + ax.text(len(months) - 1, average_value, f'Average = {average_value:.1f} kWh', ha='right', va='bottom', + color='grey') # Plotting - fig, axs = plt.subplots(2, 2, figsize=(15, 10), dpi=96) - fig.suptitle('Monthly Energy Demands', fontsize=16, weight='bold', alpha=.8) + fig, axs = plt.subplots(4, 1, figsize=(20, 16), dpi=96) + fig.suptitle(title, fontsize=16, weight='bold', alpha=.8) - # Heating bar chart - axs[0, 0].bar(months, heating, color='#2196f3', width=0.6, zorder=2) - axs[0, 0].grid(which="major", axis='x', color='#DAD8D7', alpha=0.5, zorder=1) - axs[0, 0].grid(which="major", axis='y', color='#DAD8D7', alpha=0.5, zorder=1) - axs[0, 0].set_xlabel('Month', fontsize=12, labelpad=10) - axs[0, 0].set_ylabel('Heating Demand (kWh)', fontsize=12, labelpad=10) - axs[0, 0].set_title('Monthly Heating Demands', fontsize=14, weight='bold', alpha=.8) - axs[0, 0].xaxis.set_major_locator(MaxNLocator(integer=True)) - axs[0, 0].yaxis.set_major_locator(MaxNLocator(integer=True)) - axs[0, 0].set_xticks(np.arange(len(months))) - axs[0, 0].set_xticklabels(months, rotation=45, ha='right') - axs[0, 0].bar_label(axs[0, 0].containers[0], padding=3, color='black', fontsize=8) - axs[0, 0].spines[['top', 'left', 'bottom']].set_visible(False) - axs[0, 0].spines['right'].set_linewidth(1.1) - average_heating = np.mean(heating) - axs[0, 0].axhline(y=average_heating, color='grey', linewidth=2, linestyle='--') - axs[0, 0].text(len(months)-1, average_heating, f'Average = {average_heating:.1f} kWh', ha='right', va='bottom', color='grey') - - # Cooling bar chart - axs[0, 1].bar(months, cooling, color='#ff5a5f', width=0.6, zorder=2) - axs[0, 1].grid(which="major", axis='x', color='#DAD8D7', alpha=0.5, zorder=1) - axs[0, 1].grid(which="major", axis='y', color='#DAD8D7', alpha=0.5, zorder=1) - axs[0, 1].set_xlabel('Month', fontsize=12, labelpad=10) - axs[0, 1].set_ylabel('Cooling Demand (kWh)', fontsize=12, labelpad=10) - axs[0, 1].set_title('Monthly Cooling Demands', fontsize=14, weight='bold', alpha=.8) - axs[0, 1].xaxis.set_major_locator(MaxNLocator(integer=True)) - axs[0, 1].yaxis.set_major_locator(MaxNLocator(integer=True)) - axs[0, 1].set_xticks(np.arange(len(months))) - axs[0, 1].set_xticklabels(months, rotation=45, ha='right') - axs[0, 1].bar_label(axs[0, 1].containers[0], padding=3, color='black', fontsize=8) - axs[0, 1].spines[['top', 'left', 'bottom']].set_visible(False) - axs[0, 1].spines['right'].set_linewidth(1.1) - average_cooling = np.mean(cooling) - axs[0, 1].axhline(y=average_cooling, color='grey', linewidth=2, linestyle='--') - axs[0, 1].text(len(months)-1, average_cooling, f'Average = {average_cooling:.1f} kWh', ha='right', va='bottom', color='grey') - - # DHW bar chart - axs[1, 0].bar(months, dhw, color='#4caf50', width=0.6, zorder=2) - axs[1, 0].grid(which="major", axis='x', color='#DAD8D7', alpha=0.5, zorder=1) - axs[1, 0].grid(which="major", axis='y', color='#DAD8D7', alpha=0.5, zorder=1) - axs[1, 0].set_xlabel('Month', fontsize=12, labelpad=10) - axs[1, 0].set_ylabel('DHW Demand (kWh)', fontsize=12, labelpad=10) - axs[1, 0].set_title('Monthly DHW Demands', fontsize=14, weight='bold', alpha=.8) - axs[1, 0].xaxis.set_major_locator(MaxNLocator(integer=True)) - axs[1, 0].yaxis.set_major_locator(MaxNLocator(integer=True)) - axs[1, 0].set_xticks(np.arange(len(months))) - axs[1, 0].set_xticklabels(months, rotation=45, ha='right') - axs[1, 0].bar_label(axs[1, 0].containers[0], padding=3, color='black', fontsize=8) - axs[1, 0].spines[['top', 'left', 'bottom']].set_visible(False) - axs[1, 0].spines['right'].set_linewidth(1.1) - average_dhw = np.mean(dhw) - axs[1, 0].axhline(y=average_dhw, color='grey', linewidth=2, linestyle='--') - axs[1, 0].text(len(months)-1, average_dhw, f'Average = {average_dhw:.1f} kWh', ha='right', va='bottom', color='grey') - - # Electricity bar chart - axs[1, 1].bar(months, electricity, color='#ffc107', width=0.6, zorder=2) - axs[1, 1].grid(which="major", axis='x', color='#DAD8D7', alpha=0.5, zorder=1) - axs[1, 1].grid(which="major", axis='y', color='#DAD8D7', alpha=0.5, zorder=1) - axs[1, 1].set_xlabel('Month', fontsize=12, labelpad=10) - axs[1, 1].set_ylabel('Electricity Demand (kWh)', fontsize=12, labelpad=10) - axs[1, 1].set_title('Monthly Electricity Demands', fontsize=14, weight='bold', alpha=.8) - axs[1, 1].xaxis.set_major_locator(MaxNLocator(integer=True)) - axs[1, 1].yaxis.set_major_locator(MaxNLocator(integer=True)) - axs[1, 1].set_xticks(np.arange(len(months))) - axs[1, 1].set_xticklabels(months, rotation=45, ha='right') - axs[1, 1].bar_label(axs[1, 1].containers[0], padding=3, color='black', fontsize=8) - axs[1, 1].spines[['top', 'left', 'bottom']].set_visible(False) - axs[1, 1].spines['right'].set_linewidth(1.1) - average_electricity = np.mean(electricity) - axs[1, 1].axhline(y=average_electricity, color='grey', linewidth=2, linestyle='--') - axs[1, 1].text(len(months)-1, average_electricity * 0.95, f'Average = {average_electricity:.1f} kWh', ha='right', va='top', color='grey') + plot_bar_chart(axs[0], 'heating', demands['Heating'][1], 'Heating Demand (kWh)', 'Monthly Heating Demand') + plot_bar_chart(axs[1], 'cooling', demands['Cooling'][1], 'Cooling Demand (kWh)', 'Monthly Cooling Demand') + plot_bar_chart(axs[2], 'dhw', demands['DHW'][1], 'DHW Demand (kWh)', 'Monthly DHW Demand') + plot_bar_chart(axs[3], 'lighting_appliance', demands['Electricity'][1], 'Electricity Demand (kWh)', + 'Monthly Electricity Demand') # Set a white background fig.patch.set_facecolor('white') # Adjust the margins around the plot area - plt.subplots_adjust(left=0.05, right=0.95, top=0.9, bottom=0.1, wspace=0.3, hspace=0.5) - + plt.subplots_adjust(left=0.05, right=0.95, top=0.9, bottom=0.1, hspace=0.5) # Save the plot chart_path = self.charts_path / f'{file_name}.png' @@ -182,15 +134,360 @@ class EnergySystemRetrofitReport: return chart_path - def create_report(self, current_system, new_system): - os.chdir(self.charts_path) - self.report.add_section('Current Status') - self.report.add_subsection('City Buildings Characteristics') + def plot_monthly_energy_consumption(self, data, file_name, title): + # Data preparation + months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] + consumptions = { + 'Heating': ('heating', '#2196f3', 'Heating Consumption (kWh)', 'Monthly Energy Consumption for Heating'), + 'Cooling': ('cooling', '#ff5a5f', 'Cooling Consumption (kWh)', 'Monthly Energy Consumption for Cooling'), + 'DHW': ('dhw', '#4caf50', 'DHW Consumption (kWh)', 'Monthly DHW Consumption') + } + + # Helper function for plotting + def plot_bar_chart(ax, consumption_type, color, ylabel, title): + values = data[consumption_type] + ax.bar(months, values, color=color, width=0.6, zorder=2) + ax.grid(which="major", axis='x', color='#DAD8D7', alpha=0.5, zorder=1) + ax.grid(which="major", axis='y', color='#DAD8D7', alpha=0.5, zorder=1) + ax.set_xlabel('Month', fontsize=12, labelpad=10) + ax.set_ylabel(ylabel, fontsize=14, labelpad=10) + ax.set_title(title, fontsize=14, weight='bold', alpha=.8, pad=40) + ax.xaxis.set_major_locator(MaxNLocator(integer=True)) + ax.yaxis.set_major_locator(MaxNLocator(integer=True)) + ax.set_xticks(np.arange(len(months))) + ax.set_xticklabels(months, rotation=45, ha='right') + ax.bar_label(ax.containers[0], padding=3, color='black', fontsize=12, rotation=90) + ax.spines[['top', 'left', 'bottom']].set_visible(False) + ax.spines['right'].set_linewidth(1.1) + average_value = np.mean(values) + ax.axhline(y=average_value, color='grey', linewidth=2, linestyle='--') + ax.text(len(months) - 1, average_value, f'Average = {average_value:.1f} kWh', ha='right', va='bottom', + color='grey') + + # Plotting + fig, axs = plt.subplots(3, 1, figsize=(20, 15), dpi=96) + fig.suptitle(title, fontsize=16, weight='bold', alpha=.8) + + plot_bar_chart(axs[0], 'heating', consumptions['Heating'][1], consumptions['Heating'][2], + consumptions['Heating'][3]) + plot_bar_chart(axs[1], 'cooling', consumptions['Cooling'][1], consumptions['Cooling'][2], + consumptions['Cooling'][3]) + plot_bar_chart(axs[2], 'dhw', consumptions['DHW'][1], consumptions['DHW'][2], consumptions['DHW'][3]) + + # Set a white background + fig.patch.set_facecolor('white') + + # Adjust the margins around the plot area + plt.subplots_adjust(left=0.05, right=0.95, top=0.9, bottom=0.1, wspace=0.3, hspace=0.5) + + # Save the plot + chart_path = self.charts_path / f'{file_name}.png' + plt.savefig(chart_path, bbox_inches='tight') + plt.close() + + return chart_path + + def fuel_consumption_breakdown(self, file_name, data): + fuel_consumption_breakdown = {} + for building in self.city.buildings: + for key, breakdown in data[f'{building.name}']['energy_consumption_breakdown'].items(): + if key not in fuel_consumption_breakdown: + fuel_consumption_breakdown[key] = {sector: 0 for sector in breakdown} + for sector, value in breakdown.items(): + if sector in fuel_consumption_breakdown[key]: + fuel_consumption_breakdown[key][sector] += value / 3.6e6 + else: + fuel_consumption_breakdown[key][sector] = value / 3.6e6 + + plt.style.use('ggplot') + num_keys = len(fuel_consumption_breakdown) + fig, axs = plt.subplots(1 if num_keys <= 2 else num_keys, min(num_keys, 2), figsize=(12, 5)) + axs = axs if num_keys > 1 else [axs] # Ensure axs is always iterable + + for i, (fuel, breakdown) in enumerate(fuel_consumption_breakdown.items()): + labels = breakdown.keys() + values = breakdown.values() + colors = cm.get_cmap('tab20c', len(labels)) + ax = axs[i] if num_keys > 1 else axs[0] + ax.pie(values, labels=labels, + autopct=lambda pct: f"{pct:.1f}%\n({pct / 100 * sum(values):.2f})", + startangle=90, colors=[colors(j) for j in range(len(labels))]) + ax.set_title(f'{fuel} Consumption Breakdown') + + plt.suptitle('City Energy Consumption Breakdown', fontsize=16, fontweight='bold') + plt.tight_layout(rect=[0, 0, 1, 0.95]) # Adjust layout to fit the suptitle + + chart_path = self.charts_path / f'{file_name}.png' + plt.savefig(chart_path, dpi=300) + plt.close() + return chart_path + + def energy_system_archetype_schematic(self): + energy_system_archetypes = {} + for building in self.city.buildings: + if building.energy_systems_archetype_name not in energy_system_archetypes: + energy_system_archetypes[building.energy_systems_archetype_name] = [building.name] + else: + energy_system_archetypes[building.energy_systems_archetype_name].append(building.name) + + text = "" + items = "" + for archetype, buildings in energy_system_archetypes.items(): + buildings_str = ", ".join(buildings) + text += f"Figure 4 shows the schematic of the proposed energy system for buildings {buildings_str}.\n" + if archetype in ['PV+4Pipe+DHW', 'PV+ASHP+GasBoiler+TES']: + text += "This energy system archetype is formed of the following systems: \par" + items = ['Rooftop Photovoltaic System: The rooftop PV system is tied to the grid and in case there is surplus ' + 'energy, it is sold to Hydro-Quebec through their Net-Meterin program.', + '4-Pipe HVAC System: This systems includes a 4-pipe heat pump capable of generating heat and cooling ' + 'at the same time, a natural gas boiler as the auxiliary heating system, and a hot water storage tank.' + 'The temperature inside the tank is kept between 40-55 C. The cooling demand is totally supplied by ' + 'the heat pump unit.', + 'Domestic Hot Water Heat Pump System: This system is in charge of supplying domestic hot water demand.' + 'The heat pump is connected to a thermal storage tank with electric resistance heating coil inside it.' + ' The temperature inside the tank should always remain above 60 C.'] + + self.report.add_text(text) + self.report.add_itemize(items=items) + schema_path = self.system_schemas_path / f'{archetype}.jpg' + self.report.add_image(str(schema_path).replace('\\', '/'), + f'Proposed energy system for buildings {buildings_str}') + + def plot_monthly_radiation(self): + months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] + monthly_roof_radiation = [] + for i in range(len(months)): + tilted_radiation = 0 + for building in self.city.buildings: + tilted_radiation += (building.roofs[0].global_irradiance_tilted[cte.MONTH][i] / + (cte.WATTS_HOUR_TO_JULES * 1000)) + monthly_roof_radiation.append(tilted_radiation) + + def plot_bar_chart(ax, months, values, color, ylabel, title): + ax.bar(months, values, color=color, width=0.6, zorder=2) + ax.grid(which="major", axis='x', color='#DAD8D7', alpha=0.5, zorder=1) + ax.grid(which="major", axis='y', color='#DAD8D7', alpha=0.5, zorder=1) + ax.set_xlabel('Month', fontsize=12, labelpad=10) + ax.set_ylabel(ylabel, fontsize=14, labelpad=10) + ax.set_title(title, fontsize=14, weight='bold', alpha=.8, pad=40) + ax.xaxis.set_major_locator(MaxNLocator(integer=True)) + ax.yaxis.set_major_locator(MaxNLocator(integer=True)) + ax.set_xticks(np.arange(len(months))) + ax.set_xticklabels(months, rotation=45, ha='right') + ax.bar_label(ax.containers[0], padding=3, color='black', fontsize=12, rotation=90) + ax.spines[['top', 'left', 'bottom']].set_visible(False) + ax.spines['right'].set_linewidth(1.1) + average_value = np.mean(values) + ax.axhline(y=average_value, color='grey', linewidth=2, linestyle='--') + ax.text(len(months) - 1, average_value, f'Average = {average_value:.1f} kWh', ha='right', va='bottom', + color='grey') + + # Plotting the bar chart + fig, ax = plt.subplots(figsize=(15, 8), dpi=96) + plot_bar_chart(ax, months, monthly_roof_radiation, '#ffc107', 'Tilted Roof Radiation (kWh / m2)', + 'Monthly Tilted Roof Radiation') + + # Set a white background + fig.patch.set_facecolor('white') + + # Adjust the margins around the plot area + plt.subplots_adjust(left=0.1, right=0.95, top=0.9, bottom=0.1) + + # Save the plot + chart_path = self.charts_path / 'monthly_tilted_roof_radiation.png' + plt.savefig(chart_path, bbox_inches='tight') + plt.close() + return chart_path + + def energy_consumption_comparison(self, current_status_data, retrofitted_data, file_name, title): + months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] + consumptions = { + 'Heating': ('heating', '#2196f3', 'Heating Consumption (kWh)', 'Monthly Energy Consumption for Heating'), + 'Cooling': ('cooling', '#ff5a5f', 'Cooling Consumption (kWh)', 'Monthly Energy Consumption for Cooling'), + 'DHW': ('dhw', '#4caf50', 'DHW Consumption (kWh)', 'Monthly DHW Consumption') + } + + # Helper function for plotting + def plot_double_bar_chart(ax, consumption_type, color, ylabel, title): + current_values = current_status_data[consumption_type] + retrofitted_values = retrofitted_data[consumption_type] + bar_width = 0.35 + index = np.arange(len(months)) + + ax.bar(index, current_values, bar_width, label='Current Status', color=color, alpha=0.7, zorder=2) + ax.bar(index + bar_width, retrofitted_values, bar_width, label='Retrofitted', color=color, hatch='/', zorder=2) + + ax.grid(which="major", axis='x', color='#DAD8D7', alpha=0.5, zorder=1) + ax.grid(which="major", axis='y', color='#DAD8D7', alpha=0.5, zorder=1) + ax.set_xlabel('Month', fontsize=12, labelpad=10) + ax.set_ylabel(ylabel, fontsize=14, labelpad=10) + ax.set_title(title, fontsize=14, weight='bold', alpha=.8, pad=40) + ax.set_xticks(index + bar_width / 2) + ax.set_xticklabels(months, rotation=45, ha='right') + ax.legend() + + # Adding bar labels + ax.bar_label(ax.containers[0], padding=3, color='black', fontsize=12, rotation=90) + ax.bar_label(ax.containers[1], padding=3, color='black', fontsize=12, rotation=90) + + ax.spines[['top', 'left', 'bottom']].set_visible(False) + ax.spines['right'].set_linewidth(1.1) + + # Plotting + fig, axs = plt.subplots(3, 1, figsize=(20, 15), dpi=96) + fig.suptitle(title, fontsize=16, weight='bold', alpha=.8) + + plot_double_bar_chart(axs[0], 'heating', consumptions['Heating'][1], consumptions['Heating'][2], + consumptions['Heating'][3]) + plot_double_bar_chart(axs[1], 'cooling', consumptions['Cooling'][1], consumptions['Cooling'][2], + consumptions['Cooling'][3]) + plot_double_bar_chart(axs[2], 'dhw', consumptions['DHW'][1], consumptions['DHW'][2], consumptions['DHW'][3]) + + # Set a white background + fig.patch.set_facecolor('white') + + # Adjust the margins around the plot area + plt.subplots_adjust(left=0.05, right=0.95, top=0.9, bottom=0.1, wspace=0.3, hspace=0.5) + + # Save the plot + chart_path = self.charts_path / f'{file_name}.png' + plt.savefig(chart_path, bbox_inches='tight') + plt.close() + + return chart_path + + + def yearly_consumption_comparison(self): + current_total_consumption = self.current_status_data['total_consumption'] + retrofitted_total_consumption = self.retrofitted_data['total_consumption'] + + + def pv_system(self): + self.report.add_text('The first step in PV assessments is evaluating the potential of buildings for installing ' + 'rooftop PV system. The benchmark value used for this evaluation is the mean yearly solar ' + 'incident in Montreal. According to Hydro-Quebec, the mean annual incident in Montreal is 1350' + 'kWh/m2. Therefore, any building with rooftop annual global horizontal radiation of less than ' + '1080 kWh/m2 is considered to be infeasible. Table 4 shows the yearly horizontal radiation on ' + 'buildings roofs. \par') + radiation_data = [ + ["Building Name", "Roof Area $m^2$", "Function", "Rooftop Annual Global Horizontal Radiation kWh/ $m^2$"] + ] + pv_feasible_buildings = [] + for building in self.city.buildings: + if building.roofs[0].global_irradiance[cte.YEAR][0] > 1080: + pv_feasible_buildings.append(building.name) + data = [building.name, str(format(building.roofs[0].perimeter_area, '.2f')), building.function, + str(format(building.roofs[0].global_irradiance[cte.YEAR][0] / (cte.WATTS_HOUR_TO_JULES * 1000), '.2f'))] + radiation_data.append(data) + + self.report.add_table(radiation_data, + caption='Buildings Roof Characteristics') + + if len(pv_feasible_buildings) == len(self.city.buildings): + buildings_str = 'all' + else: + buildings_str = ", ".join(pv_feasible_buildings) + self.report.add_text(f"From the table it can be seen that {buildings_str} buildings are good candidates to have " + f"rooftop PV system. The next step is calculating the amount of solar radiation on a tilted " + f"surface. Figure 5 shows the total monthly solar radiation on a surface with the tilt angle " + f"of 45 degrees on the roofs of those buildings that are identified to have rooftop PV system." + f"\par") + tilted_radiation = self.plot_monthly_radiation() + self.report.add_image(str(tilted_radiation).replace('\\', '/'), + caption='Total Monthly Solar Radiation on Buildings Roofs on a 45 Degrees Tilted Surface', + placement='H') + self.report.add_text('The first step in sizing the PV system is to find the available roof area. ' + 'Few considerations need to be made here. The considerations include space for maintenance ' + 'crew, space for mechanical equipment, and orientation correction factor to make sure all ' + 'the panel are truly facing south. After all these considerations, the minimum distance ' + 'between the panels to avoid shading throughout the year is found. Table 5 shows the number of' + 'panles on each buildings roof, yearly PV production, total electricity consumption, and self ' + 'consumption. \par') + + pv_output_table = [['Building Name', 'Total Surface Area of PV Panels ($m^2$)', + 'Total Solar Incident on PV Modules (MWh)', 'Yearly PV production (MWh)']] + + for building in self.city.buildings: + if building.name in pv_feasible_buildings: + pv_data = [] + pv_data.append(building.name) + yearly_solar_incident = (building.roofs[0].global_irradiance_tilted[cte.YEAR][0] * + building.roofs[0].installed_solar_collector_area) / (cte.WATTS_HOUR_TO_JULES * 1e6) + yearly_solar_incident_str = format(yearly_solar_incident, '.2f') + yearly_pv_output = building.onsite_electrical_production[cte.YEAR][0] / (cte.WATTS_HOUR_TO_JULES * 1e6) + yearly_pv_output_str = format(yearly_pv_output, '.2f') + + pv_data.append(format(building.roofs[0].installed_solar_collector_area, '.2f')) + pv_data.append(yearly_solar_incident_str) + pv_data.append(yearly_pv_output_str) + + pv_output_table.append(pv_data) + + self.report.add_table(pv_output_table, caption='PV System Simulation Results', first_column_width=3) + + def create_report(self): + # Add sections and text to the report + self.report.add_section('Overview of the Current Status in Buildings') + self.report.add_text('In this section, an overview of the current status of buildings characteristics, ' + 'energy demand and consumptions are provided') + self.report.add_subsection('Buildings Characteristics') + self.building_energy_info() - monthly_demands = self.monthly_demands() - monthly_demands_path = str(Path(self.charts_path / 'monthly_demands.png')) - self.plot_monthly_energy_demands(demands=monthly_demands, - file_name='monthly_demands') - self.report.add_image('monthly_demands.png', 'Total Monthly Energy Demands in City' ) + + # current monthly demands and consumptions + current_monthly_demands = self.current_status_data['monthly_demands'] + current_monthly_consumptions = self.current_status_data['monthly_consumptions'] + + # Plot and save demand chart + current_demand_chart_path = self.plot_monthly_energy_demands(data=current_monthly_demands, + file_name='current_monthly_demands', + title='Current Status Monthly Energy Demands') + # Plot and save consumption chart + current_consumption_chart_path = self.plot_monthly_energy_consumption(data=current_monthly_consumptions, + file_name='monthly_consumptions', + title='Monthly Energy Consumptions') + current_consumption_breakdown_path = self.fuel_consumption_breakdown('City_Energy_Consumption_Breakdown', + self.current_status_data) + # Add current state of energy demands in the city + self.report.add_subsection('Current State of Energy Demands in the City') + self.report.add_text('The total monthly energy demands in the city are shown in Figure 1. It should be noted ' + 'that the electricity demand refers to total lighting and appliance electricity demands') + self.report.add_image(str(current_demand_chart_path).replace('\\', '/'), + 'Total Monthly Energy Demands in City', + placement='h') + + # Add current state of energy consumption in the city + self.report.add_subsection('Current State of Energy Consumption in the City') + self.report.add_text('The following figure shows the amount of energy consumed to supply heating, cooling, and ' + 'domestic hot water needs in the city. The details of the systems in each building before ' + 'and after retrofit are provided in Section 4. \par') + self.report.add_image(str(current_consumption_chart_path).replace('\\', '/'), + 'Total Monthly Energy Consumptions in City', + placement='H') + self.report.add_text('Figure 3 shows the yearly energy supplied to the city by fuel in different sectors. ' + 'All the values are in kWh.') + self.report.add_image(str(current_consumption_breakdown_path).replace('\\', '/'), + 'Current Energy Consumption Breakdown in the City by Fuel', + placement='H') + self.report.add_section(f'{self.retrofit_scenario}') + self.report.add_subsection('Proposed Systems') + self.energy_system_archetype_schematic() + if 'PV' in self.retrofit_scenario: + self.report.add_subsection('Rooftop Photovoltaic System Implementation') + self.pv_system() + if 'System' in self.retrofit_scenario: + self.report.add_subsection('Retrofitted HVAC and DHW Systems') + self.report.add_text('Figure 6 shows a comparison between total monthly energy consumption in the selected ' + 'buildings before and after retrofitting.') + consumption_comparison = self.energy_consumption_comparison(self.current_status_data['monthly_consumptions'], + self.retrofitted_data['monthly_consumptions'], + 'energy_consumption_comparison_in_city', + 'Total Monthly Energy Consumption Comparison in ' + 'Buildings') + self.report.add_image(str(consumption_comparison).replace('\\', '/'), + caption='Comparison of Total Monthly Energy Consumption in City Buildings', + placement='H') + + # Save and compile the report self.report.save_report() self.report.compile_to_pdf() diff --git a/scripts/energy_system_retrofit_results.py b/scripts/energy_system_retrofit_results.py index 034b9a2e..e1082908 100644 --- a/scripts/energy_system_retrofit_results.py +++ b/scripts/energy_system_retrofit_results.py @@ -1,68 +1,67 @@ import hub.helpers.constants as cte -def system_results(buildings): - system_performance_summary = {} - fields = ["Energy System Archetype", "Heating Equipments", "Cooling Equipments", "DHW Equipments", - "Photovoltaic System Capacity", "Heating Fuel", "Yearly HVAC Energy Consumption (MWh)", - "DHW Energy Consumption (MWH)", "PV Yearly Production (kWh)", "LCC Analysis Duration (Years)", - "Energy System Capital Cost (CAD)", "Energy System Average Yearly Operational Cost (CAD)", - "Energy System Life Cycle Cost (CAD)"] - for building in buildings: - system_performance_summary[f'{building.name}'] = {} - for field in fields: - system_performance_summary[f'{building.name}'][field] = '-' +def consumption_data(city): + current_status_energy_consumption_data = {} + for building in city.buildings: + current_status_energy_consumption_data[f'{building.name}'] = {'heating_consumption': building.heating_consumption, + 'cooling_consumption': building.cooling_consumption, + 'domestic_hot_water_consumption': + building.domestic_hot_water_consumption, + 'energy_consumption_breakdown': + building.energy_consumption_breakdown} + heating_demand_monthly = [] + cooling_demand_monthly = [] + dhw_demand_monthly = [] + lighting_appliance_monthly = [] + heating_consumption_monthly = [] + cooling_consumption_monthly = [] + dhw_consumption_monthly = [] + for i in range(12): + heating_demand = 0 + cooling_demand = 0 + dhw_demand = 0 + lighting_appliance_demand = 0 + heating_consumption = 0 + cooling_consumption = 0 + dhw_consumption = 0 + for building in city.buildings: + heating_demand += building.heating_demand[cte.MONTH][i] / 3.6e6 + cooling_demand += building.cooling_demand[cte.MONTH][i] / 3.6e6 + dhw_demand += building.domestic_hot_water_heat_demand[cte.MONTH][i] / 3.6e6 + lighting_appliance_demand += building.lighting_electrical_demand[cte.MONTH][i] / 3.6e6 + heating_consumption += building.heating_consumption[cte.MONTH][i] / 3.6e6 + if building.cooling_demand[cte.YEAR][0] == 0: + cooling_consumption += building.cooling_demand[cte.MONTH][i] / (3.6e6 * 2) + else: + cooling_consumption += building.cooling_consumption[cte.MONTH][i] / 3.6e6 + dhw_consumption += building.domestic_hot_water_consumption[cte.MONTH][i] / 3.6e6 + heating_demand_monthly.append(heating_demand) + cooling_demand_monthly.append(cooling_demand) + dhw_demand_monthly.append(dhw_demand) + lighting_appliance_monthly.append(lighting_appliance_demand) + heating_consumption_monthly.append(heating_consumption) + cooling_consumption_monthly.append(cooling_consumption) + dhw_consumption_monthly.append(dhw_consumption) - for building in buildings: - fuels = [] - system_performance_summary[f'{building.name}']['Energy System Archetype'] = building.energy_systems_archetype_name - energy_systems = building.energy_systems - for energy_system in energy_systems: - demand_types = energy_system.demand_types - for demand_type in demand_types: - if demand_type == cte.COOLING: - equipments = [] - for generation_system in energy_system.generation_systems: - if generation_system.fuel_type == cte.ELECTRICITY: - equipments.append(generation_system.name or generation_system.system_type) - cooling_equipments = ", ".join(equipments) - system_performance_summary[f'{building.name}']['Cooling Equipments'] = cooling_equipments - elif demand_type == cte.HEATING: - equipments = [] - for generation_system in energy_system.generation_systems: - if generation_system.nominal_heat_output is not None: - equipments.append(generation_system.name or generation_system.system_type) - fuels.append(generation_system.fuel_type) - heating_equipments = ", ".join(equipments) - system_performance_summary[f'{building.name}']['Heating Equipments'] = heating_equipments - elif demand_type == cte.DOMESTIC_HOT_WATER: - equipments = [] - for generation_system in energy_system.generation_systems: - equipments.append(generation_system.name or generation_system.system_type) - dhw_equipments = ", ".join(equipments) - system_performance_summary[f'{building.name}']['DHW Equipments'] = dhw_equipments - for generation_system in energy_system.generation_systems: - if generation_system.system_type == cte.PHOTOVOLTAIC: - system_performance_summary[f'{building.name}'][ - 'Photovoltaic System Capacity'] = generation_system.nominal_electricity_output or str(0) - heating_fuels = ", ".join(fuels) - system_performance_summary[f'{building.name}']['Heating Fuel'] = heating_fuels - system_performance_summary[f'{building.name}']['Yearly HVAC Energy Consumption (MWh)'] = format( - (building.heating_consumption[cte.YEAR][0] + building.cooling_consumption[cte.YEAR][0]) / 3.6e9, '.2f') - system_performance_summary[f'{building.name}']['DHW Energy Consumption (MWH)'] = format( - building.domestic_hot_water_consumption[cte.YEAR][0] / 1e6, '.2f') - return system_performance_summary + monthly_demands = {'heating': heating_demand_monthly, + 'cooling': cooling_demand_monthly, + 'dhw': dhw_demand_monthly, + 'lighting_appliance': lighting_appliance_monthly} + monthly_consumptions = {'heating': heating_consumption_monthly, + 'cooling': cooling_consumption_monthly, + 'dhw': dhw_consumption_monthly} + yearly_heating = 0 + yearly_cooling = 0 + yearly_dhw = 0 + for building in city.buildings: + yearly_heating += building.heating_consumption[cte.YEAR][0] / 3.6e6 + yearly_cooling += building.cooling_consumption[cte.YEAR][0] / 3.6e6 + yearly_dhw += building.domestic_hot_water_consumption[cte.YEAR][0] / 3.6e6 + total_consumption = yearly_heating + yearly_cooling + yearly_dhw + current_status_energy_consumption_data['monthly_demands'] = monthly_demands + current_status_energy_consumption_data['monthly_consumptions'] = monthly_consumptions + current_status_energy_consumption_data['total_consumption'] = total_consumption -def new_system_results(buildings): - new_system_performance_summary = {} - fields = ["Energy System Archetype", "Heating Equipments", "Cooling Equipments", "DHW Equipments", - "Photovoltaic System Capacity", "Heating Fuel", "Yearly HVAC Energy Consumption (MWh)", - "DHW Energy Consumption (MWH)", "PV Yearly Production (kWh)", "LCC Analysis Duration (Years)", - "Energy System Capital Cost (CAD)", "Energy System Average Yearly Operational Cost (CAD)", - "Energy System Life Cycle Cost (CAD)"] - for building in buildings: - new_system_performance_summary[f'{building.name}'] = {} - for field in fields: - new_system_performance_summary[f'{building.name}'][field] = '-' - return new_system_performance_summary + return current_status_energy_consumption_data diff --git a/scripts/geojson_creator.py b/scripts/geojson_creator.py index f3c28b2e..c96c340d 100644 --- a/scripts/geojson_creator.py +++ b/scripts/geojson_creator.py @@ -4,13 +4,16 @@ from shapely import Point from pathlib import Path -def process_geojson(x, y, diff): +def process_geojson(x, y, diff, expansion=False): selection_box = Polygon([[x + diff, y - diff], [x - diff, y - diff], [x - diff, y + diff], [x + diff, y + diff]]) geojson_file = Path('./data/collinear_clean 2.geojson').resolve() - output_file = Path('./input_files/output_buildings.geojson').resolve() + if not expansion: + output_file = Path('./input_files/output_buildings.geojson').resolve() + else: + output_file = Path('./input_files/output_buildings_expanded.geojson').resolve() buildings_in_region = [] with open(geojson_file, 'r') as file: diff --git a/scripts/pv_feasibility.py b/scripts/pv_feasibility.py new file mode 100644 index 00000000..00488e39 --- /dev/null +++ b/scripts/pv_feasibility.py @@ -0,0 +1,34 @@ +from pathlib import Path +import subprocess +from hub.imports.geometry_factory import GeometryFactory +from scripts.geojson_creator import process_geojson +from hub.helpers.dictionaries import Dictionaries +from hub.imports.weather_factory import WeatherFactory +from hub.imports.results_factory import ResultFactory +from hub.exports.exports_factory import ExportsFactory + + +def pv_feasibility(current_x, current_y, current_diff, selected_buildings): + new_diff = current_diff * 5 + geojson_file = process_geojson(x=current_x, y=current_y, diff=new_diff, expansion=True) + file_path = (Path(__file__).parent.parent / 'input_files' / 'output_buildings_expanded.geojson') + output_path = (Path(__file__).parent.parent / 'out_files').resolve() + city = GeometryFactory('geojson', + path=file_path, + height_field='height', + year_of_construction_field='year_of_construction', + function_field='function', + function_to_hub=Dictionaries().montreal_function_to_hub_function).city + WeatherFactory('epw', city).enrich() + ExportsFactory('sra', city, output_path).export() + sra_path = (output_path / f'{city.name}_sra.xml').resolve() + subprocess.run(['sra', str(sra_path)]) + ResultFactory('sra', city, output_path).enrich() + for selected_building in selected_buildings: + for building in city.buildings: + if selected_building.name == building.name: + selected_building.roofs[0].global_irradiance = building.roofs[0].global_irradiance + + + + diff --git a/scripts/random_assignation.py b/scripts/random_assignation.py index 7483cced..ca7678bc 100644 --- a/scripts/random_assignation.py +++ b/scripts/random_assignation.py @@ -15,8 +15,8 @@ from hub.city_model_structure.building import Building energy_systems_format = 'montreal_custom' # parameters: -residential_systems_percentage = {'system 1 gas': 100, - 'system 1 electricity': 0, +residential_systems_percentage = {'system 1 gas': 44, + 'system 1 electricity': 6, 'system 2 gas': 0, 'system 2 electricity': 0, 'system 3 and 4 gas': 0, @@ -25,8 +25,8 @@ residential_systems_percentage = {'system 1 gas': 100, 'system 5 electricity': 0, 'system 6 gas': 0, 'system 6 electricity': 0, - 'system 8 gas': 0, - 'system 8 electricity': 0} + 'system 8 gas': 44, + 'system 8 electricity': 6} residential_new_systems_percentage = {'PV+ASHP+GasBoiler+TES': 0, 'PV+4Pipe+DHW': 100, diff --git a/scripts/report_creation.py b/scripts/report_creation.py index e1e88927..6298d232 100644 --- a/scripts/report_creation.py +++ b/scripts/report_creation.py @@ -15,6 +15,8 @@ class LatexReport: self.content.append(r'\usepackage[margin=2.5cm]{geometry}') self.content.append(r'\usepackage{graphicx}') self.content.append(r'\usepackage{tabularx}') + self.content.append(r'\usepackage{multirow}') + self.content.append(r'\usepackage{float}') self.content.append(r'\begin{document}') current_datetime = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") @@ -35,12 +37,16 @@ class LatexReport: def add_subsection(self, subsection_title): self.content.append(r'\subsection{' + subsection_title + r'}') + def add_subsubsection(self, subsection_title): + self.content.append(r'\subsubsection{' + subsection_title + r'}') + def add_text(self, text): self.content.append(text) - def add_table(self, table_data, caption=None, first_column_width=None): + def add_table(self, table_data, caption=None, first_column_width=None, merge_first_column=False): num_columns = len(table_data[0]) total_width = 0.9 + first_column_width_str = '' if first_column_width is not None: first_column_width_str = str(first_column_width) + 'cm' @@ -51,31 +57,58 @@ class LatexReport: self.content.append(r'\caption{' + caption + r'}') self.content.append(r'\centering') - self.content.append(r'\begin{tabularx}{\textwidth}{|p{' + first_column_width_str + r'}|' + '|'.join(['X'] * ( - num_columns - 1)) + '|}' if first_column_width is not None else r'\begin{tabularx}{\textwidth}{|' + '|'.join( - ['X'] * num_columns) + '|}') + column_format = '|p{' + first_column_width_str + r'}|' + '|'.join( + ['X'] * (num_columns - 1)) + '|' if first_column_width is not None else '|' + '|'.join(['X'] * num_columns) + '|' + self.content.append(r'\begin{tabularx}{\textwidth}{' + column_format + '}') self.content.append(r'\hline') - for row in table_data: - self.content.append(' & '.join(row) + r' \\') + + previous_first_column = None + rowspan_count = 1 + + for i, row in enumerate(table_data): + if merge_first_column and i > 0 and row[0] == previous_first_column: + rowspan_count += 1 + self.content.append(' & '.join(['' if j == 0 else cell for j, cell in enumerate(row)]) + r' \\') + else: + if merge_first_column and i > 0 and rowspan_count > 1: + self.content[-rowspan_count] = self.content[-rowspan_count].replace(r'\multirow{1}', + r'\multirow{' + str(rowspan_count) + '}') + rowspan_count = 1 + if merge_first_column and i < len(table_data) - 1 and row[0] == table_data[i + 1][0]: + self.content.append(r'\multirow{1}{*}{' + row[0] + '}' + ' & ' + ' & '.join(row[1:]) + r' \\') + else: + self.content.append(' & '.join(row) + r' \\') + previous_first_column = row[0] self.content.append(r'\hline') + + if merge_first_column and rowspan_count > 1: + self.content[-rowspan_count] = self.content[-rowspan_count].replace(r'\multirow{1}', + r'\multirow{' + str(rowspan_count) + '}') + self.content.append(r'\end{tabularx}') if caption: self.content.append(r'\end{table}') - def add_image(self, image_path, caption=None): + def add_image(self, image_path, caption=None, placement='ht'): if caption: - self.content.append(r'\begin{figure}[htbp]') + self.content.append(r'\begin{figure}[' + placement + r']') self.content.append(r'\centering') self.content.append(r'\includegraphics[width=\textwidth]{' + image_path + r'}') self.content.append(r'\caption{' + caption + r'}') self.content.append(r'\end{figure}') else: - self.content.append(r'\begin{figure}[htbp]') + self.content.append(r'\begin{figure}[' + placement + r']') self.content.append(r'\centering') self.content.append(r'\includegraphics[width=\textwidth]{' + image_path + r'}') self.content.append(r'\end{figure}') + def add_itemize(self, items): + self.content.append(r'\begin{itemize}') + for item in items: + self.content.append(r'\item ' + item) + self.content.append(r'\end{itemize}') + def save_report(self): self.content.append(r'\end{document}') with open(self.file_path, 'w') as f: diff --git a/scripts/system_simulation_models/archetype13.py b/scripts/system_simulation_models/archetype13.py index 5219af2f..892b9f3f 100644 --- a/scripts/system_simulation_models/archetype13.py +++ b/scripts/system_simulation_models/archetype13.py @@ -17,8 +17,8 @@ class Archetype13: self._heating_peak_load = building.heating_peak_load[cte.YEAR][0] self._cooling_peak_load = building.cooling_peak_load[cte.YEAR][0] self._domestic_hot_water_peak_load = building.domestic_hot_water_peak_load[cte.YEAR][0] - self._hourly_heating_demand = [demand / cte.HOUR_TO_SECONDS for demand in building.heating_demand[cte.HOUR]] - self._hourly_cooling_demand = [demand / cte.HOUR_TO_SECONDS for demand in building.cooling_demand[cte.HOUR]] + self._hourly_heating_demand = [demand / cte.WATTS_HOUR_TO_JULES for demand in building.heating_demand[cte.HOUR]] + self._hourly_cooling_demand = [demand / cte.WATTS_HOUR_TO_JULES for demand in building.cooling_demand[cte.HOUR]] self._hourly_dhw_demand = [demand / cte.WATTS_HOUR_TO_JULES for demand in building.domestic_hot_water_heat_demand[cte.HOUR]] self._output_path = output_path @@ -125,11 +125,11 @@ class Archetype13: m_dis[i + 1] = 0 t_ret[i + 1] = t_tank[i + 1] else: - if demand[i + 1] > 0.5 * self._heating_peak_load / cte.HOUR_TO_SECONDS: + if demand[i + 1] > 0.5 * self._heating_peak_load: factor = 8 else: factor = 4 - m_dis[i + 1] = self._heating_peak_load / (cte.WATER_HEAT_CAPACITY * factor * cte.HOUR_TO_SECONDS) + m_dis[i + 1] = self._heating_peak_load / (cte.WATER_HEAT_CAPACITY * factor) t_ret[i + 1] = t_tank[i + 1] - demand[i + 1] / (m_dis[i + 1] * cte.WATER_HEAT_CAPACITY) tes.temperature = [] hp_electricity_j = [(x * cte.WATTS_HOUR_TO_JULES) / number_of_ts for x in hp_electricity] @@ -191,11 +191,11 @@ class Archetype13: for i in range(1, len(demand)): if demand[i] > 0: - m[i] = self._cooling_peak_load / (cte.WATER_HEAT_CAPACITY * 5 * cte.HOUR_TO_SECONDS) + m[i] = hp.nominal_cooling_output / (cte.WATER_HEAT_CAPACITY * 5) if t_ret[i - 1] >= 13: - if demand[i] < 0.25 * self._cooling_peak_load / cte.HOUR_TO_SECONDS: + if demand[i] < 0.25 * self._cooling_peak_load: q_hp[i] = 0.25 * hp.nominal_cooling_output - elif demand[i] < 0.5 * self._cooling_peak_load / cte.HOUR_TO_SECONDS: + elif demand[i] < 0.5 * self._cooling_peak_load: q_hp[i] = 0.5 * hp.nominal_cooling_output else: q_hp[i] = hp.nominal_cooling_output @@ -210,7 +210,7 @@ class Archetype13: else: m[i] = 0 q_hp[i] = 0 - t_sup_hp[i] = t_ret[i -1] + t_sup_hp[i] = t_ret[i - 1] t_ret[i] = t_ret[i - 1] t_sup_hp_fahrenheit = 1.8 * t_sup_hp[i] + 32 t_out_fahrenheit = 1.8 * t_out[i] + 32 @@ -221,7 +221,7 @@ class Archetype13: eer_curve_coefficients[3] * t_out_fahrenheit + eer_curve_coefficients[4] * t_out_fahrenheit ** 2 + eer_curve_coefficients[5] * t_sup_hp_fahrenheit * t_out_fahrenheit) - hp_electricity[i] = q_hp[i] / hp_eer[i] + hp_electricity[i] = q_hp[i] / cooling_efficiency else: hp_eer[i] = 0 hp_electricity[i] = 0 From f32c74f84a9191115284c6ff2fdb6670c6e17ec4 Mon Sep 17 00:00:00 2001 From: s_ranjbar Date: Thu, 18 Jul 2024 08:47:18 -0400 Subject: [PATCH 12/15] fix: The report is almost completed. It will be continued after fixing all the simulation models --- energy_system_retrofit.py | 94 ++++++++---- hub/data/costs/montreal_costs_completed.xml | 2 +- report_test.py | 71 --------- scripts/costs/total_operational_costs.py | 2 +- scripts/costs/total_operational_incomes.py | 7 +- scripts/energy_system_retrofit_report.py | 120 +++++++++++++-- scripts/energy_system_retrofit_results.py | 139 ++++++++++++++++-- scripts/ep_run_enrich.py | 4 +- scripts/pv_feasibility.py | 13 +- .../system_simulation_models/archetype13.py | 4 +- 10 files changed, 316 insertions(+), 140 deletions(-) delete mode 100644 report_test.py diff --git a/energy_system_retrofit.py b/energy_system_retrofit.py index 2e49b382..b6b8cd31 100644 --- a/energy_system_retrofit.py +++ b/energy_system_retrofit.py @@ -1,4 +1,3 @@ -from scripts.geojson_creator import process_geojson from pathlib import Path import subprocess from scripts.ep_run_enrich import energy_plus_workflow @@ -8,57 +7,92 @@ from hub.imports.construction_factory import ConstructionFactory from hub.imports.usage_factory import UsageFactory from hub.imports.weather_factory import WeatherFactory from hub.imports.results_factory import ResultFactory -from scripts.energy_system_analysis_report import EnergySystemAnalysisReport +from scripts.energy_system_retrofit_report import EnergySystemRetrofitReport +from scripts.geojson_creator import process_geojson from scripts import random_assignation from hub.imports.energy_systems_factory import EnergySystemsFactory from scripts.energy_system_sizing import SystemSizing -from scripts.energy_system_retrofit_results import system_results, new_system_results +from scripts.solar_angles import CitySolarAngles +from scripts.pv_sizing_and_simulation import PVSizingSimulation +from scripts.energy_system_retrofit_results import consumption_data, cost_data from scripts.energy_system_sizing_and_simulation_factory import EnergySystemsSimulationFactory from scripts.costs.cost import Cost -from scripts.costs.constants import SKIN_RETROFIT_AND_SYSTEM_RETROFIT_AND_PV, SYSTEM_RETROFIT_AND_PV +from scripts.costs.constants import SKIN_RETROFIT_AND_SYSTEM_RETROFIT_AND_PV, SYSTEM_RETROFIT_AND_PV, CURRENT_STATUS import hub.helpers.constants as cte from hub.exports.exports_factory import ExportsFactory +from scripts.pv_feasibility import pv_feasibility + # Specify the GeoJSON file path +input_files_path = (Path(__file__).parent / 'input_files') +input_files_path.mkdir(parents=True, exist_ok=True) geojson_file = process_geojson(x=-73.5681295982132, y=45.49218262677643, diff=0.0001) -file_path = (Path(__file__).parent / 'input_files' / 'output_buildings.geojson') -# Specify the output path for the PDF file +geojson_file_path = input_files_path / 'output_buildings.geojson' output_path = (Path(__file__).parent / 'out_files').resolve() -# Create city object from GeoJSON file -city = GeometryFactory('geojson', - path=file_path, +output_path.mkdir(parents=True, exist_ok=True) +energy_plus_output_path = output_path / 'energy_plus_outputs' +energy_plus_output_path.mkdir(parents=True, exist_ok=True) +simulation_results_path = (Path(__file__).parent / 'out_files' / 'simulation_results').resolve() +simulation_results_path.mkdir(parents=True, exist_ok=True) +sra_output_path = output_path / 'sra_outputs' +sra_output_path.mkdir(parents=True, exist_ok=True) +cost_analysis_output_path = output_path / 'cost_analysis' +cost_analysis_output_path.mkdir(parents=True, exist_ok=True) +city = GeometryFactory(file_type='geojson', + path=geojson_file_path, height_field='height', year_of_construction_field='year_of_construction', function_field='function', function_to_hub=Dictionaries().montreal_function_to_hub_function).city -# Enrich city data ConstructionFactory('nrcan', city).enrich() - UsageFactory('nrcan', city).enrich() WeatherFactory('epw', city).enrich() -ExportsFactory('sra', city, output_path).export() -sra_path = (output_path / f'{city.name}_sra.xml').resolve() +ExportsFactory('sra', city, sra_output_path).export() +sra_path = (sra_output_path / f'{city.name}_sra.xml').resolve() subprocess.run(['sra', str(sra_path)]) -ResultFactory('sra', city, output_path).enrich() -energy_plus_workflow(city) +ResultFactory('sra', city, sra_output_path).enrich() +pv_feasibility(-73.5681295982132, 45.49218262677643, 0.0001, selected_buildings=city.buildings) +energy_plus_workflow(city, energy_plus_output_path) +solar_angles = CitySolarAngles(city.name, + city.latitude, + city.longitude, + tilt_angle=45, + surface_azimuth_angle=180).calculate random_assignation.call_random(city.buildings, random_assignation.residential_systems_percentage) EnergySystemsFactory('montreal_custom', city).enrich() SystemSizing(city.buildings).montreal_custom() -current_system = new_system_results(city.buildings) +current_status_energy_consumption = consumption_data(city) +current_status_life_cycle_cost = {} +for building in city.buildings: + cost_retrofit_scenario = CURRENT_STATUS + lcc_dataframe = Cost(building=building, + retrofit_scenario=cost_retrofit_scenario, + fuel_tariffs=['Electricity-D', 'Gas-Energir']).life_cycle + lcc_dataframe.to_csv(cost_analysis_output_path / f'{building.name}_current_status_lcc.csv') + current_status_life_cycle_cost[f'{building.name}'] = cost_data(building, lcc_dataframe, cost_retrofit_scenario) random_assignation.call_random(city.buildings, random_assignation.residential_new_systems_percentage) EnergySystemsFactory('montreal_future', city).enrich() for building in city.buildings: - EnergySystemsSimulationFactory('archetype1', building=building, output_path=output_path).enrich() - print(building.energy_consumption_breakdown[cte.ELECTRICITY][cte.COOLING] + - building.energy_consumption_breakdown[cte.ELECTRICITY][cte.HEATING] + - building.energy_consumption_breakdown[cte.ELECTRICITY][cte.DOMESTIC_HOT_WATER]) -new_system = new_system_results(city.buildings) -# EnergySystemAnalysisReport(city, output_path).create_report(current_system, new_system) + if 'PV' in building.energy_systems_archetype_name: + ghi = [x / cte.WATTS_HOUR_TO_JULES for x in building.roofs[0].global_irradiance[cte.HOUR]] + pv_sizing_simulation = PVSizingSimulation(building, + solar_angles, + tilt_angle=45, + module_height=1, + module_width=2, + ghi=ghi) + pv_sizing_simulation.pv_output() + if building.energy_systems_archetype_name == 'PV+4Pipe+DHW': + EnergySystemsSimulationFactory('archetype13', building=building, output_path=simulation_results_path).enrich() +retrofitted_energy_consumption = consumption_data(city) +retrofitted_life_cycle_cost = {} for building in city.buildings: - costs = Cost(building=building, retrofit_scenario=SYSTEM_RETROFIT_AND_PV).life_cycle - costs.to_csv(output_path / f'{building.name}_lcc.csv') - (costs.loc['global_operational_costs', f'Scenario {SYSTEM_RETROFIT_AND_PV}']. - to_csv(output_path / f'{building.name}_op.csv')) - costs.loc['global_capital_costs', f'Scenario {SYSTEM_RETROFIT_AND_PV}'].to_csv( - output_path / f'{building.name}_cc.csv') - costs.loc['global_maintenance_costs', f'Scenario {SYSTEM_RETROFIT_AND_PV}'].to_csv( - output_path / f'{building.name}_m.csv') \ No newline at end of file + cost_retrofit_scenario = SYSTEM_RETROFIT_AND_PV + lcc_dataframe = Cost(building=building, + retrofit_scenario=cost_retrofit_scenario, + fuel_tariffs=['Electricity-D', 'Gas-Energir']).life_cycle + lcc_dataframe.to_csv(cost_analysis_output_path / f'{building.name}_retrofitted_lcc.csv') + retrofitted_life_cycle_cost[f'{building.name}'] = cost_data(building, lcc_dataframe, cost_retrofit_scenario) +(EnergySystemRetrofitReport(city, output_path, 'PV Implementation and System Retrofit', + current_status_energy_consumption, retrofitted_energy_consumption, + current_status_life_cycle_cost, retrofitted_life_cycle_cost).create_report()) + diff --git a/hub/data/costs/montreal_costs_completed.xml b/hub/data/costs/montreal_costs_completed.xml index fc23634a..6b3fc41f 100644 --- a/hub/data/costs/montreal_costs_completed.xml +++ b/hub/data/costs/montreal_costs_completed.xml @@ -187,7 +187,7 @@ 1.5 3.6 - 0.07 + 0.075 5 diff --git a/report_test.py b/report_test.py deleted file mode 100644 index aa67926b..00000000 --- a/report_test.py +++ /dev/null @@ -1,71 +0,0 @@ -from pathlib import Path -import subprocess -from scripts.ep_run_enrich import energy_plus_workflow -from hub.imports.geometry_factory import GeometryFactory -from hub.helpers.dictionaries import Dictionaries -from hub.imports.construction_factory import ConstructionFactory -from hub.imports.usage_factory import UsageFactory -from hub.imports.weather_factory import WeatherFactory -from hub.imports.results_factory import ResultFactory -from scripts.energy_system_retrofit_report import EnergySystemRetrofitReport -from scripts.geojson_creator import process_geojson -from scripts import random_assignation -from hub.imports.energy_systems_factory import EnergySystemsFactory -from scripts.energy_system_sizing import SystemSizing -from scripts.solar_angles import CitySolarAngles -from scripts.pv_sizing_and_simulation import PVSizingSimulation -from scripts.energy_system_retrofit_results import consumption_data -from scripts.energy_system_sizing_and_simulation_factory import EnergySystemsSimulationFactory -from scripts.costs.cost import Cost -from scripts.costs.constants import SKIN_RETROFIT_AND_SYSTEM_RETROFIT_AND_PV, SYSTEM_RETROFIT_AND_PV -import hub.helpers.constants as cte -from hub.exports.exports_factory import ExportsFactory -from scripts.pv_feasibility import pv_feasibility - -geojson_file = process_geojson(x=-73.5681295982132, y=45.49218262677643, diff=0.0001) -file_path = (Path(__file__).parent / 'input_files' / 'output_buildings.geojson') -output_path = (Path(__file__).parent / 'out_files').resolve() -simulation_results_path = (Path(__file__).parent / 'out_files' / 'simulation_results').resolve() -simulation_results_path.mkdir(parents=True, exist_ok=True) -city = GeometryFactory(file_type='geojson', - path=file_path, - height_field='height', - year_of_construction_field='year_of_construction', - function_field='function', - function_to_hub=Dictionaries().montreal_function_to_hub_function).city -ConstructionFactory('nrcan', city).enrich() -UsageFactory('nrcan', city).enrich() -WeatherFactory('epw', city).enrich() -ExportsFactory('sra', city, output_path).export() -sra_path = (output_path / f'{city.name}_sra.xml').resolve() -subprocess.run(['sra', str(sra_path)]) -ResultFactory('sra', city, output_path).enrich() -pv_feasibility(-73.5681295982132, 45.49218262677643, 0.0001, selected_buildings=city.buildings) -energy_plus_workflow(city) -solar_angles = CitySolarAngles(city.name, - city.latitude, - city.longitude, - tilt_angle=45, - surface_azimuth_angle=180).calculate -random_assignation.call_random(city.buildings, random_assignation.residential_systems_percentage) -EnergySystemsFactory('montreal_custom', city).enrich() -SystemSizing(city.buildings).montreal_custom() -current_status_energy_consumption = consumption_data(city) -random_assignation.call_random(city.buildings, random_assignation.residential_new_systems_percentage) -EnergySystemsFactory('montreal_future', city).enrich() -for building in city.buildings: - if 'PV' in building.energy_systems_archetype_name: - ghi = [x / cte.WATTS_HOUR_TO_JULES for x in building.roofs[0].global_irradiance[cte.HOUR]] - pv_sizing_simulation = PVSizingSimulation(building, - solar_angles, - tilt_angle=45, - module_height=1, - module_width=2, - ghi=ghi) - pv_sizing_simulation.pv_output() - if building.energy_systems_archetype_name == 'PV+4Pipe+DHW': - EnergySystemsSimulationFactory('archetype13', building=building, output_path=simulation_results_path).enrich() -retrofitted_energy_consumption = consumption_data(city) -(EnergySystemRetrofitReport(city, output_path, 'PV Implementation and System Retrofit', - current_status_energy_consumption, retrofitted_energy_consumption).create_report()) - diff --git a/scripts/costs/total_operational_costs.py b/scripts/costs/total_operational_costs.py index dc672fa1..60ed54a9 100644 --- a/scripts/costs/total_operational_costs.py +++ b/scripts/costs/total_operational_costs.py @@ -196,7 +196,7 @@ class TotalOperationalCosts(CostBase): if cooling is not None: hourly += cooling[i] / 3600 if dhw is not None: - dhw += dhw[i] / 3600 + hourly += dhw[i] / 3600 hourly_fuel_consumption.append(hourly) else: heating = None diff --git a/scripts/costs/total_operational_incomes.py b/scripts/costs/total_operational_incomes.py index 2a110761..66d789ed 100644 --- a/scripts/costs/total_operational_incomes.py +++ b/scripts/costs/total_operational_incomes.py @@ -36,11 +36,10 @@ class TotalOperationalIncomes(CostBase): for year in range(1, self._configuration.number_of_years + 1): price_increase_electricity = math.pow(1 + self._configuration.electricity_price_index, year) - # todo: check the adequate assignation of price. Pilar - price_export = archetype.income.electricity_export * cte.WATTS_HOUR_TO_JULES * 1000 # to account for unit change + price_export = archetype.income.electricity_export # to account for unit change self._yearly_operational_incomes.loc[year, 'Incomes electricity'] = ( - onsite_electricity_production * price_export * price_increase_electricity + (onsite_electricity_production / cte.WATTS_HOUR_TO_JULES) * price_export * price_increase_electricity ) self._yearly_operational_incomes.fillna(0, inplace=True) - return self._yearly_operational_incomes + return self._yearly_operational_incomes \ No newline at end of file diff --git a/scripts/energy_system_retrofit_report.py b/scripts/energy_system_retrofit_report.py index 60e82ece..fcfb764f 100644 --- a/scripts/energy_system_retrofit_report.py +++ b/scripts/energy_system_retrofit_report.py @@ -1,11 +1,8 @@ import os import hub.helpers.constants as cte import matplotlib.pyplot as plt -import random -import matplotlib.colors as mcolors from matplotlib import cm from scripts.report_creation import LatexReport -import matplotlib as mpl from matplotlib.ticker import MaxNLocator import numpy as np from pathlib import Path @@ -14,10 +11,12 @@ import glob class EnergySystemRetrofitReport: def __init__(self, city, output_path, retrofit_scenario, current_status_energy_consumption_data, - retrofitted_energy_consumption_data): + retrofitted_energy_consumption_data, current_status_lcc_data, retrofitted_lcc_data): self.city = city self.current_status_data = current_status_energy_consumption_data self.retrofitted_data = retrofitted_energy_consumption_data + self.current_status_lcc = current_status_lcc_data + self.retrofitted_lcc = retrofitted_lcc_data self.output_path = output_path self.content = [] self.retrofit_scenario = retrofit_scenario @@ -334,7 +333,7 @@ class EnergySystemRetrofitReport: ax.spines['right'].set_linewidth(1.1) # Plotting - fig, axs = plt.subplots(3, 1, figsize=(20, 15), dpi=96) + fig, axs = plt.subplots(3, 1, figsize=(20, 25), dpi=96) fig.suptitle(title, fontsize=16, weight='bold', alpha=.8) plot_double_bar_chart(axs[0], 'heating', consumptions['Heating'][1], consumptions['Heating'][2], @@ -356,11 +355,21 @@ class EnergySystemRetrofitReport: return chart_path - def yearly_consumption_comparison(self): - current_total_consumption = self.current_status_data['total_consumption'] - retrofitted_total_consumption = self.retrofitted_data['total_consumption'] - + current_total_consumption = round(self.current_status_data['total_consumption'], 2) + retrofitted_total_consumption = round(self.retrofitted_data['total_consumption'], 2) + text = ( + f'The total yearly energy consumption before and after the retrofit are {current_total_consumption} MWh and ' + f'{retrofitted_total_consumption} MWh, respectively.') + if retrofitted_total_consumption < current_total_consumption: + change = str(round((current_total_consumption - retrofitted_total_consumption) * 100 / current_total_consumption, + 2)) + text += f'Therefore, the total yearly energy consumption decreased by {change} \%.' + else: + change = str(round((retrofitted_total_consumption - current_total_consumption) * 100 / + retrofitted_total_consumption, 2)) + text += f'Therefore, the total yearly energy consumption increased by {change} \%. \par' + self.report.add_text(text) def pv_system(self): self.report.add_text('The first step in PV assessments is evaluating the potential of buildings for installing ' @@ -425,6 +434,85 @@ class EnergySystemRetrofitReport: self.report.add_table(pv_output_table, caption='PV System Simulation Results', first_column_width=3) + def life_cycle_cost_stacked_bar(self, file_name, title): + # Aggregate LCC components for current and retrofitted statuses + current_status_capex = 0 + current_status_opex = 0 + current_status_maintenance = 0 + current_status_end_of_life = 0 + retrofitted_capex = 0 + retrofitted_opex = 0 + retrofitted_maintenance = 0 + retrofitted_end_of_life = 0 + + for building in self.city.buildings: + current_status_capex += self.current_status_lcc[f'{building.name}']['capital_cost_per_sqm'] + retrofitted_capex += self.retrofitted_lcc[f'{building.name}']['capital_cost_per_sqm'] + current_status_opex += self.current_status_lcc[f'{building.name}']['operational_cost_per_sqm'] + retrofitted_opex += self.retrofitted_lcc[f'{building.name}']['operational_cost_per_sqm'] + current_status_maintenance += self.current_status_lcc[f'{building.name}']['maintenance_cost_per_sqm'] + retrofitted_maintenance += self.retrofitted_lcc[f'{building.name}']['maintenance_cost_per_sqm'] + current_status_end_of_life += self.current_status_lcc[f'{building.name}']['end_of_life_cost_per_sqm'] + retrofitted_end_of_life += self.retrofitted_lcc[f'{building.name}']['end_of_life_cost_per_sqm'] + + current_status_lcc_components_sqm = { + 'Capital Cost': current_status_capex / len(self.city.buildings), + 'Operational Cost': current_status_opex / len(self.city.buildings), + 'Maintenance Cost': current_status_maintenance / len(self.city.buildings), + 'End of Life Cost': current_status_end_of_life / len(self.city.buildings) + } + retrofitted_lcc_components_sqm = { + 'Capital Cost': retrofitted_capex / len(self.city.buildings), + 'Operational Cost': retrofitted_opex / len(self.city.buildings), + 'Maintenance Cost': retrofitted_maintenance / len(self.city.buildings), + 'End of Life Cost': retrofitted_end_of_life / len(self.city.buildings) + } + + labels = ['Current Status', 'Retrofitted Status'] + categories = ['Capital Cost', 'Operational Cost', 'Maintenance Cost', 'End of Life Cost'] + current_values = list(current_status_lcc_components_sqm.values()) + retrofitted_values = list(retrofitted_lcc_components_sqm.values()) + colors = ['#2196f3', '#ff5a5f', '#4caf50', '#ffc107'] + + # Data preparation + bar_width = 0.35 + r = np.arange(len(labels)) + + fig, ax = plt.subplots(figsize=(12, 8), dpi=96) + fig.suptitle(title, fontsize=16, weight='bold', alpha=.8) + + # Plotting current status data + bottom = np.zeros(2) + for i, (category, color) in enumerate(zip(categories, colors)): + values = [current_status_lcc_components_sqm[category], retrofitted_lcc_components_sqm[category]] + ax.bar(r, values, bottom=bottom, color=color, edgecolor='white', width=bar_width, label=category) + bottom += values + + # Adding summation annotations at the top of the bars + for idx, (x, total) in enumerate(zip(r, bottom)): + ax.text(x, total, f'{total:.1f}', ha='center', va='bottom', fontsize=12, fontweight='bold') + + # Adding labels, title, and grid + ax.set_xlabel('LCC Components', fontsize=12, labelpad=10) + ax.set_ylabel('Average Cost (CAD/m²)', fontsize=14, labelpad=10) + ax.grid(which="major", axis='y', color='#DAD8D7', alpha=0.5, zorder=1) + ax.set_xticks(r) + ax.set_xticklabels(labels, rotation=45, ha='right') + ax.legend() + + # Adding a white background + fig.patch.set_facecolor('white') + + # Adjusting the margins around the plot area + plt.subplots_adjust(left=0.05, right=0.95, top=0.9, bottom=0.2) + + # Save the plot + chart_path = self.charts_path / f'{file_name}.png' + plt.savefig(chart_path, bbox_inches='tight') + plt.close() + + return chart_path + def create_report(self): # Add sections and text to the report self.report.add_section('Overview of the Current Status in Buildings') @@ -448,6 +536,11 @@ class EnergySystemRetrofitReport: title='Monthly Energy Consumptions') current_consumption_breakdown_path = self.fuel_consumption_breakdown('City_Energy_Consumption_Breakdown', self.current_status_data) + retrofitted_consumption_breakdown_path = self.fuel_consumption_breakdown( + 'fuel_consumption_breakdown_after_retrofit', + self.retrofitted_data) + life_cycle_cost_sqm_stacked_bar_chart_path = self.life_cycle_cost_stacked_bar('lcc_per_sqm', + 'LCC Analysis') # Add current state of energy demands in the city self.report.add_subsection('Current State of Energy Demands in the City') self.report.add_text('The total monthly energy demands in the city are shown in Figure 1. It should be noted ' @@ -487,6 +580,15 @@ class EnergySystemRetrofitReport: self.report.add_image(str(consumption_comparison).replace('\\', '/'), caption='Comparison of Total Monthly Energy Consumption in City Buildings', placement='H') + self.yearly_consumption_comparison() + self.report.add_text('Figure 7 shows the fuel consumption breakdown in the area after the retrofit.') + self.report.add_image(str(retrofitted_consumption_breakdown_path).replace('\\', '/'), + caption=f'Fuel Consumption Breakdown After {self.retrofit_scenario}', + placement='H') + self.report.add_subsection('Life Cycle Cost Analysis') + self.report.add_image(str(life_cycle_cost_sqm_stacked_bar_chart_path).replace('\\', '/'), + caption='Average Life Cycle Cost Components', + placement='H') # Save and compile the report self.report.save_report() diff --git a/scripts/energy_system_retrofit_results.py b/scripts/energy_system_retrofit_results.py index e1082908..9d85d0d9 100644 --- a/scripts/energy_system_retrofit_results.py +++ b/scripts/energy_system_retrofit_results.py @@ -1,15 +1,88 @@ import hub.helpers.constants as cte +def hourly_electricity_consumption_profile(building): + hourly_electricity_consumption = [] + energy_systems = building.energy_systems + appliance = building.appliances_electrical_demand[cte.HOUR] + lighting = building.lighting_electrical_demand[cte.HOUR] + elec_heating = 0 + elec_cooling = 0 + elec_dhw = 0 + if cte.HEATING in building.energy_consumption_breakdown[cte.ELECTRICITY]: + elec_heating = 1 + if cte.COOLING in building.energy_consumption_breakdown[cte.ELECTRICITY]: + elec_cooling = 1 + if cte.DOMESTIC_HOT_WATER in building.energy_consumption_breakdown[cte.ELECTRICITY]: + elec_dhw = 1 + heating = None + cooling = None + dhw = None + if elec_heating == 1: + for energy_system in energy_systems: + if cte.HEATING in energy_system.demand_types: + for generation_system in energy_system.generation_systems: + if generation_system.fuel_type == cte.ELECTRICITY: + if cte.HEATING in generation_system.energy_consumption: + heating = generation_system.energy_consumption[cte.HEATING][cte.HOUR] + else: + if len(energy_system.generation_systems) > 1: + heating = [x / 2 for x in building.heating_consumption[cte.HOUR]] + else: + heating = building.heating_consumption[cte.HOUR] + if elec_dhw == 1: + for energy_system in energy_systems: + if cte.DOMESTIC_HOT_WATER in energy_system.demand_types: + for generation_system in energy_system.generation_systems: + if generation_system.fuel_type == cte.ELECTRICITY: + if cte.DOMESTIC_HOT_WATER in generation_system.energy_consumption: + dhw = generation_system.energy_consumption[cte.DOMESTIC_HOT_WATER][cte.HOUR] + else: + if len(energy_system.generation_systems) > 1: + dhw = [x / 2 for x in building.domestic_hot_water_consumption[cte.HOUR]] + else: + dhw = building.domestic_hot_water_consumption[cte.HOUR] + + if elec_cooling == 1: + for energy_system in energy_systems: + if cte.COOLING in energy_system.demand_types: + for generation_system in energy_system.generation_systems: + if cte.COOLING in generation_system.energy_consumption: + cooling = generation_system.energy_consumption[cte.COOLING][cte.HOUR] + else: + if len(energy_system.generation_systems) > 1: + cooling = [x / 2 for x in building.cooling_consumption[cte.HOUR]] + else: + cooling = building.cooling_consumption[cte.HOUR] + + for i in range(len(building.heating_demand[cte.HOUR])): + hourly = 0 + hourly += appliance[i] / 3600 + hourly += lighting[i] / 3600 + if heating is not None: + hourly += heating[i] / 3600 + if cooling is not None: + hourly += cooling[i] / 3600 + if dhw is not None: + hourly += dhw[i] / 3600 + hourly_electricity_consumption.append(hourly) + return hourly_electricity_consumption + + def consumption_data(city): - current_status_energy_consumption_data = {} + energy_consumption_data = {} for building in city.buildings: - current_status_energy_consumption_data[f'{building.name}'] = {'heating_consumption': building.heating_consumption, - 'cooling_consumption': building.cooling_consumption, - 'domestic_hot_water_consumption': - building.domestic_hot_water_consumption, - 'energy_consumption_breakdown': - building.energy_consumption_breakdown} + hourly_electricity_consumption = hourly_electricity_consumption_profile(building) + energy_consumption_data[f'{building.name}'] = {'heating_consumption': building.heating_consumption, + 'cooling_consumption': building.cooling_consumption, + 'domestic_hot_water_consumption': + building.domestic_hot_water_consumption, + 'energy_consumption_breakdown': + building.energy_consumption_breakdown, + 'hourly_electricity_consumption': hourly_electricity_consumption} + peak_electricity_consumption = 0 + for building in energy_consumption_data: + peak_electricity_consumption += max(energy_consumption_data[building]['hourly_electricity_consumption']) heating_demand_monthly = [] cooling_demand_monthly = [] dhw_demand_monthly = [] @@ -54,14 +127,50 @@ def consumption_data(city): yearly_heating = 0 yearly_cooling = 0 yearly_dhw = 0 + yearly_appliance = 0 + yearly_lighting = 0 for building in city.buildings: - yearly_heating += building.heating_consumption[cte.YEAR][0] / 3.6e6 - yearly_cooling += building.cooling_consumption[cte.YEAR][0] / 3.6e6 - yearly_dhw += building.domestic_hot_water_consumption[cte.YEAR][0] / 3.6e6 + yearly_appliance += building.appliances_electrical_demand[cte.YEAR][0] / 3.6e9 + yearly_lighting += building.lighting_electrical_demand[cte.YEAR][0] / 3.6e9 + yearly_heating += building.heating_consumption[cte.YEAR][0] / 3.6e9 + yearly_cooling += building.cooling_consumption[cte.YEAR][0] / 3.6e9 + yearly_dhw += building.domestic_hot_water_consumption[cte.YEAR][0] / 3.6e9 - total_consumption = yearly_heating + yearly_cooling + yearly_dhw - current_status_energy_consumption_data['monthly_demands'] = monthly_demands - current_status_energy_consumption_data['monthly_consumptions'] = monthly_consumptions - current_status_energy_consumption_data['total_consumption'] = total_consumption + total_consumption = yearly_heating + yearly_cooling + yearly_dhw + yearly_appliance + yearly_lighting + energy_consumption_data['monthly_demands'] = monthly_demands + energy_consumption_data['monthly_consumptions'] = monthly_consumptions + energy_consumption_data['total_consumption'] = total_consumption + energy_consumption_data['maximum_hourly_electricity_consumption'] = peak_electricity_consumption - return current_status_energy_consumption_data + return energy_consumption_data + + +def cost_data(building, lcc_dataframe, cost_retrofit_scenario): + total_floor_area = 0 + for thermal_zone in building.thermal_zones_from_internal_zones: + total_floor_area += thermal_zone.total_floor_area + capital_cost = lcc_dataframe.loc['total_capital_costs_systems', f'Scenario {cost_retrofit_scenario}'] + operational_cost = lcc_dataframe.loc['total_operational_costs', f'Scenario {cost_retrofit_scenario}'] + maintenance_cost = lcc_dataframe.loc['total_maintenance_costs', f'Scenario {cost_retrofit_scenario}'] + end_of_life_cost = lcc_dataframe.loc['end_of_life_costs', f'Scenario {cost_retrofit_scenario}'] + operational_income = lcc_dataframe.loc['operational_incomes', f'Scenario {cost_retrofit_scenario}'] + total_life_cycle_cost = capital_cost + operational_cost + maintenance_cost + end_of_life_cost + operational_income + specific_capital_cost = capital_cost / total_floor_area + specific_operational_cost = operational_cost / total_floor_area + specific_maintenance_cost = maintenance_cost / total_floor_area + specific_end_of_life_cost = end_of_life_cost / total_floor_area + specific_operational_income = operational_income / total_floor_area + specific_life_cycle_cost = total_life_cycle_cost / total_floor_area + life_cycle_cost_analysis = {'capital_cost': capital_cost, + 'capital_cost_per_sqm': specific_capital_cost, + 'operational_cost': operational_cost, + 'operational_cost_per_sqm': specific_operational_cost, + 'maintenance_cost': maintenance_cost, + 'maintenance_cost_per_sqm': specific_maintenance_cost, + 'end_of_life_cost': end_of_life_cost, + 'end_of_life_cost_per_sqm': specific_end_of_life_cost, + 'operational_income': operational_income, + 'operational_income_per_sqm': specific_operational_income, + 'total_life_cycle_cost': total_life_cycle_cost, + 'total_life_cycle_cost_per_sqm': specific_life_cycle_cost} + return life_cycle_cost_analysis diff --git a/scripts/ep_run_enrich.py b/scripts/ep_run_enrich.py index 24ee4b11..68c24c8c 100644 --- a/scripts/ep_run_enrich.py +++ b/scripts/ep_run_enrich.py @@ -9,10 +9,10 @@ from hub.imports.results_factory import ResultFactory sys.path.append('./') -def energy_plus_workflow(city): +def energy_plus_workflow(city, output_path): try: # city = city - out_path = (Path(__file__).parent.parent / 'out_files') + out_path = output_path files = glob.glob(f'{out_path}/*') # for file in files: diff --git a/scripts/pv_feasibility.py b/scripts/pv_feasibility.py index 00488e39..034a5efb 100644 --- a/scripts/pv_feasibility.py +++ b/scripts/pv_feasibility.py @@ -9,10 +9,13 @@ from hub.exports.exports_factory import ExportsFactory def pv_feasibility(current_x, current_y, current_diff, selected_buildings): + input_files_path = (Path(__file__).parent.parent / 'input_files') + output_path = (Path(__file__).parent.parent / 'out_files').resolve() + sra_output_path = output_path / 'sra_outputs' / 'extended_city_sra_outputs' + sra_output_path.mkdir(parents=True, exist_ok=True) new_diff = current_diff * 5 geojson_file = process_geojson(x=current_x, y=current_y, diff=new_diff, expansion=True) - file_path = (Path(__file__).parent.parent / 'input_files' / 'output_buildings_expanded.geojson') - output_path = (Path(__file__).parent.parent / 'out_files').resolve() + file_path = input_files_path / 'output_buildings.geojson' city = GeometryFactory('geojson', path=file_path, height_field='height', @@ -20,10 +23,10 @@ def pv_feasibility(current_x, current_y, current_diff, selected_buildings): function_field='function', function_to_hub=Dictionaries().montreal_function_to_hub_function).city WeatherFactory('epw', city).enrich() - ExportsFactory('sra', city, output_path).export() - sra_path = (output_path / f'{city.name}_sra.xml').resolve() + ExportsFactory('sra', city, sra_output_path).export() + sra_path = (sra_output_path / f'{city.name}_sra.xml').resolve() subprocess.run(['sra', str(sra_path)]) - ResultFactory('sra', city, output_path).enrich() + ResultFactory('sra', city, sra_output_path).enrich() for selected_building in selected_buildings: for building in city.buildings: if selected_building.name == building.name: diff --git a/scripts/system_simulation_models/archetype13.py b/scripts/system_simulation_models/archetype13.py index 892b9f3f..77b52da6 100644 --- a/scripts/system_simulation_models/archetype13.py +++ b/scripts/system_simulation_models/archetype13.py @@ -377,8 +377,8 @@ class Archetype13: self._building.domestic_hot_water_consumption[cte.HOUR] = dhw_consumption self._building.domestic_hot_water_consumption[cte.MONTH] = ( MonthlyValues.get_total_month(self._building.domestic_hot_water_consumption[cte.HOUR])) - self._building.domestic_hot_water_consumption[cte.YEAR] = ( - sum(self._building.domestic_hot_water_consumption[cte.MONTH])) + self._building.domestic_hot_water_consumption[cte.YEAR] = [ + sum(self._building.domestic_hot_water_consumption[cte.MONTH])] file_name = f'energy_system_simulation_results_{self._name}.csv' with open(self._output_path / file_name, 'w', newline='') as csvfile: output_file = csv.writer(csvfile) From 6044cfc4e5cc667f61cf7a9e757805a3084b14cc Mon Sep 17 00:00:00 2001 From: s_ranjbar Date: Thu, 18 Jul 2024 08:52:58 -0400 Subject: [PATCH 13/15] fix: cleaning the repo --- main.py | 68 ----- scripts/energy_system_analysis_report.py | 338 ----------------------- scripts/system_simulation.py | 135 --------- 3 files changed, 541 deletions(-) delete mode 100644 scripts/energy_system_analysis_report.py delete mode 100644 scripts/system_simulation.py diff --git a/main.py b/main.py index 5140022c..e69de29b 100644 --- a/main.py +++ b/main.py @@ -1,68 +0,0 @@ -from scripts.geojson_creator import process_geojson -from pathlib import Path -import subprocess -from scripts.ep_run_enrich import energy_plus_workflow -from hub.imports.geometry_factory import GeometryFactory -from hub.helpers.dictionaries import Dictionaries -from hub.imports.construction_factory import ConstructionFactory -from hub.imports.usage_factory import UsageFactory -from hub.imports.weather_factory import WeatherFactory -from hub.imports.results_factory import ResultFactory -from scripts import random_assignation -from hub.imports.energy_systems_factory import EnergySystemsFactory -from scripts.energy_system_sizing_and_simulation_factory import EnergySystemsSimulationFactory -from scripts.costs.cost import Cost -from scripts.costs.constants import SKIN_RETROFIT_AND_SYSTEM_RETROFIT_AND_PV, SYSTEM_RETROFIT_AND_PV, SYSTEM_RETROFIT -import hub.helpers.constants as cte -from scripts.solar_angles import CitySolarAngles -from scripts.pv_sizing_and_simulation import PVSizingSimulation -from hub.exports.exports_factory import ExportsFactory -# Specify the GeoJSON file path -geojson_file = process_geojson(x=-73.5681295982132, y=45.49218262677643, diff=0.0001) -file_path = (Path(__file__).parent / 'input_files' / 'output_buildings.geojson') -# Specify the output path for the PDF file -output_path = (Path(__file__).parent / 'out_files').resolve() -# Create city object from GeoJSON file -city = GeometryFactory('geojson', - path=file_path, - height_field='height', - year_of_construction_field='year_of_construction', - function_field='function', - function_to_hub=Dictionaries().montreal_function_to_hub_function).city -# Enrich city data -ConstructionFactory('nrcan', city).enrich() - -UsageFactory('nrcan', city).enrich() -WeatherFactory('epw', city).enrich() -ExportsFactory('sra', city, output_path).export() -sra_path = (output_path / f'{city.name}_sra.xml').resolve() -subprocess.run(['sra', str(sra_path)]) -ResultFactory('sra', city, output_path).enrich() -solar_angles = CitySolarAngles(city.name, - city.latitude, - city.longitude, - tilt_angle=45, - surface_azimuth_angle=180).calculate -energy_plus_workflow(city) -random_assignation.call_random(city.buildings, random_assignation.residential_new_systems_percentage) -EnergySystemsFactory('montreal_future', city).enrich() -for building in city.buildings: - EnergySystemsSimulationFactory('archetype13', building=building, output_path=output_path).enrich() - if 'PV' in building.energy_systems_archetype_name: - ghi = [x / cte.WATTS_HOUR_TO_JULES for x in building.roofs[0].global_irradiance[cte.HOUR]] - pv_sizing_simulation = PVSizingSimulation(building, - solar_angles, - tilt_angle=45, - module_height=1, - module_width=2, - ghi=ghi) - pv_sizing_simulation.pv_output() -for building in city.buildings: - costs = Cost(building=building, retrofit_scenario=SYSTEM_RETROFIT).life_cycle - costs.to_csv(output_path / f'{building.name}_lcc.csv') - (costs.loc['global_operational_costs', f'Scenario {SYSTEM_RETROFIT}']. - to_csv(output_path / f'{building.name}_op.csv')) - costs.loc['global_capital_costs', f'Scenario {SYSTEM_RETROFIT}'].to_csv( - output_path / f'{building.name}_cc.csv') - costs.loc['global_maintenance_costs', f'Scenario {SYSTEM_RETROFIT}'].to_csv( - output_path / f'{building.name}_m.csv') diff --git a/scripts/energy_system_analysis_report.py b/scripts/energy_system_analysis_report.py deleted file mode 100644 index 16620fc6..00000000 --- a/scripts/energy_system_analysis_report.py +++ /dev/null @@ -1,338 +0,0 @@ -import os -import hub.helpers.constants as cte -import matplotlib.pyplot as plt -import random -import matplotlib.colors as mcolors -from matplotlib import cm -from scripts.report_creation import LatexReport - -class EnergySystemAnalysisReport: - def __init__(self, city, output_path): - self.city = city - self.output_path = output_path - self.content = [] - self.report = LatexReport('energy_system_analysis_report.tex') - - def building_energy_info(self): - - table_data = [ - ["Building Name", "Year of Construction", "function", "Yearly Heating Demand (MWh)", - "Yearly Cooling Demand (MWh)", "Yearly DHW Demand (MWh)", "Yearly Electricity Demand (MWh)"] - ] - intensity_table_data = [["Building Name", "Total Floor Area m2", "Heating Demand Intensity kWh/m2", - "Cooling Demand Intensity kWh/m2", "Electricity Intensity kWh/m2"]] - - for building in self.city.buildings: - total_floor_area = 0 - for zone in building.thermal_zones_from_internal_zones: - total_floor_area += zone.total_floor_area - building_data = [ - building.name, - str(building.year_of_construction), - building.function, - str(format(building.heating_demand[cte.YEAR][0] / 3.6e9, '.2f')), - str(format(building.cooling_demand[cte.YEAR][0] / 3.6e9, '.2f')), - str(format(building.domestic_hot_water_heat_demand[cte.YEAR][0] / 1e6, '.2f')), - str(format( - (building.lighting_electrical_demand[cte.YEAR][0] + building.appliances_electrical_demand[cte.YEAR][0]) - / 1e6, '.2f')), - ] - intensity_data = [ - building.name, - str(format(total_floor_area, '.2f')), - str(format(building.heating_demand[cte.YEAR][0] / (3.6e6 * total_floor_area), '.2f')), - str(format(building.cooling_demand[cte.YEAR][0] / (3.6e6 * total_floor_area), '.2f')), - str(format( - (building.lighting_electrical_demand[cte.YEAR][0] + building.appliances_electrical_demand[cte.YEAR][0]) / - (1e3 * total_floor_area), '.2f')) - ] - table_data.append(building_data) - intensity_table_data.append(intensity_data) - - self.report.add_table(table_data, caption='City Buildings Energy Demands') - self.report.add_table(intensity_table_data, caption='Energy Intensity Information') - - def base_case_charts(self): - save_directory = self.output_path - - def autolabel(bars, ax): - for bar in bars: - height = bar.get_height() - ax.annotate('{:.1f}'.format(height), - xy=(bar.get_x() + bar.get_width() / 2, height), - xytext=(0, 3), # 3 points vertical offset - textcoords="offset points", - ha='center', va='bottom') - - def create_hvac_demand_chart(building_names, yearly_heating_demand, yearly_cooling_demand): - fig, ax = plt.subplots() - bar_width = 0.35 - index = range(len(building_names)) - - bars1 = ax.bar(index, yearly_heating_demand, bar_width, label='Yearly Heating Demand (MWh)') - bars2 = ax.bar([i + bar_width for i in index], yearly_cooling_demand, bar_width, - label='Yearly Cooling Demand (MWh)') - - ax.set_xlabel('Building Name') - ax.set_ylabel('Energy Demand (MWh)') - ax.set_title('Yearly HVAC Demands') - ax.set_xticks([i + bar_width / 2 for i in index]) - ax.set_xticklabels(building_names, rotation=45, ha='right') - ax.legend() - autolabel(bars1, ax) - autolabel(bars2, ax) - fig.tight_layout() - plt.savefig(save_directory / 'hvac_demand_chart.jpg') - plt.close() - - def create_bar_chart(title, ylabel, data, filename, bar_color=None): - fig, ax = plt.subplots() - bar_width = 0.35 - index = range(len(building_names)) - - if bar_color is None: - # Generate a random color - bar_color = random.choice(list(mcolors.CSS4_COLORS.values())) - - bars = ax.bar(index, data, bar_width, label=ylabel, color=bar_color) - - ax.set_xlabel('Building Name') - ax.set_ylabel('Energy Demand (MWh)') - ax.set_title(title) - ax.set_xticks([i + bar_width / 2 for i in index]) - ax.set_xticklabels(building_names, rotation=45, ha='right') - ax.legend() - autolabel(bars, ax) - fig.tight_layout() - plt.savefig(save_directory / filename) - plt.close() - - building_names = [building.name for building in self.city.buildings] - yearly_heating_demand = [building.heating_demand[cte.YEAR][0] / 3.6e9 for building in self.city.buildings] - yearly_cooling_demand = [building.cooling_demand[cte.YEAR][0] / 3.6e9 for building in self.city.buildings] - yearly_dhw_demand = [building.domestic_hot_water_heat_demand[cte.YEAR][0] / 1e6 for building in - self.city.buildings] - yearly_electricity_demand = [(building.lighting_electrical_demand[cte.YEAR][0] + - building.appliances_electrical_demand[cte.YEAR][0]) / 1e6 for building in - self.city.buildings] - - create_hvac_demand_chart(building_names, yearly_heating_demand, yearly_cooling_demand) - create_bar_chart('Yearly DHW Demands', 'Energy Demand (MWh)', yearly_dhw_demand, 'dhw_demand_chart.jpg', ) - create_bar_chart('Yearly Electricity Demands', 'Energy Demand (MWh)', yearly_electricity_demand, - 'electricity_demand_chart.jpg') - - def maximum_monthly_hvac_chart(self): - save_directory = self.output_path - months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', - 'November', 'December'] - for building in self.city.buildings: - maximum_monthly_heating_load = [] - maximum_monthly_cooling_load = [] - fig, axs = plt.subplots(1, 2, figsize=(12, 6)) # Create a figure with 2 subplots side by side - for demand in building.heating_peak_load[cte.MONTH]: - maximum_monthly_heating_load.append(demand / 3.6e6) - for demand in building.cooling_peak_load[cte.MONTH]: - maximum_monthly_cooling_load.append(demand / 3.6e6) - - # Plot maximum monthly heating load - axs[0].bar(months, maximum_monthly_heating_load, color='red') # Plot on the first subplot - axs[0].set_title('Maximum Monthly Heating Load') - axs[0].set_xlabel('Month') - axs[0].set_ylabel('Load (kWh)') - axs[0].tick_params(axis='x', rotation=45) - - # Plot maximum monthly cooling load - axs[1].bar(months, maximum_monthly_cooling_load, color='blue') # Plot on the second subplot - axs[1].set_title('Maximum Monthly Cooling Load') - axs[1].set_xlabel('Month') - axs[1].set_ylabel('Load (kWh)') - axs[1].tick_params(axis='x', rotation=45) - - plt.tight_layout() # Adjust layout to prevent overlapping - plt.savefig(save_directory / f'{building.name}_monthly_maximum_hvac_loads.jpg') - plt.close() - - def load_duration_curves(self): - save_directory = self.output_path - for building in self.city.buildings: - heating_demand = [demand / 3.6e6 for demand in building.heating_demand[cte.HOUR]] - cooling_demand = [demand / 3.6e6 for demand in building.cooling_demand[cte.HOUR]] - heating_demand_sorted = sorted(heating_demand, reverse=True) - cooling_demand_sorted = sorted(cooling_demand, reverse=True) - - plt.style.use('ggplot') - - # Create figure and axis objects with 1 row and 2 columns - fig, axs = plt.subplots(1, 2, figsize=(12, 6)) - - # Plot sorted heating demand - axs[0].plot(heating_demand_sorted, color='red', linewidth=2, label='Heating Demand') - axs[0].set_xlabel('Hour', fontsize=14) - axs[0].set_ylabel('Heating Demand (kWh)', fontsize=14) - axs[0].set_title('Heating Load Duration Curve', fontsize=16) - axs[0].grid(True) - axs[0].legend(loc='upper right', fontsize=12) - - # Plot sorted cooling demand - axs[1].plot(cooling_demand_sorted, color='blue', linewidth=2, label='Cooling Demand') - axs[1].set_xlabel('Hour', fontsize=14) - axs[1].set_ylabel('Cooling Demand (kWh)', fontsize=14) - axs[1].set_title('Cooling Load Duration Curve', fontsize=16) - axs[1].grid(True) - axs[1].legend(loc='upper right', fontsize=12) - - # Adjust layout - plt.tight_layout() - plt.savefig(save_directory / f'{building.name}_load_duration_curve.jpg') - plt.close() - - def individual_building_info(self, building): - table_data = [ - ["Maximum Monthly HVAC Demands", - f"\\includegraphics[width=1\\linewidth]{{{building.name}_monthly_maximum_hvac_loads.jpg}}"], - ["Load Duration Curve", f"\\includegraphics[width=1\\linewidth]{{{building.name}_load_duration_curve.jpg}}"], - ] - - self.report.add_table(table_data, caption=f'{building.name} Information', first_column_width=1.5) - - def building_system_retrofit_results(self, building_name, current_system, new_system): - current_system_archetype = current_system[f'{building_name}']['Energy System Archetype'] - current_system_heating = current_system[f'{building_name}']['Heating Equipments'] - current_system_cooling = current_system[f'{building_name}']['Cooling Equipments'] - current_system_dhw = current_system[f'{building_name}']['DHW Equipments'] - current_system_pv = current_system[f'{building_name}']['Photovoltaic System Capacity'] - current_system_heating_fuel = current_system[f'{building_name}']['Heating Fuel'] - current_system_hvac_consumption = current_system[f'{building_name}']['Yearly HVAC Energy Consumption (MWh)'] - current_system_dhw_consumption = current_system[f'{building_name}']['DHW Energy Consumption (MWH)'] - current_pv_production = current_system[f'{building_name}']['PV Yearly Production (kWh)'] - current_capital_cost = current_system[f'{building_name}']['Energy System Capital Cost (CAD)'] - current_operational = current_system[f'{building_name}']['Energy System Average Yearly Operational Cost (CAD)'] - current_lcc = current_system[f'{building_name}']['Energy System Life Cycle Cost (CAD)'] - new_system_archetype = new_system[f'{building_name}']['Energy System Archetype'] - new_system_heating = new_system[f'{building_name}']['Heating Equipments'] - new_system_cooling = new_system[f'{building_name}']['Cooling Equipments'] - new_system_dhw = new_system[f'{building_name}']['DHW Equipments'] - new_system_pv = new_system[f'{building_name}']['Photovoltaic System Capacity'] - new_system_heating_fuel = new_system[f'{building_name}']['Heating Fuel'] - new_system_hvac_consumption = new_system[f'{building_name}']['Yearly HVAC Energy Consumption (MWh)'] - new_system_dhw_consumption = new_system[f'{building_name}']['DHW Energy Consumption (MWH)'] - new_pv_production = new_system[f'{building_name}']['PV Yearly Production (kWh)'] - new_capital_cost = new_system[f'{building_name}']['Energy System Capital Cost (CAD)'] - new_operational = new_system[f'{building_name}']['Energy System Average Yearly Operational Cost (CAD)'] - new_lcc = new_system[f'{building_name}']['Energy System Life Cycle Cost (CAD)'] - - energy_system_table_data = [ - ["Detail", "Existing System", "Proposed System"], - ["Energy System Archetype", current_system_archetype, new_system_archetype], - ["Heating Equipments", current_system_heating, new_system_heating], - ["Cooling Equipments", current_system_cooling, new_system_cooling], - ["DHW Equipments", current_system_dhw, new_system_dhw], - ["Photovoltaic System Capacity", current_system_pv, new_system_pv], - ["Heating Fuel", current_system_heating_fuel, new_system_heating_fuel], - ["Yearly HVAC Energy Consumption (MWh)", current_system_hvac_consumption, new_system_hvac_consumption], - ["DHW Energy Consumption (MWH)", current_system_dhw_consumption, new_system_dhw_consumption], - ["PV Yearly Production (kWh)", current_pv_production, new_pv_production], - ["Energy System Capital Cost (CAD)", current_capital_cost, new_capital_cost], - ["Energy System Average Yearly Operational Cost (CAD)", current_operational, new_operational], - ["Energy System Life Cycle Cost (CAD)", current_lcc, new_lcc] - ] - self.report.add_table(energy_system_table_data, caption=f'Building {building_name} Energy System Characteristics') - - def building_fuel_consumption_breakdown(self, building): - save_directory = self.output_path - # Initialize variables to store fuel consumption breakdown - fuel_breakdown = { - "Heating": {"Gas": 0, "Electricity": 0}, - "Domestic Hot Water": {"Gas": 0, "Electricity": 0}, - "Cooling": {"Electricity": 0}, - "Appliance": building.appliances_electrical_demand[cte.YEAR][0] / 1e6, - "Lighting": building.lighting_electrical_demand[cte.YEAR][0] / 1e6 - } - - # Iterate through energy systems of the building - for energy_system in building.energy_systems: - for demand_type in energy_system.demand_types: - if demand_type == cte.HEATING: - consumption = building.heating_consumption[cte.YEAR][0] / 3.6e9 - for generation_system in energy_system.generation_systems: - if generation_system.fuel_type == cte.ELECTRICITY: - fuel_breakdown[demand_type]["Electricity"] += consumption - else: - fuel_breakdown[demand_type]["Gas"] += consumption - elif demand_type == cte.DOMESTIC_HOT_WATER: - consumption = building.domestic_hot_water_consumption[cte.YEAR][0] / 1e6 - for generation_system in energy_system.generation_systems: - if generation_system.fuel_type == cte.ELECTRICITY: - fuel_breakdown[demand_type]["Electricity"] += consumption - else: - fuel_breakdown[demand_type]["Gas"] += consumption - elif demand_type == cte.COOLING: - consumption = building.cooling_consumption[cte.YEAR][0] / 3.6e9 - fuel_breakdown[demand_type]["Electricity"] += consumption - - electricity_labels = ['Appliance', 'Lighting'] - electricity_sizes = [fuel_breakdown['Appliance'], fuel_breakdown['Lighting']] - if fuel_breakdown['Heating']['Electricity'] > 0: - electricity_labels.append('Heating') - electricity_sizes.append(fuel_breakdown['Heating']['Electricity']) - if fuel_breakdown['Cooling']['Electricity'] > 0: - electricity_labels.append('Cooling') - electricity_sizes.append(fuel_breakdown['Cooling']['Electricity']) - if fuel_breakdown['Domestic Hot Water']['Electricity'] > 0: - electricity_labels.append('Domestic Hot Water') - electricity_sizes.append(fuel_breakdown['Domestic Hot Water']['Electricity']) - - # Data for bar chart - gas_labels = ['Heating', 'Domestic Hot Water'] - gas_sizes = [fuel_breakdown['Heating']['Gas'], fuel_breakdown['Domestic Hot Water']['Gas']] - - # Set the style - plt.style.use('ggplot') - - # Create plot grid - fig, axs = plt.subplots(1, 2, figsize=(12, 6)) - - # Plot pie chart for electricity consumption breakdown - colors = cm.get_cmap('tab20c', len(electricity_labels)) - axs[0].pie(electricity_sizes, labels=electricity_labels, - autopct=lambda pct: f"{pct:.1f}%\n({pct / 100 * sum(electricity_sizes):.2f})", - startangle=90, colors=[colors(i) for i in range(len(electricity_labels))]) - axs[0].set_title('Electricity Consumption Breakdown') - - # Plot bar chart for natural gas consumption breakdown - colors = cm.get_cmap('Paired', len(gas_labels)) - axs[1].bar(gas_labels, gas_sizes, color=[colors(i) for i in range(len(gas_labels))]) - axs[1].set_ylabel('Consumption (MWh)') - axs[1].set_title('Natural Gas Consumption Breakdown') - - # Add grid to bar chart - axs[1].grid(axis='y', linestyle='--', alpha=0.7) - - # Add a title to the entire figure - plt.suptitle('Building Energy Consumption Breakdown', fontsize=16, fontweight='bold') - - # Adjust layout - plt.tight_layout() - - # Save the plot as a high-quality image - plt.savefig(save_directory / f'{building.name}_energy_consumption_breakdown.png', dpi=300) - plt.close() - - def create_report(self, current_system, new_system): - os.chdir(self.output_path) - self.report.add_section('Current Status') - self.building_energy_info() - self.base_case_charts() - self.report.add_image('hvac_demand_chart.jpg', caption='Yearly HVAC Demands') - self.report.add_image('dhw_demand_chart.jpg', caption='Yearly DHW Demands') - self.report.add_image('electricity_demand_chart.jpg', caption='Yearly Electricity Demands') - self.maximum_monthly_hvac_chart() - self.load_duration_curves() - for building in self.city.buildings: - self.individual_building_info(building) - self.building_system_retrofit_results(building_name=building.name, current_system=current_system, new_system=new_system) - self.building_fuel_consumption_breakdown(building) - self.report.add_image(f'{building.name}_energy_consumption_breakdown.png', - caption=f'Building {building.name} Consumption by source and sector breakdown') - self.report.save_report() - self.report.compile_to_pdf() diff --git a/scripts/system_simulation.py b/scripts/system_simulation.py deleted file mode 100644 index a097d0b0..00000000 --- a/scripts/system_simulation.py +++ /dev/null @@ -1,135 +0,0 @@ -import csv -import math -from typing import List - -from pathlib import Path - -import hub.helpers.constants as cte -from hub.helpers.monthly_values import MonthlyValues - - -class SystemSimulation: - def __init__(self, building, out_path): - self.building = building - self.energy_systems = building.energy_systems - self.heating_demand = [0] + building.heating_demand[cte.HOUR] - self.cooling_demand = building.cooling_demand - self.dhw_demand = building.domestic_hot_water_heat_demand - self.T_out = building.external_temperature[cte.HOUR] - self.maximum_heating_demand = building.heating_peak_load[cte.YEAR][0] - self.maximum_cooling_demand = building.cooling_peak_load[cte.YEAR][0] - self.name = building.name - self.energy_system_archetype = building.energy_systems_archetype_name - self.out_path = out_path - - def archetype1(self): - out_path = self.out_path - T, T_sup, T_ret, m_ch, m_dis, q_hp, q_aux = [0] * len(self.heating_demand), [0] * len( - self.heating_demand), [0] * len(self.heating_demand), [0] * len(self.heating_demand), [0] * len( - self.heating_demand), [0] * len(self.heating_demand), [0] * len(self.heating_demand) - hp_electricity: List[float] = [0.0] * len(self.heating_demand) - aux_fuel: List[float] = [0.0] * len(self.heating_demand) - heating_consumption: List[float] = [0.0] * len(self.heating_demand) - boiler_consumption: List[float] = [0.0] * len(self.heating_demand) - T[0], dt = 25, 3600 # Assuming dt is defined somewhere - ua, v, hp_cap, hp_efficiency, boiler_efficiency = 0, 0, 0, 0, 0 - for energy_system in self.energy_systems: - if cte.ELECTRICITY not in energy_system.demand_types: - generation_systems = energy_system.generation_systems - for generation_system in generation_systems: - if generation_system.system_type == cte.HEAT_PUMP and cte.HEATING in energy_system.demand_types: - hp_cap = generation_system.nominal_heat_output - hp_efficiency = float(generation_system.heat_efficiency) - for storage in generation_system.energy_storage_systems: - if storage.type_energy_stored == 'thermal': - v, h = float(storage.volume), float(storage.height) - r_tot = sum(float(layer.thickness) / float(layer.material.conductivity) for layer in - storage.layers) - u_tot = 1 / r_tot - d = math.sqrt((4 * v) / (math.pi * h)) - a_side = math.pi * d * h - a_top = math.pi * d ** 2 / 4 - ua = u_tot * (2 * a_top + a_side) - elif generation_system.system_type == cte.BOILER: - boiler_cap = generation_system.nominal_heat_output - boiler_efficiency = float(generation_system.heat_efficiency) - - for i in range(len(self.heating_demand) - 1): - T[i + 1] = T[i] + ((m_ch[i] * (T_sup[i] - T[i])) + ( - ua * (self.T_out[i] - T[i])) / cte.WATER_HEAT_CAPACITY - m_dis[i] * (T[i] - T_ret[i])) * (dt / (cte.WATER_DENSITY * v)) - if T[i + 1] < 35: - q_hp[i + 1] = hp_cap * 1000 - m_ch[i + 1] = q_hp[i + 1] / (cte.WATER_HEAT_CAPACITY * 7) - T_sup[i + 1] = (q_hp[i + 1] / (m_ch[i + 1] * cte.WATER_HEAT_CAPACITY)) + T[i + 1] - elif 35 <= T[i + 1] < 45 and q_hp[i] == 0: - q_hp[i + 1] = 0 - m_ch[i + 1] = 0 - T_sup[i + 1] = T[i + 1] - elif 35 <= T[i + 1] < 45 and q_hp[i] > 0: - q_hp[i + 1] = hp_cap * 1000 - m_ch[i + 1] = q_hp[i + 1] / (cte.WATER_HEAT_CAPACITY * 3) - T_sup[i + 1] = (q_hp[i + 1] / (m_ch[i + 1] * cte.WATER_HEAT_CAPACITY)) + T[i + 1] - else: - q_hp[i + 1], m_ch[i + 1], T_sup[i + 1] = 0, 0, T[i + 1] - - hp_electricity[i + 1] = q_hp[i + 1] / hp_efficiency - if self.heating_demand[i + 1] == 0: - m_dis[i + 1], t_return, T_ret[i + 1] = 0, T[i + 1], T[i + 1] - else: - if self.heating_demand[i + 1] > 0.5 * self.maximum_heating_demand: - factor = 8 - else: - factor = 4 - m_dis[i + 1] = self.maximum_heating_demand / (cte.WATER_HEAT_CAPACITY * factor * 3600) - t_return = T[i + 1] - self.heating_demand[i + 1] / (m_dis[i + 1] * cte.WATER_HEAT_CAPACITY * 3600) - if m_dis[i + 1] == 0 or (m_dis[i + 1] > 0 and t_return < 25): - T_ret[i + 1] = max(25, T[i + 1]) - else: - T_ret[i + 1] = T[i + 1] - self.heating_demand[i + 1] / (m_dis[i + 1] * cte.WATER_HEAT_CAPACITY * 3600) - tes_output = m_dis[i + 1] * cte.WATER_HEAT_CAPACITY * (T[i + 1] - T_ret[i + 1]) - if tes_output < (self.heating_demand[i + 1] / 3600): - q_aux[i + 1] = (self.heating_demand[i + 1] / 3600) - tes_output - aux_fuel[i + 1] = (q_aux[i + 1] * dt) / 35.8e6 - boiler_consumption[i + 1] = q_aux[i + 1] / boiler_efficiency - heating_consumption[i + 1] = boiler_consumption[i + 1] + hp_electricity[i + 1] - data = list(zip(T, T_sup, T_ret, m_ch, m_dis, q_hp, hp_electricity, aux_fuel, q_aux, self.heating_demand)) - file_name = f'simulation_results_{self.name}.csv' - with open(out_path / file_name, 'w', newline='') as csvfile: - output_file = csv.writer(csvfile) - # Write header - output_file.writerow(['T', 'T_sup', 'T_ret', 'm_ch', 'm_dis', 'q_hp', 'hp_electricity', 'aux_fuel', 'q_aux', 'heating_demand']) - # Write data - output_file.writerows(data) - return heating_consumption, hp_electricity, boiler_consumption, T_sup - - def enrich(self): - if self.energy_system_archetype == 'PV+ASHP+GasBoiler+TES' or 'PV+4Pipe+DHW': - building_new_heating_consumption, building_heating_electricity_consumption, building_heating_gas_consumption, supply_temperature = ( - self.archetype1()) - self.building.heating_consumption[cte.HOUR] = building_new_heating_consumption - self.building.heating_consumption[cte.MONTH] = MonthlyValues.get_total_month(self.building.heating_consumption[cte.HOUR]) - self.building.heating_consumption[cte.YEAR] = [sum(self.building.heating_consumption[cte.MONTH])] - disaggregated_consumption = {} - for energy_system in self.building.energy_systems: - if cte.HEATING in energy_system.demand_types: - for generation_system in energy_system.generation_systems: - if generation_system.system_type == cte.HEAT_PUMP: - generation_system.heat_supply_temperature = supply_temperature - disaggregated_consumption[generation_system.fuel_type] = {} - if generation_system.fuel_type == cte.ELECTRICITY: - disaggregated_consumption[generation_system.fuel_type][ - cte.HOUR] = building_heating_electricity_consumption - disaggregated_consumption[generation_system.fuel_type][cte.MONTH] = MonthlyValues.get_total_month( - disaggregated_consumption[generation_system.fuel_type][cte.HOUR]) - disaggregated_consumption[generation_system.fuel_type][cte.YEAR] = [ - sum(disaggregated_consumption[generation_system.fuel_type][cte.MONTH])] - else: - disaggregated_consumption[generation_system.fuel_type][cte.HOUR] = building_heating_gas_consumption - disaggregated_consumption[generation_system.fuel_type][cte.MONTH] = MonthlyValues.get_total_month( - disaggregated_consumption[generation_system.fuel_type][cte.HOUR]) - disaggregated_consumption[generation_system.fuel_type][cte.YEAR] = [ - sum(disaggregated_consumption[generation_system.fuel_type][cte.MONTH])] - self.building.heating_fuel_consumption_disaggregated = disaggregated_consumption - return self.building - - From 93ab78b34ea78fdfa51b3d0d4d5630ba961a5253 Mon Sep 17 00:00:00 2001 From: s_ranjbar Date: Mon, 22 Jul 2024 08:09:14 -0400 Subject: [PATCH 14/15] fix: importers are fixed for mixed use buildings --- .idea/misc.xml | 3 + .../construction/nrcan_physics_parameters.py | 17 +++- hub/imports/geometry/geojson.py | 13 +++ hub/imports/usage/comnet_usage_parameters.py | 75 ++++++++++----- hub/imports/usage/nrcan_usage_parameters.py | 73 +++++++++------ input_files/test_geojson.geojson | 55 +++++++++++ main.py | 86 ++++++++++++++++++ plot_nrcan.png | Bin 0 -> 47568 bytes 8 files changed, 270 insertions(+), 52 deletions(-) create mode 100644 input_files/test_geojson.geojson create mode 100644 plot_nrcan.png diff --git a/.idea/misc.xml b/.idea/misc.xml index dc7953c5..757b558a 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,7 @@ + + \ No newline at end of file diff --git a/hub/imports/construction/nrcan_physics_parameters.py b/hub/imports/construction/nrcan_physics_parameters.py index e57aa22e..e39774e7 100644 --- a/hub/imports/construction/nrcan_physics_parameters.py +++ b/hub/imports/construction/nrcan_physics_parameters.py @@ -32,10 +32,21 @@ class NrcanPhysicsParameters: city = self._city nrcan_catalog = ConstructionCatalogFactory('nrcan').catalog for building in city.buildings: - if building.function not in Dictionaries().hub_function_to_nrcan_construction_function: - logging.error('Building %s has an unknown building function %s', building.name, building.function) + main_function = None + functions = building.function.split('_') + if len(functions) > 1: + maximum_percentage = 0 + for function in functions: + percentage_and_function = function.split('-') + if float(percentage_and_function[0]) > maximum_percentage: + maximum_percentage = float(percentage_and_function[0]) + main_function = percentage_and_function[-1] + else: + main_function = functions[-1] + if main_function not in Dictionaries().hub_function_to_nrcan_construction_function: + logging.error('Building %s has an unknown building function %s', building.name, main_function) continue - function = Dictionaries().hub_function_to_nrcan_construction_function[building.function] + function = Dictionaries().hub_function_to_nrcan_construction_function[main_function] try: archetype = self._search_archetype(nrcan_catalog, function, building.year_of_construction, self._climate_zone) diff --git a/hub/imports/geometry/geojson.py b/hub/imports/geometry/geojson.py index b07a6a7b..3505c27a 100644 --- a/hub/imports/geometry/geojson.py +++ b/hub/imports/geometry/geojson.py @@ -127,6 +127,19 @@ class Geojson: function = None if self._function_field is not None: function = str(feature['properties'][self._function_field]) + if function == 'Mixed use' or function == 'mixed use': + function_parts = [] + for key, value in feature['properties'].items(): + if key.startswith("mixed_type_") and not key.endswith("_percentage"): + type_key = key + percentage_key = f"{key}_percentage" + if percentage_key in feature['properties']: + if self._function_to_hub is not None and feature['properties'][type_key] in self._function_to_hub: + usage_function = self._function_to_hub[feature['properties'][type_key]] + function_parts.append(f"{feature['properties'][percentage_key]}-{usage_function}") + else: + function_parts.append(f"{feature['properties'][percentage_key]}-{feature['properties'][type_key]}") + function = "_".join(function_parts) if self._function_to_hub is not None: # use the transformation dictionary to retrieve the proper function if function in self._function_to_hub: diff --git a/hub/imports/usage/comnet_usage_parameters.py b/hub/imports/usage/comnet_usage_parameters.py index 02f9b77e..08d8175c 100644 --- a/hub/imports/usage/comnet_usage_parameters.py +++ b/hub/imports/usage/comnet_usage_parameters.py @@ -35,29 +35,60 @@ class ComnetUsageParameters: city = self._city comnet_catalog = UsageCatalogFactory('comnet').catalog for building in city.buildings: - usage_name = Dictionaries().hub_usage_to_comnet_usage[building.function] - try: - archetype_usage = self._search_archetypes(comnet_catalog, usage_name) - except KeyError: - logging.error('Building %s has unknown usage archetype for usage %s', building.name, usage_name) - continue - - for internal_zone in building.internal_zones: - if internal_zone.area is None: - raise TypeError('Internal zone area not defined, ACH cannot be calculated') - if internal_zone.volume is None: - raise TypeError('Internal zone volume not defined, ACH cannot be calculated') - if internal_zone.area <= 0: - raise TypeError('Internal zone area is zero, ACH cannot be calculated') - volume_per_area = internal_zone.volume / internal_zone.area - usage = Usage() - usage.name = usage_name - self._assign_values(usage, archetype_usage, volume_per_area, building.cold_water_temperature) - usage.percentage = 1 - self._calculate_reduced_values_from_extended_library(usage, archetype_usage) - - internal_zone.usages = [usage] + usages = [] + comnet_archetype_usages = [] + building_functions = building.function.split('_') + for function in building_functions: + usages.append(function.split('-')) + for usage in usages: + comnet_usage_name = Dictionaries().hub_usage_to_comnet_usage[usage[-1]] + try: + comnet_archetype_usage = self._search_archetypes(comnet_catalog, comnet_usage_name) + comnet_archetype_usages.append(comnet_archetype_usage) + except KeyError: + logging.error('Building %s has unknown usage archetype for usage %s', building.name, comnet_usage_name) + continue + for (i, internal_zone) in enumerate(building.internal_zones): + internal_zone_usages = [] + if len(building.internal_zones) > 1: + volume_per_area = 0 + if internal_zone.area is None: + logging.error('Building %s has internal zone area not defined, ACH cannot be calculated for usage %s', + building.name, usages[i][-1]) + continue + if internal_zone.volume is None: + logging.error('Building %s has internal zone volume not defined, ACH cannot be calculated for usage %s', + building.name, usages[i][-1]) + continue + if internal_zone.area <= 0: + logging.error('Building %s has internal zone area equal to 0, ACH cannot be calculated for usage %s', + building.name, usages[i][-1]) + continue + volume_per_area += internal_zone.volume / internal_zone.area + usage = Usage() + usage.name = usages[i][-1] + self._assign_values(usage, comnet_archetype_usages[i], volume_per_area, building.cold_water_temperature) + usage.percentage = 1 + self._calculate_reduced_values_from_extended_library(usage, comnet_archetype_usages[i]) + internal_zone_usages.append(usage) + else: + if building.storeys_above_ground is None: + logging.error('Building %s no number of storeys assigned, ACH cannot be calculated for usage %s', + building.name, usages) + continue + volume_per_area = building.volume / building.floor_area / building.storeys_above_ground + for (j, mixed_usage) in enumerate(usages): + usage = Usage() + usage.name = mixed_usage[-1] + if len(usages) > 1: + usage.percentage = float(mixed_usage[0]) / 100 + else: + usage.percentage = 1 + self._assign_values(usage, comnet_archetype_usages[j], volume_per_area, building.cold_water_temperature) + self._calculate_reduced_values_from_extended_library(usage, comnet_archetype_usages[j]) + internal_zone_usages.append(usage) + internal_zone.usages = internal_zone_usages @staticmethod def _search_archetypes(comnet_catalog, usage_name): comnet_archetypes = comnet_catalog.entries('archetypes').usages diff --git a/hub/imports/usage/nrcan_usage_parameters.py b/hub/imports/usage/nrcan_usage_parameters.py index 9d718d33..4ff99a29 100644 --- a/hub/imports/usage/nrcan_usage_parameters.py +++ b/hub/imports/usage/nrcan_usage_parameters.py @@ -33,53 +33,72 @@ class NrcanUsageParameters: city = self._city nrcan_catalog = UsageCatalogFactory('nrcan').catalog comnet_catalog = UsageCatalogFactory('comnet').catalog - for building in city.buildings: - usage_name = Dictionaries().hub_usage_to_nrcan_usage[building.function] - try: - archetype_usage = self._search_archetypes(nrcan_catalog, usage_name) - except KeyError: - logging.error('Building %s has unknown usage archetype for usage %s', building.name, usage_name) - continue + usages = [] + nrcan_archetype_usages = [] + comnet_archetype_usages = [] + building_functions = building.function.split('_') + for function in building_functions: + usages.append(function.split('-')) + for usage in usages: + usage_name = Dictionaries().hub_usage_to_nrcan_usage[usage[-1]] + try: + archetype_usage = self._search_archetypes(nrcan_catalog, usage_name) + nrcan_archetype_usages.append(archetype_usage) + except KeyError: + logging.error('Building %s has unknown usage archetype for usage %s', building.name, usage_name) + continue + comnet_usage_name = Dictionaries().hub_usage_to_comnet_usage[usage[-1]] + try: + comnet_archetype_usage = self._search_archetypes(comnet_catalog, comnet_usage_name) + comnet_archetype_usages.append(comnet_archetype_usage) + except KeyError: + logging.error('Building %s has unknown usage archetype for usage %s', building.name, comnet_usage_name) + continue - comnet_usage_name = Dictionaries().hub_usage_to_comnet_usage[building.function] - try: - comnet_archetype_usage = self._search_archetypes(comnet_catalog, comnet_usage_name) - except KeyError: - logging.error('Building %s has unknown usage archetype for usage %s', building.name, comnet_usage_name) - continue - - for internal_zone in building.internal_zones: + for (i, internal_zone) in enumerate(building.internal_zones): + internal_zone_usages = [] if len(building.internal_zones) > 1: volume_per_area = 0 if internal_zone.area is None: logging.error('Building %s has internal zone area not defined, ACH cannot be calculated for usage %s', - building.name, usage_name) + building.name, usages[i][-1]) continue if internal_zone.volume is None: logging.error('Building %s has internal zone volume not defined, ACH cannot be calculated for usage %s', - building.name, usage_name) + building.name, usages[i][-1]) continue if internal_zone.area <= 0: logging.error('Building %s has internal zone area equal to 0, ACH cannot be calculated for usage %s', - building.name, usage_name) + building.name, usages[i][-1]) continue volume_per_area += internal_zone.volume / internal_zone.area + usage = Usage() + usage.name = usages[i][-1] + self._assign_values(usage, nrcan_archetype_usages[i], volume_per_area, building.cold_water_temperature) + self._assign_comnet_extra_values(usage, comnet_archetype_usages[i], nrcan_archetype_usages[i].occupancy.occupancy_density) + usage.percentage = 1 + self._calculate_reduced_values_from_extended_library(usage, nrcan_archetype_usages[i]) + internal_zone_usages.append(usage) else: if building.storeys_above_ground is None: logging.error('Building %s no number of storeys assigned, ACH cannot be calculated for usage %s', - building.name, usage_name) + building.name, usages) continue volume_per_area = building.volume / building.floor_area / building.storeys_above_ground + for (j, mixed_usage) in enumerate(usages): + usage = Usage() + usage.name = mixed_usage[-1] + if len(usages) > 1: + usage.percentage = float(mixed_usage[0]) / 100 + else: + usage.percentage = 1 + self._assign_values(usage, nrcan_archetype_usages[j], volume_per_area, building.cold_water_temperature) + self._assign_comnet_extra_values(usage, comnet_archetype_usages[j], nrcan_archetype_usages[j].occupancy.occupancy_density) + self._calculate_reduced_values_from_extended_library(usage, nrcan_archetype_usages[j]) + internal_zone_usages.append(usage) - usage = Usage() - usage.name = usage_name - self._assign_values(usage, archetype_usage, volume_per_area, building.cold_water_temperature) - self._assign_comnet_extra_values(usage, comnet_archetype_usage, archetype_usage.occupancy.occupancy_density) - usage.percentage = 1 - self._calculate_reduced_values_from_extended_library(usage, archetype_usage) - - internal_zone.usages = [usage] + internal_zone.usages = internal_zone_usages @staticmethod def _search_archetypes(catalog, usage_name): diff --git a/input_files/test_geojson.geojson b/input_files/test_geojson.geojson new file mode 100644 index 00000000..df9f0c4c --- /dev/null +++ b/input_files/test_geojson.geojson @@ -0,0 +1,55 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.58000127109773, + 45.49613461675315 + ], + [ + -73.57962787855432, + 45.496524875557746 + ], + [ + -73.57996357265695, + 45.49668114195629 + ], + [ + -73.57996427397713, + 45.496680342403664 + ], + [ + -73.58034707390021, + 45.49625804233725 + ], + [ + -73.58034697395713, + 45.496257942524835 + ], + [ + -73.58000127109773, + 45.49613461675315 + ] + ] + ] + }, + "id": 179764, + "properties": { + "name": "01119274", + "address": "rue Guy (MTL) 2157", + "function": "Mixed use", + "mixed_type_1": "commercial", + "mixed_type_1_percentage": 50, + "mixed_type_2": "6000", + "mixed_type_2_percentage": 50, + "height": 62, + "year_of_construction": 1954 + } + } + ] +} \ No newline at end of file diff --git a/main.py b/main.py index e69de29b..edce68d2 100644 --- a/main.py +++ b/main.py @@ -0,0 +1,86 @@ +from pathlib import Path +import subprocess +from scripts.ep_run_enrich import energy_plus_workflow +from hub.imports.geometry_factory import GeometryFactory +from hub.helpers.dictionaries import Dictionaries +from hub.imports.construction_factory import ConstructionFactory +from hub.imports.usage_factory import UsageFactory +from hub.imports.weather_factory import WeatherFactory +from hub.imports.results_factory import ResultFactory +from scripts.energy_system_retrofit_report import EnergySystemRetrofitReport +from scripts.geojson_creator import process_geojson +from scripts import random_assignation +from hub.imports.energy_systems_factory import EnergySystemsFactory +from scripts.energy_system_sizing import SystemSizing +from scripts.solar_angles import CitySolarAngles +from scripts.pv_sizing_and_simulation import PVSizingSimulation +from scripts.energy_system_retrofit_results import consumption_data, cost_data +from scripts.energy_system_sizing_and_simulation_factory import EnergySystemsSimulationFactory +from scripts.costs.cost import Cost +from scripts.costs.constants import SKIN_RETROFIT_AND_SYSTEM_RETROFIT_AND_PV, SYSTEM_RETROFIT_AND_PV, CURRENT_STATUS +import hub.helpers.constants as cte +from hub.exports.exports_factory import ExportsFactory +from scripts.pv_feasibility import pv_feasibility +import matplotlib.pyplot as plt +import numpy as np +# Specify the GeoJSON file path +data = {} +input_files_path = (Path(__file__).parent / 'input_files') +input_files_path.mkdir(parents=True, exist_ok=True) +# geojson_file = process_geojson(x=-73.58001358793511, y=45.496445294438715, diff=0.0001) +geojson_file_path = input_files_path / 'test_geojson.geojson' +output_path = (Path(__file__).parent / 'out_files').resolve() +output_path.mkdir(parents=True, exist_ok=True) +energy_plus_output_path = output_path / 'energy_plus_outputs' +energy_plus_output_path.mkdir(parents=True, exist_ok=True) +simulation_results_path = (Path(__file__).parent / 'out_files' / 'simulation_results').resolve() +simulation_results_path.mkdir(parents=True, exist_ok=True) +sra_output_path = output_path / 'sra_outputs' +sra_output_path.mkdir(parents=True, exist_ok=True) +cost_analysis_output_path = output_path / 'cost_analysis' +cost_analysis_output_path.mkdir(parents=True, exist_ok=True) +city = GeometryFactory(file_type='geojson', + path=geojson_file_path, + height_field='height', + year_of_construction_field='year_of_construction', + function_field='function', + function_to_hub=Dictionaries().montreal_function_to_hub_function).city +ConstructionFactory('nrcan', city).enrich() +UsageFactory('nrcan', city).enrich() +WeatherFactory('epw', city).enrich() +energy_plus_workflow(city, energy_plus_output_path) +data[f'{city.buildings[0].function}'] = city.buildings[0].heating_demand[cte.YEAR][0] / 3.6e9 +city.buildings[0].function = cte.COMMERCIAL +ConstructionFactory('nrcan', city).enrich() +UsageFactory('nrcan', city).enrich() +energy_plus_workflow(city, energy_plus_output_path) +data[f'{city.buildings[0].function}'] = city.buildings[0].heating_demand[cte.YEAR][0] / 3.6e9 +city.buildings[0].function = cte.MEDIUM_OFFICE +ConstructionFactory('nrcan', city).enrich() +UsageFactory('nrcan', city).enrich() +energy_plus_workflow(city, energy_plus_output_path) +data[f'{city.buildings[0].function}'] = city.buildings[0].heating_demand[cte.YEAR][0] / 3.6e9 +categories = list(data.keys()) +values = list(data.values()) +# Plotting +fig, ax = plt.subplots(figsize=(10, 6), dpi=96) +fig.suptitle('Impact of different usages on yearly heating demand', fontsize=16, weight='bold', alpha=.8) +ax.bar(categories, values, color=['#2196f3', '#ff5a5f', '#4caf50'], width=0.6, zorder=2) +ax.grid(which="major", axis='x', color='#DAD8D7', alpha=0.5, zorder=1) +ax.grid(which="major", axis='y', color='#DAD8D7', alpha=0.5, zorder=1) +ax.set_xlabel('Building Type', fontsize=12, labelpad=10) +ax.set_ylabel('Energy Consumption (MWh)', fontsize=14, labelpad=10) +ax.yaxis.set_major_locator(plt.MaxNLocator(integer=True)) +ax.set_xticks(np.arange(len(categories))) +ax.set_xticklabels(categories, rotation=45, ha='right') +ax.bar_label(ax.containers[0], padding=3, color='black', fontsize=12, rotation=0) +ax.spines[['top', 'left', 'bottom']].set_visible(False) +ax.spines['right'].set_linewidth(1.1) +# Set a white background +fig.patch.set_facecolor('white') +# Adjust the margins around the plot area +plt.subplots_adjust(left=0.1, right=0.9, top=0.85, bottom=0.25) +# Save the plot +plt.savefig('plot_nrcan.png', bbox_inches='tight') +plt.close() +print('test') \ No newline at end of file diff --git a/plot_nrcan.png b/plot_nrcan.png new file mode 100644 index 0000000000000000000000000000000000000000..c780de3f1e96c7a0462569834f4f66216736a0df GIT binary patch literal 47568 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 literal 0 HcmV?d00001 From 96711ad41ef0d36a6b5cdadd4d67e817760f13c5 Mon Sep 17 00:00:00 2001 From: s_ranjbar Date: Mon, 22 Jul 2024 17:37:40 -0400 Subject: [PATCH 15/15] fix: small bug is cooling simulation and operational income are fixed --- .../cost/montreal_complete_cost_catalog.py | 2 +- scripts/costs/total_operational_incomes.py | 3 +- scripts/energy_system_retrofit_report.py | 21 +++--- scripts/energy_system_retrofit_results.py | 2 +- scripts/random_assignation.py | 8 +-- .../system_simulation_models/archetype13.py | 2 +- simulation_result_test.py | 67 +++++++++++++++++++ 7 files changed, 86 insertions(+), 19 deletions(-) create mode 100644 simulation_result_test.py diff --git a/hub/catalog_factories/cost/montreal_complete_cost_catalog.py b/hub/catalog_factories/cost/montreal_complete_cost_catalog.py index 5e3927ee..0c51bfd2 100644 --- a/hub/catalog_factories/cost/montreal_complete_cost_catalog.py +++ b/hub/catalog_factories/cost/montreal_complete_cost_catalog.py @@ -48,7 +48,7 @@ class MontrealNewCatalog(Catalog): construction = float(archetype['incomes']['subsidies']['construction']['#text']) hvac = float(archetype['incomes']['subsidies']['hvac']['#text']) photovoltaic_system = float(archetype['incomes']['subsidies']['photovoltaic']['#text']) - electricity_exports = float(archetype['incomes']['electricity_export']['#text']) / 1000 / 3600 + electricity_exports = float(archetype['incomes']['electricity_export']['#text']) reduction_tax = float(archetype['incomes']['tax_reduction']['#text']) / 100 income = Income(construction_subsidy=construction, hvac_subsidy=hvac, diff --git a/scripts/costs/total_operational_incomes.py b/scripts/costs/total_operational_incomes.py index 66d789ed..f3a9f8ac 100644 --- a/scripts/costs/total_operational_incomes.py +++ b/scripts/costs/total_operational_incomes.py @@ -33,12 +33,11 @@ class TotalOperationalIncomes(CostBase): onsite_electricity_production = 0 else: onsite_electricity_production = building.onsite_electrical_production[cte.YEAR][0] - for year in range(1, self._configuration.number_of_years + 1): price_increase_electricity = math.pow(1 + self._configuration.electricity_price_index, year) price_export = archetype.income.electricity_export # to account for unit change self._yearly_operational_incomes.loc[year, 'Incomes electricity'] = ( - (onsite_electricity_production / cte.WATTS_HOUR_TO_JULES) * price_export * price_increase_electricity + (onsite_electricity_production / 3.6e6) * price_export * price_increase_electricity ) self._yearly_operational_incomes.fillna(0, inplace=True) diff --git a/scripts/energy_system_retrofit_report.py b/scripts/energy_system_retrofit_report.py index fcfb764f..738437c9 100644 --- a/scripts/energy_system_retrofit_report.py +++ b/scripts/energy_system_retrofit_report.py @@ -95,7 +95,6 @@ class EnergySystemRetrofitReport: ax.bar(months, values, color=color, width=0.6, zorder=2) ax.grid(which="major", axis='x', color='#DAD8D7', alpha=0.5, zorder=1) ax.grid(which="major", axis='y', color='#DAD8D7', alpha=0.5, zorder=1) - ax.set_xlabel('Month', fontsize=12, labelpad=10) ax.set_ylabel(ylabel, fontsize=14, labelpad=10) ax.set_title(title, fontsize=14, weight='bold', alpha=.8, pad=40) ax.xaxis.set_major_locator(MaxNLocator(integer=True)) @@ -444,6 +443,8 @@ class EnergySystemRetrofitReport: retrofitted_opex = 0 retrofitted_maintenance = 0 retrofitted_end_of_life = 0 + current_status_operational_income = 0 + retrofitted_operational_income = 0 for building in self.city.buildings: current_status_capex += self.current_status_lcc[f'{building.name}']['capital_cost_per_sqm'] @@ -454,25 +455,25 @@ class EnergySystemRetrofitReport: retrofitted_maintenance += self.retrofitted_lcc[f'{building.name}']['maintenance_cost_per_sqm'] current_status_end_of_life += self.current_status_lcc[f'{building.name}']['end_of_life_cost_per_sqm'] retrofitted_end_of_life += self.retrofitted_lcc[f'{building.name}']['end_of_life_cost_per_sqm'] + current_status_operational_income += self.current_status_lcc[f'{building.name}']['operational_income_per_sqm'] + retrofitted_operational_income += self.retrofitted_lcc[f'{building.name}']['operational_income_per_sqm'] current_status_lcc_components_sqm = { 'Capital Cost': current_status_capex / len(self.city.buildings), - 'Operational Cost': current_status_opex / len(self.city.buildings), + 'Operational Cost': (current_status_opex - current_status_operational_income) / len(self.city.buildings), 'Maintenance Cost': current_status_maintenance / len(self.city.buildings), - 'End of Life Cost': current_status_end_of_life / len(self.city.buildings) + 'End of Life Cost': current_status_end_of_life / len(self.city.buildings), } retrofitted_lcc_components_sqm = { 'Capital Cost': retrofitted_capex / len(self.city.buildings), - 'Operational Cost': retrofitted_opex / len(self.city.buildings), + 'Operational Cost': (retrofitted_opex - retrofitted_operational_income) / len(self.city.buildings), 'Maintenance Cost': retrofitted_maintenance / len(self.city.buildings), - 'End of Life Cost': retrofitted_end_of_life / len(self.city.buildings) + 'End of Life Cost': retrofitted_end_of_life / len(self.city.buildings), } labels = ['Current Status', 'Retrofitted Status'] categories = ['Capital Cost', 'Operational Cost', 'Maintenance Cost', 'End of Life Cost'] - current_values = list(current_status_lcc_components_sqm.values()) - retrofitted_values = list(retrofitted_lcc_components_sqm.values()) - colors = ['#2196f3', '#ff5a5f', '#4caf50', '#ffc107'] + colors = ['#2196f3', '#ff5a5f', '#4caf50', '#ffc107'] # Added new color # Data preparation bar_width = 0.35 @@ -482,8 +483,8 @@ class EnergySystemRetrofitReport: fig.suptitle(title, fontsize=16, weight='bold', alpha=.8) # Plotting current status data - bottom = np.zeros(2) - for i, (category, color) in enumerate(zip(categories, colors)): + bottom = np.zeros(len(labels)) + for category, color in zip(categories, colors): values = [current_status_lcc_components_sqm[category], retrofitted_lcc_components_sqm[category]] ax.bar(r, values, bottom=bottom, color=color, edgecolor='white', width=bar_width, label=category) bottom += values diff --git a/scripts/energy_system_retrofit_results.py b/scripts/energy_system_retrofit_results.py index 9d85d0d9..1b84601a 100644 --- a/scripts/energy_system_retrofit_results.py +++ b/scripts/energy_system_retrofit_results.py @@ -104,7 +104,7 @@ def consumption_data(city): dhw_demand += building.domestic_hot_water_heat_demand[cte.MONTH][i] / 3.6e6 lighting_appliance_demand += building.lighting_electrical_demand[cte.MONTH][i] / 3.6e6 heating_consumption += building.heating_consumption[cte.MONTH][i] / 3.6e6 - if building.cooling_demand[cte.YEAR][0] == 0: + if building.cooling_consumption[cte.YEAR][0] == 0: cooling_consumption += building.cooling_demand[cte.MONTH][i] / (3.6e6 * 2) else: cooling_consumption += building.cooling_consumption[cte.MONTH][i] / 3.6e6 diff --git a/scripts/random_assignation.py b/scripts/random_assignation.py index ca7678bc..7483cced 100644 --- a/scripts/random_assignation.py +++ b/scripts/random_assignation.py @@ -15,8 +15,8 @@ from hub.city_model_structure.building import Building energy_systems_format = 'montreal_custom' # parameters: -residential_systems_percentage = {'system 1 gas': 44, - 'system 1 electricity': 6, +residential_systems_percentage = {'system 1 gas': 100, + 'system 1 electricity': 0, 'system 2 gas': 0, 'system 2 electricity': 0, 'system 3 and 4 gas': 0, @@ -25,8 +25,8 @@ residential_systems_percentage = {'system 1 gas': 44, 'system 5 electricity': 0, 'system 6 gas': 0, 'system 6 electricity': 0, - 'system 8 gas': 44, - 'system 8 electricity': 6} + 'system 8 gas': 0, + 'system 8 electricity': 0} residential_new_systems_percentage = {'PV+ASHP+GasBoiler+TES': 0, 'PV+4Pipe+DHW': 100, diff --git a/scripts/system_simulation_models/archetype13.py b/scripts/system_simulation_models/archetype13.py index 77b52da6..aa9c03bd 100644 --- a/scripts/system_simulation_models/archetype13.py +++ b/scripts/system_simulation_models/archetype13.py @@ -190,7 +190,7 @@ class Archetype13: t_ret[0] = 13 for i in range(1, len(demand)): - if demand[i] > 0: + if demand[i] > 0.15 * self._cooling_peak_load: m[i] = hp.nominal_cooling_output / (cte.WATER_HEAT_CAPACITY * 5) if t_ret[i - 1] >= 13: if demand[i] < 0.25 * self._cooling_peak_load: diff --git a/simulation_result_test.py b/simulation_result_test.py new file mode 100644 index 00000000..a54477b4 --- /dev/null +++ b/simulation_result_test.py @@ -0,0 +1,67 @@ +from pathlib import Path +import subprocess +from scripts.ep_run_enrich import energy_plus_workflow +from hub.imports.geometry_factory import GeometryFactory +from hub.helpers.dictionaries import Dictionaries +from hub.imports.construction_factory import ConstructionFactory +from hub.imports.usage_factory import UsageFactory +from hub.imports.weather_factory import WeatherFactory +from hub.imports.results_factory import ResultFactory +from scripts.energy_system_retrofit_report import EnergySystemRetrofitReport +from scripts.geojson_creator import process_geojson +from scripts import random_assignation +from hub.imports.energy_systems_factory import EnergySystemsFactory +from scripts.energy_system_sizing import SystemSizing +from scripts.solar_angles import CitySolarAngles +from scripts.pv_sizing_and_simulation import PVSizingSimulation +from scripts.energy_system_retrofit_results import consumption_data, cost_data +from scripts.energy_system_sizing_and_simulation_factory import EnergySystemsSimulationFactory +from scripts.costs.cost import Cost +from scripts.costs.constants import SKIN_RETROFIT_AND_SYSTEM_RETROFIT_AND_PV, SYSTEM_RETROFIT_AND_PV, CURRENT_STATUS +import hub.helpers.constants as cte +from hub.exports.exports_factory import ExportsFactory +from scripts.pv_feasibility import pv_feasibility + +# Specify the GeoJSON file path +input_files_path = (Path(__file__).parent / 'input_files') +input_files_path.mkdir(parents=True, exist_ok=True) +geojson_file = process_geojson(x=-73.5681295982132, y=45.49218262677643, diff=0.0001) +geojson_file_path = input_files_path / 'output_buildings.geojson' +output_path = (Path(__file__).parent / 'out_files').resolve() +output_path.mkdir(parents=True, exist_ok=True) +energy_plus_output_path = output_path / 'energy_plus_outputs' +energy_plus_output_path.mkdir(parents=True, exist_ok=True) +simulation_results_path = (Path(__file__).parent / 'out_files' / 'simulation_results').resolve() +simulation_results_path.mkdir(parents=True, exist_ok=True) +sra_output_path = output_path / 'sra_outputs' +sra_output_path.mkdir(parents=True, exist_ok=True) +cost_analysis_output_path = output_path / 'cost_analysis' +cost_analysis_output_path.mkdir(parents=True, exist_ok=True) +city = GeometryFactory(file_type='geojson', + path=geojson_file_path, + height_field='height', + year_of_construction_field='year_of_construction', + function_field='function', + function_to_hub=Dictionaries().montreal_function_to_hub_function).city +ConstructionFactory('nrcan', city).enrich() +UsageFactory('nrcan', city).enrich() +WeatherFactory('epw', city).enrich() +energy_plus_workflow(city, energy_plus_output_path) +random_assignation.call_random(city.buildings, random_assignation.residential_systems_percentage) +EnergySystemsFactory('montreal_custom', city).enrich() +SystemSizing(city.buildings).montreal_custom() +for i in range(12): + monthly_cooling = 0 + for building in city.buildings: + monthly_cooling += building.cooling_consumption[cte.MONTH][i] / (cte.WATTS_HOUR_TO_JULES * 1000) + print(monthly_cooling) +random_assignation.call_random(city.buildings, random_assignation.residential_new_systems_percentage) +EnergySystemsFactory('montreal_future', city).enrich() +for building in city.buildings: + if building.energy_systems_archetype_name == 'PV+4Pipe+DHW': + EnergySystemsSimulationFactory('archetype13', building=building, output_path=simulation_results_path).enrich() +for i in range(12): + monthly_cooling = 0 + for building in city.buildings: + monthly_cooling += building.cooling_consumption[cte.MONTH][i] / (cte.WATTS_HOUR_TO_JULES * 1000) + print(monthly_cooling)