diff --git a/base_case_modelling.py b/base_case_modelling.py new file mode 100644 index 00000000..056e843a --- /dev/null +++ b/base_case_modelling.py @@ -0,0 +1,80 @@ +from hub.imports.energy_systems_factory import EnergySystemsFactory +from hub.imports.results_factory import ResultFactory +from pathlib import Path +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 +import json +import hub.helpers.constants as cte +from scripts.energy_system_sizing_and_simulation_factory import EnergySystemsSimulationFactory + +# Specify the GeoJSON file path +input_files_path = (Path(__file__).parent / 'input_files') +geojson_file_path = input_files_path / 'omhm_selected_buildings.geojson' +output_path = (Path(__file__).parent / 'out_files').resolve() +output_path.mkdir(parents=True, exist_ok=True) +ep_output_path = output_path / 'ep_outputs' +ep_output_path.mkdir(parents=True, exist_ok=True) +# Create city object from GeoJSON file +city = GeometryFactory('geojson', + path=geojson_file_path, + height_field='Hieght_LiD', + year_of_construction_field='ANNEE_CONS', + function_field='CODE_UTILI', + 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() +ResultFactory('energy_plus_multiple_buildings', city, ep_output_path).enrich() +# for building in city.buildings: +# building.energy_systems_archetype_name = 'system 7 electricity pv' +# EnergySystemsFactory('montreal_custom', city).enrich() +for building in city.buildings: + building.energy_systems_archetype_name = 'PV+4Pipe+DHW' +EnergySystemsFactory('montreal_future', city).enrich() +for building in city.buildings: + EnergySystemsSimulationFactory('archetype13', building=building, output_path=output_path).enrich() +month_names = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] +building_data = {} +for building in city.buildings: + building_data[f'building_{building.name}'] = {'id': building.name, + 'total_floor_area': + building.thermal_zones_from_internal_zones[0].total_floor_area, + 'yearly_heating_consumption_kWh': + building.heating_consumption[cte.YEAR][0] / 3.6e6, + 'yearly_cooling_consumption_kWh': + building.cooling_consumption[cte.YEAR][0] / 3.6e6, + 'yearly_dhw_consumption_kWh': + building.domestic_hot_water_consumption[cte.YEAR][0] / 3.6e6, + 'yearly_appliance_electricity_consumption_kWh': + building.appliances_electrical_demand[cte.YEAR][0] / 3.6e6, + 'yearly_lighting_electricity_consumption_kWh': + building.lighting_electrical_demand[cte.YEAR][0] / 3.6e6, + 'heating_peak_load_kW': max( + building.heating_consumption[cte.HOUR]) / 3.6e6, + 'cooling_peak_load_kW': max( + building.cooling_consumption[cte.HOUR]) / 3.6e6, + 'monthly_heating_demand': + {month_name: building.heating_demand[cte.MONTH][i] / 3.6e6 + for (i, month_name) in enumerate(month_names)}, + 'monthly_heating_consumption_kWh': + {month_name: building.heating_consumption[cte.MONTH][i] / 3.6e6 + for (i, month_name) in enumerate(month_names)}, + 'monthly_cooling_demand_kWh': + {month_name: building.cooling_demand[cte.MONTH][i] / 3.6e6 + for (i, month_name) in enumerate(month_names)}, + 'monthly_cooling_consumption_kWh': + {month_name: building.cooling_consumption[cte.MONTH][i] / 3.6e6 + for (i, month_name) in enumerate(month_names)}, + 'monthly_dhw_demand_kWh': + {month_name: building.domestic_hot_water_heat_demand[cte.MONTH][i] / 3.6e6 + for (i, month_name) in enumerate(month_names)}, + 'monthly_dhw_consumption_kWh': + {month_name: building.domestic_hot_water_consumption[cte.MONTH][i] / + 3.6e6 for (i, month_name) in enumerate(month_names)}} + +with open(output_path / "air_to_water_hp_buildings_data.json", "w") as json_file: + json.dump(building_data, json_file, indent=4) diff --git a/building_modelling/ep_workflow.py b/building_modelling/ep_workflow.py new file mode 100644 index 00000000..455cce98 --- /dev/null +++ b/building_modelling/ep_workflow.py @@ -0,0 +1,45 @@ +import glob +import os +import sys +from pathlib import Path +import csv +from hub.exports.energy_building_exports_factory import EnergyBuildingsExportsFactory +from hub.imports.results_factory import ResultFactory + +sys.path.append('../energy_system_modelling_package/') + + +def energy_plus_workflow(city, output_path): + try: + # city = city + out_path = output_path + files = glob.glob(f'{out_path}/*') + + # for file in files: + # if file != '.gitignore': + # os.remove(file) + area = 0 + volume = 0 + for building in city.buildings: + volume = building.volume + for ground in building.grounds: + area += ground.perimeter_polygon.area + + print('exporting:') + _idf = EnergyBuildingsExportsFactory('idf', city, out_path).export() + print(' idf exported...') + _idf.run() + + csv_file = str((out_path / f'{city.name}_out.csv').resolve()) + eso_file = str((out_path / f'{city.name}_out.eso').resolve()) + idf_file = str((out_path / f'{city.name}.idf').resolve()) + obj_file = str((out_path / f'{city.name}.obj').resolve()) + ResultFactory('energy_plus_multiple_buildings', city, out_path).enrich() + + + + except Exception as ex: + print(ex) + print('error: ', ex) + print('[simulation abort]') + sys.stdout.flush() diff --git a/building_modelling/geojson_creator.py b/building_modelling/geojson_creator.py new file mode 100644 index 00000000..c96c340d --- /dev/null +++ b/building_modelling/geojson_creator.py @@ -0,0 +1,37 @@ +import json +from shapely import Polygon +from shapely import Point +from pathlib import Path + + +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() + 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: + city = json.load(file) + buildings = city['features'] + + for building in buildings: + coordinates = building['geometry']['coordinates'][0] + building_polygon = Polygon(coordinates) + centroid = Point(building_polygon.centroid) + + if centroid.within(selection_box): + buildings_in_region.append(building) + + output_region = {"type": "FeatureCollection", + "features": buildings_in_region} + + with open(output_file, 'w') as file: + file.write(json.dumps(output_region, indent=2)) + + return output_file diff --git a/costing_package/capital_costs.py b/costing_package/capital_costs.py new file mode 100644 index 00000000..951e92c1 --- /dev/null +++ b/costing_package/capital_costs.py @@ -0,0 +1,417 @@ +""" +Capital costs module +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2023 Project Coder Guille Gutierrez guillermo.gutierrezmorote@concordia.ca +Code contributor Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +Code contributor Oriol Gavalda Torrellas oriol.gavalda@concordia.ca +""" +import math + +import pandas as pd +import numpy_financial as npf +from hub.city_model_structure.building import Building +import hub.helpers.constants as cte +from costing_package.configuration import Configuration +from costing_package.constants import (SKIN_RETROFIT, SKIN_RETROFIT_AND_SYSTEM_RETROFIT_AND_PV, + SYSTEM_RETROFIT_AND_PV, CURRENT_STATUS, PV, SYSTEM_RETROFIT) +from costing_package.cost_base import CostBase + + +class CapitalCosts(CostBase): + """ + Capital costs class + """ + + def __init__(self, building: Building, configuration: Configuration): + super().__init__(building, configuration) + self._yearly_capital_costs = pd.DataFrame( + index=self._rng, + columns=[ + 'B2010_opaque_walls', + 'B2020_transparent', + 'B3010_opaque_roof', + 'B1010_superstructure', + 'D2010_photovoltaic_system', + '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' + ) + self._yearly_capital_costs.loc[0, 'B2010_opaque_walls'] = 0 + self._yearly_capital_costs.loc[0, 'B2020_transparent'] = 0 + 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_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( + index=self._rng, + columns=[ + 'Subsidies construction', + 'Subsidies HVAC', + 'Subsidies PV' + ], + dtype='float' + ) + self._yearly_capital_incomes.loc[0, 'Subsidies construction'] = 0 + self._yearly_capital_incomes.loc[0, 'Subsidies HVAC'] = 0 + self._yearly_capital_incomes.loc[0, 'Subsidies PV'] = 0 + self._yearly_capital_costs.fillna(0, inplace=True) + self._own_capital = 1 - self._configuration.percentage_credit + self._surface_pv = 0 + 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]: + self.skin_capital_cost() + self.energy_system_capital_cost() + self.skin_yearly_capital_costs() + self.yearly_energy_system_costs() + self.yearly_incomes() + return self._yearly_capital_costs, self._yearly_capital_incomes + + def skin_capital_cost(self): + """ + calculating skin costs + :return: + """ + surface_opaque = 0 + surface_transparent = 0 + surface_roof = 0 + surface_ground = 0 + + for thermal_zone in self._building.thermal_zones_from_internal_zones: + for thermal_boundary in thermal_zone.thermal_boundaries: + if thermal_boundary.type == 'Ground': + surface_ground += thermal_boundary.opaque_area + elif thermal_boundary.type == 'Roof': + surface_roof += thermal_boundary.opaque_area + elif thermal_boundary.type == 'Wall': + surface_opaque += thermal_boundary.opaque_area * (1 - thermal_boundary.window_ratio) + surface_transparent += thermal_boundary.opaque_area * thermal_boundary.window_ratio + + chapter = self._capital_costs_chapter.chapter('B_shell') + capital_cost_opaque = surface_opaque * chapter.item('B2010_opaque_walls').refurbishment[0] + 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, 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 + 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 + + def skin_yearly_capital_costs(self): + skin_capital_cost = self.skin_capital_cost() + for year in range(1, self._configuration.number_of_years): + self._yearly_capital_costs.loc[year, 'B2010_opaque_walls'] = ( + -npf.pmt( + self._configuration.interest_rate, + self._configuration.credit_years, + skin_capital_cost[0] * self._configuration.percentage_credit + ) + ) + self._yearly_capital_costs.loc[year, 'B2020_transparent'] = ( + -npf.pmt( + self._configuration.interest_rate, + self._configuration.credit_years, + skin_capital_cost[1] * self._configuration.percentage_credit + ) + ) + self._yearly_capital_costs.loc[year, 'B3010_opaque_roof'] = ( + -npf.pmt( + self._configuration.interest_rate, + self._configuration.credit_years, + skin_capital_cost[2] * self._configuration.percentage_credit + ) + ) + self._yearly_capital_costs.loc[year, 'B1010_superstructure'] = ( + -npf.pmt( + self._configuration.interest_rate, + self._configuration.credit_years, + skin_capital_cost[3] * self._configuration.percentage_credit + ) + ) + + def energy_system_capital_cost(self): + 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] == '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] + elif component_categories[i] == 'distribution': + capital_cost_distribution_equipment += chapter.item(component).initial_investment[0] * \ + component_sizes[i] + else: + capital_cost_energy_storage_equipment += chapter.item(component).initial_investment[0] * component_sizes[i] + + 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, 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, '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, '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_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') + system_investment_costs = self.energy_system_capital_cost() + 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'] = ( + -npf.pmt( + self._configuration.interest_rate, + self._configuration.credit_years, + system_investment_costs[0] * self._configuration.percentage_credit + ) + ) + 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, '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, '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, '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] == '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) + 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, '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, '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'] += ( + 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 = [] + component_categories = [] + sizes = [] + energy_systems = self._building.energy_systems + for energy_system in energy_systems: + demand_types = energy_system.demand_types + generation_systems = energy_system.generation_systems + distribution_systems = energy_system.distribution_systems + for generation_system in generation_systems: + if generation_system.system_type != cte.PHOTOVOLTAIC: + heating_capacity = generation_system.nominal_heat_output or 0 + cooling_capacity = generation_system.nominal_cooling_output or 0 + installed_capacity = max(heating_capacity, cooling_capacity) / 1000 + if cte.DOMESTIC_HOT_WATER in demand_types and cte.HEATING not in demand_types: + component_categories.append('dhw') + sizes.append(installed_capacity) + if generation_system.system_type == cte.HEAT_PUMP: + system_components.append(self.heat_pump_type(generation_system, domestic_how_water=True)) + elif generation_system.system_type == cte.BOILER and generation_system.fuel_type == cte.ELECTRICITY: + system_components.append(self.boiler_type(generation_system)) + else: + system_components.append('D302010_template_heat') + 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) + system_components.append(item_type) + elif generation_system.system_type == cte.BOILER: + item_type = self.boiler_type(generation_system) + system_components.append(item_type) + else: + 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: + if storage_system.type_energy_stored == 'thermal': + component_categories.append('thermal storage') + sizes.append(storage_system.volume or 0) + system_components.append('D306010_storage_tank') + 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] / 1000) + system_components.append('D3040_distribution_systems') + return system_components, component_categories, sizes + + def yearly_incomes(self): + capital_cost_skin = self.skin_capital_cost()[-1] + system_investment_cost = self.energy_system_capital_cost() + capital_cost_hvac = system_investment_cost[-1] + capital_cost_pv = system_investment_cost[0] + + self._yearly_capital_incomes.loc[0, 'Subsidies construction'] = ( + capital_cost_skin * self._archetype.income.construction_subsidy/100 + ) + self._yearly_capital_incomes.loc[0, 'Subsidies HVAC'] = capital_cost_hvac * self._archetype.income.hvac_subsidy/100 + self._yearly_capital_incomes.loc[0, 'Subsidies PV'] = capital_cost_pv * self._archetype.income.photovoltaic_subsidy/100 + self._yearly_capital_incomes.fillna(0, inplace=True) + + @staticmethod + def heat_pump_type(generation_system, domestic_how_water=False): + source_medium = generation_system.source_medium + supply_medium = generation_system.supply_medium + if domestic_how_water: + heat_pump_item = 'D4010_hot_water_heat_pump' + else: + if source_medium == cte.AIR and supply_medium == cte.WATER: + heat_pump_item = 'D302020_air_to_water_heat_pump' + elif source_medium == cte.AIR and supply_medium == cte.AIR: + heat_pump_item = 'D302050_air_to_air_heat_pump' + elif source_medium == cte.GROUND and supply_medium == cte.WATER: + heat_pump_item = 'D302030_ground_to_water_heat_pump' + elif source_medium == cte.GROUND and supply_medium == cte.AIR: + heat_pump_item = 'D302100_ground_to_air_heat_pump' + elif source_medium == cte.WATER and supply_medium == cte.WATER: + heat_pump_item = 'D302040_water_to_water_heat_pump' + elif source_medium == cte.WATER and supply_medium == cte.AIR: + heat_pump_item = 'D302110_water_to_air_heat_pump' + else: + heat_pump_item = 'D302010_template_heat' + return heat_pump_item + + @staticmethod + def boiler_type(generation_system): + fuel = generation_system.fuel_type + if fuel == cte.ELECTRICITY: + boiler_item = 'D302080_electrical_boiler' + elif fuel == cte.GAS: + boiler_item = 'D302070_natural_gas_boiler' + else: + boiler_item = 'D302010_template_heat' + return boiler_item + + + + diff --git a/costing_package/configuration.py b/costing_package/configuration.py new file mode 100644 index 00000000..3d5b9485 --- /dev/null +++ b/costing_package/configuration.py @@ -0,0 +1,238 @@ +""" +Configuration module +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2023 Project Coder Guille Gutierrez guillermo.gutierrezmorote@concordia.ca +Code contributor Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +Code contributor Oriol Gavalda Torrellas oriol.gavalda@concordia.ca +""" +from hub.catalog_factories.costs_catalog_factory import CostsCatalogFactory +from hub.catalog_factories.catalog import Catalog + + +class Configuration: + """ + Configuration class + """ + + def __init__(self, + number_of_years, + percentage_credit, + interest_rate, + credit_years, + consumer_price_index, + electricity_peak_index, + electricity_price_index, + gas_price_index, + discount_rate, + retrofitting_year_construction, + factories_handler, + retrofit_scenario, + fuel_type, + dictionary, + fuel_tariffs + ): + self._number_of_years = number_of_years + self._percentage_credit = percentage_credit + self._interest_rate = interest_rate + self._credit_years = credit_years + self._consumer_price_index = consumer_price_index + self._electricity_peak_index = electricity_peak_index + self._electricity_price_index = electricity_price_index + self._gas_price_index = gas_price_index + self._discount_rate = discount_rate + self._retrofitting_year_construction = retrofitting_year_construction + self._factories_handler = factories_handler + self._costs_catalog = CostsCatalogFactory(factories_handler).catalog + self._retrofit_scenario = retrofit_scenario + self._fuel_type = fuel_type + self._dictionary = dictionary + self._fuel_tariffs = fuel_tariffs + + @property + def number_of_years(self): + """ + Get number of years + """ + return self._number_of_years + + @number_of_years.setter + def number_of_years(self, value): + """ + Set number of years + """ + self._number_of_years = value + + @property + def percentage_credit(self): + """ + Get percentage credit + """ + return self._percentage_credit + + @percentage_credit.setter + def percentage_credit(self, value): + """ + Set percentage credit + """ + self._percentage_credit = value + + @property + def interest_rate(self): + """ + Get interest rate + """ + return self._interest_rate + + @interest_rate.setter + def interest_rate(self, value): + """ + Set interest rate + """ + self._interest_rate = value + + @property + def credit_years(self): + """ + Get credit years + """ + return self._credit_years + + @credit_years.setter + def credit_years(self, value): + """ + Set credit years + """ + self._credit_years = value + + @property + def consumer_price_index(self): + """ + Get consumer price index + """ + return self._consumer_price_index + + @consumer_price_index.setter + def consumer_price_index(self, value): + """ + Set consumer price index + """ + self._consumer_price_index = value + + @property + def electricity_peak_index(self): + """ + Get electricity peak index + """ + return self._electricity_peak_index + + @electricity_peak_index.setter + def electricity_peak_index(self, value): + """ + Set electricity peak index + """ + self._electricity_peak_index = value + + @property + def electricity_price_index(self): + """ + Get electricity price index + """ + return self._electricity_price_index + + @electricity_price_index.setter + def electricity_price_index(self, value): + """ + Set electricity price index + """ + self._electricity_price_index = value + + @property + def gas_price_index(self): + """ + Get gas price index + """ + return self._gas_price_index + + @gas_price_index.setter + def gas_price_index(self, value): + """ + Set gas price index + """ + self._gas_price_index = value + + @property + def discount_rate(self): + """ + Get discount rate + """ + return self._discount_rate + + @discount_rate.setter + def discount_rate(self, value): + """ + Set discount rate + """ + self._discount_rate = value + + @property + def retrofitting_year_construction(self): + """ + Get retrofitting year construction + """ + return self._retrofitting_year_construction + + @retrofitting_year_construction.setter + def retrofitting_year_construction(self, value): + """ + Set retrofitting year construction + """ + self._retrofitting_year_construction = value + + @property + def factories_handler(self): + """ + Get factories handler + """ + return self._factories_handler + + @factories_handler.setter + def factories_handler(self, value): + """ + Set factories handler + """ + self._factories_handler = value + + @property + def costs_catalog(self) -> Catalog: + """ + Get costs catalog + """ + return self._costs_catalog + + @property + def retrofit_scenario(self): + """ + Get retrofit scenario + """ + return self._retrofit_scenario + + @property + def fuel_type(self): + """ + Get fuel type (0: Electricity, 1: Gas) + """ + return self._fuel_type + + @property + def dictionary(self): + """ + 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/costing_package/constants.py b/costing_package/constants.py new file mode 100644 index 00000000..87372d3d --- /dev/null +++ b/costing_package/constants.py @@ -0,0 +1,23 @@ +""" +Constants module +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2023 Project Coder Guille Gutierrez guillermo.gutierrezmorote@concordia.ca +Code contributor Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +Code contributor Oriol Gavalda Torrellas oriol.gavalda@concordia.ca +""" + +# constants +CURRENT_STATUS = 0 +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, + SYSTEM_RETROFIT +] diff --git a/costing_package/cost.py b/costing_package/cost.py new file mode 100644 index 00000000..2b2bdcd7 --- /dev/null +++ b/costing_package/cost.py @@ -0,0 +1,170 @@ +""" +Cost module +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2023 Project Coder Guille Gutierrez guillermo.gutierrezmorote@concordia.ca +Code contributor Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +Code contributor Oriol Gavalda Torrellas oriol.gavalda@concordia.ca +""" + +import pandas as pd +import numpy_financial as npf +from hub.city_model_structure.building import Building +from hub.helpers.dictionaries import Dictionaries +from costing_package.configuration import Configuration +from costing_package.capital_costs import CapitalCosts +from costing_package.end_of_life_costs import EndOfLifeCosts +from costing_package.total_maintenance_costs import TotalMaintenanceCosts +from costing_package.total_operational_costs import TotalOperationalCosts +from costing_package.total_operational_incomes import TotalOperationalIncomes +from costing_package.constants import CURRENT_STATUS +import hub.helpers.constants as cte + + +class Cost: + """ + Cost class + """ + + def __init__(self, + building: Building, + number_of_years=31, + percentage_credit=0, + interest_rate=0.04, + credit_years=15, + consumer_price_index=0.04, + electricity_peak_index=0.05, + electricity_price_index=0.05, + fuel_price_index=0.05, + discount_rate=0.03, + retrofitting_year_construction=2020, + factories_handler='montreal_new', + retrofit_scenario=CURRENT_STATUS, + 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 + fuel_type = self._building.energy_consumption_breakdown.keys() + self._configuration = Configuration(number_of_years, + percentage_credit, + interest_rate, credit_years, + consumer_price_index, + electricity_peak_index, + electricity_price_index, + fuel_price_index, + discount_rate, + retrofitting_year_construction, + factories_handler, + retrofit_scenario, + fuel_type, + dictionary, + fuel_tariffs) + + @property + def building(self) -> Building: + """ + Get current building. + """ + return self._building + + def _npv_from_list(self, list_cashflow): + return npf.npv(self._configuration.discount_rate, list_cashflow) + + @property + def life_cycle(self) -> pd.DataFrame: + """ + Get complete life cycle costs + :return: DataFrame + """ + results = pd.DataFrame() + global_capital_costs, global_capital_incomes = CapitalCosts(self._building, self._configuration).calculate() + global_end_of_life_costs = EndOfLifeCosts(self._building, self._configuration).calculate() + global_operational_costs = TotalOperationalCosts(self._building, self._configuration).calculate() + global_maintenance_costs = TotalMaintenanceCosts(self._building, self._configuration).calculate() + global_operational_incomes = TotalOperationalIncomes(self._building, self._configuration).calculate() + + df_capital_costs_skin = ( + global_capital_costs['B2010_opaque_walls'] + + global_capital_costs['B2020_transparent'] + + global_capital_costs['B3010_opaque_roof'] + + global_capital_costs['B1010_superstructure'] + ) + df_capital_costs_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'] + ) + + df_end_of_life_costs = global_end_of_life_costs['End_of_life_costs'] + operational_costs_list = [ + global_operational_costs['Fixed Costs Electricity Peak'], + global_operational_costs['Fixed Costs Electricity Monthly'], + global_operational_costs['Variable Costs Electricity'] + ] + additional_costs = [ + global_operational_costs[f'Fixed Costs {fuel}'] for fuel in + self._building.energy_consumption_breakdown.keys() if fuel != cte.ELECTRICITY + ] + [ + global_operational_costs[f'Variable Costs {fuel}'] for fuel in + self._building.energy_consumption_breakdown.keys() if fuel != cte.ELECTRICITY + ] + df_operational_costs = sum(operational_costs_list + additional_costs) + df_maintenance_costs = ( + global_maintenance_costs['Heating_maintenance'] + + global_maintenance_costs['Cooling_maintenance'] + + global_maintenance_costs['PV_maintenance'] + ) + df_operational_incomes = global_operational_incomes['Incomes electricity'] + df_capital_incomes = ( + global_capital_incomes['Subsidies construction'] + + global_capital_incomes['Subsidies HVAC'] + + global_capital_incomes['Subsidies PV'] + ) + + life_cycle_costs_capital_skin = self._npv_from_list(df_capital_costs_skin.values.tolist()) + life_cycle_costs_capital_systems = self._npv_from_list(df_capital_costs_systems.values.tolist()) + life_cycle_costs_end_of_life_costs = self._npv_from_list(df_end_of_life_costs.values.tolist()) + life_cycle_operational_costs = self._npv_from_list(df_operational_costs) + life_cycle_maintenance_costs = self._npv_from_list(df_maintenance_costs.values.tolist()) + life_cycle_operational_incomes = self._npv_from_list(df_operational_incomes.values.tolist()) + life_cycle_capital_incomes = self._npv_from_list(df_capital_incomes.values.tolist()) + + results[f'Scenario {self._configuration.retrofit_scenario}'] = [ + life_cycle_costs_capital_skin, + life_cycle_costs_capital_systems, + life_cycle_costs_end_of_life_costs, + life_cycle_operational_costs, + life_cycle_maintenance_costs, + life_cycle_operational_incomes, + life_cycle_capital_incomes, + global_capital_costs, + global_capital_incomes, + global_end_of_life_costs, + global_operational_costs, + global_maintenance_costs, + global_operational_incomes + ] + + results.index = [ + 'total_capital_costs_skin', + 'total_capital_costs_systems', + 'end_of_life_costs', + 'total_operational_costs', + 'total_maintenance_costs', + 'operational_incomes', + 'capital_incomes', + 'global_capital_costs', + 'global_capital_incomes', + 'global_end_of_life_costs', + 'global_operational_costs', + 'global_maintenance_costs', + 'global_operational_incomes' + ] + return results diff --git a/costing_package/cost_base.py b/costing_package/cost_base.py new file mode 100644 index 00000000..ad5a5aee --- /dev/null +++ b/costing_package/cost_base.py @@ -0,0 +1,40 @@ +""" +Cost base module +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2023 Project Coder Guille Gutierrez guillermo.gutierrezmorote@concordia.ca +Code contributor Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +Code contributor Oriol Gavalda Torrellas oriol.gavalda@concordia.ca +""" + +from hub.city_model_structure.building import Building + +from costing_package.configuration import Configuration + + +class CostBase: + """ + Abstract base class for the costs + """ + def __init__(self, building: Building, configuration: Configuration): + self._building = building + self._configuration = configuration + self._total_floor_area = 0 + for thermal_zone in building.thermal_zones_from_internal_zones: + self._total_floor_area += thermal_zone.total_floor_area + self._archetype = None + self._capital_costs_chapter = None + for archetype in self._configuration.costs_catalog.entries().archetypes: + if configuration.dictionary[str(building.function)] == str(archetype.function): + self._archetype = archetype + self._capital_costs_chapter = self._archetype.capital_cost + break + if not self._archetype: + raise KeyError(f'archetype not found for function {building.function}') + + self._rng = range(configuration.number_of_years) + + def calculate(self): + """ + Raises not implemented exception + """ + raise NotImplementedError() diff --git a/costing_package/end_of_life_costs.py b/costing_package/end_of_life_costs.py new file mode 100644 index 00000000..f04386ef --- /dev/null +++ b/costing_package/end_of_life_costs.py @@ -0,0 +1,38 @@ +""" +End of life costs module +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2023 Project Coder Guille Gutierrez guillermo.gutierrezmorote@concordia.ca +Code contributor Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +Code contributor Oriol Gavalda Torrellas oriol.gavalda@concordia.ca +""" +import math +import pandas as pd +from hub.city_model_structure.building import Building + +from costing_package.configuration import Configuration +from costing_package.cost_base import CostBase + + +class EndOfLifeCosts(CostBase): + """ + End of life costs class + """ + def __init__(self, building: Building, configuration: Configuration): + super().__init__(building, configuration) + self._yearly_end_of_life_costs = pd.DataFrame(index=self._rng, columns=['End_of_life_costs'], dtype='float') + + def calculate(self): + """ + Calculate end of life costs + :return: pd.DataFrame + """ + archetype = self._archetype + total_floor_area = self._total_floor_area + for year in range(1, self._configuration.number_of_years + 1): + price_increase = math.pow(1 + self._configuration.consumer_price_index, year) + if year == self._configuration.number_of_years: + self._yearly_end_of_life_costs.at[year, 'End_of_life_costs'] = ( + total_floor_area * archetype.end_of_life_cost * price_increase + ) + self._yearly_end_of_life_costs.fillna(0, inplace=True) + return self._yearly_end_of_life_costs diff --git a/costing_package/peak_load.py b/costing_package/peak_load.py new file mode 100644 index 00000000..422f563b --- /dev/null +++ b/costing_package/peak_load.py @@ -0,0 +1,56 @@ +""" +Peak load module +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2023 Project Coder Guille Gutierrez guillermo.gutierrezmorote@concordia.ca +Code contributor Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +Code contributor Oriol Gavalda Torrellas oriol.gavalda@concordia.ca +""" + +import pandas as pd + +import hub.helpers.constants as cte + + +class PeakLoad: + """ + Peak load class + """ + + def __init__(self, building): + self._building = building + + @property + def electricity_peak_load(self): + """ + Get the electricity peak load in W + """ + array = [None] * 12 + heating = 0 + cooling = 0 + for system in self._building.energy_systems: + if cte.HEATING in system.demand_types: + heating = 1 + if cte.COOLING in system.demand_types: + cooling = 1 + if cte.MONTH in self._building.heating_peak_load.keys() and cte.MONTH in self._building.cooling_peak_load.keys(): + peak_lighting = self._building.lighting_peak_load[cte.YEAR][0] + peak_appliances = self._building.appliances_peak_load[cte.YEAR][0] + monthly_electricity_peak = [0.9 * peak_lighting + 0.7 * peak_appliances] * 12 + conditioning_peak = max(self._building.heating_peak_load[cte.MONTH], self._building.cooling_peak_load[cte.MONTH]) + for i in range(len(conditioning_peak)): + if cooling == 1 and heating == 1: + conditioning_peak[i] = conditioning_peak[i] + continue + elif cooling == 0: + 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] + + electricity_peak_load_results = pd.DataFrame( + monthly_electricity_peak, + columns=[f'electricity peak load W'] + ) + else: + electricity_peak_load_results = pd.DataFrame(array, columns=[f'electricity peak load W']) + return electricity_peak_load_results diff --git a/costing_package/total_maintenance_costs.py b/costing_package/total_maintenance_costs.py new file mode 100644 index 00000000..c4e89f20 --- /dev/null +++ b/costing_package/total_maintenance_costs.py @@ -0,0 +1,138 @@ +""" +Total maintenance costs module +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2023 Project Coder Guille Gutierrez guillermo.gutierrezmorote@concordia.ca +Code contributor Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +Code contributor Oriol Gavalda Torrellas oriol.gavalda@concordia.ca +""" +import math +import pandas as pd +from hub.city_model_structure.building import Building +import hub.helpers.constants as cte + +from costing_package.configuration import Configuration +from costing_package.cost_base import CostBase + + +class TotalMaintenanceCosts(CostBase): + """ + Total maintenance costs class + """ + def __init__(self, building: Building, configuration: Configuration): + super().__init__(building, configuration) + self._yearly_maintenance_costs = pd.DataFrame( + index=self._rng, + columns=[ + 'Heating_maintenance', + 'Cooling_maintenance', + 'DHW_maintenance', + 'PV_maintenance' + ], + dtype='float' + ) + + def calculate(self) -> pd.DataFrame: + """ + Calculate total maintenance costs + :return: pd.DataFrame + """ + building = self._building + archetype = self._archetype + # todo: change area pv when the variable exists + roof_area = 0 + 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 + + 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.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: + 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 + + + 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_pv_0 = surface_pv * archetype.operational_cost.maintenance_pv + + for year in range(1, self._configuration.number_of_years + 1): + costs_increase = math.pow(1 + self._configuration.consumer_price_index, year) + self._yearly_maintenance_costs.loc[year, 'Heating_maintenance'] = ( + maintenance_heating_0 * costs_increase + ) + 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/costing_package/total_operational_costs.py b/costing_package/total_operational_costs.py new file mode 100644 index 00000000..cc8c56d6 --- /dev/null +++ b/costing_package/total_operational_costs.py @@ -0,0 +1,237 @@ +""" +Total operational costs module +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2024 Project Coder Saeed Ranjbar saeed.ranjbar@mail.concordia.ca +Code contributor Oriol Gavalda Torrellas oriol.gavalda@concordia.ca +""" +import math +import pandas as pd + +from hub.city_model_structure.building import Building +import hub.helpers.constants as cte + +from costing_package.configuration import Configuration +from costing_package.cost_base import CostBase +from costing_package.peak_load import PeakLoad + + +class TotalOperationalCosts(CostBase): + """ + Total Operational costs class + """ + + def __init__(self, building: Building, configuration: Configuration): + super().__init__(building, configuration) + columns_list = self.columns() + self._yearly_operational_costs = pd.DataFrame( + index=self._rng, + columns=columns_list, + dtype='float' + ) + + def calculate(self) -> pd.DataFrame: + """ + Calculate total operational costs + :return: pd.DataFrame + """ + building = self._building + fuel_consumption_breakdown = building.energy_consumption_breakdown + archetype = self._archetype + total_floor_area = self._total_floor_area + if archetype.function == 'residential': + factor = total_floor_area / 80 + else: + factor = 1 + 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 + 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 * float(fuel.variable.values[0]) / 1000 + ) + else: + 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: + 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()) / ( + 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 + + def columns(self): + columns_list = [] + fuels = [key for key in self._building.energy_consumption_breakdown.keys()] + for fuel in fuels: + if fuel == cte.ELECTRICITY: + columns_list.append('Fixed Costs Electricity Peak') + columns_list.append('Fixed Costs Electricity Monthly') + columns_list.append('Variable Costs Electricity') + else: + columns_list.append(f'Fixed Costs {fuel}') + columns_list.append(f'Variable Costs {fuel}') + + 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: + hourly += 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/costing_package/total_operational_incomes.py b/costing_package/total_operational_incomes.py new file mode 100644 index 00000000..bb190999 --- /dev/null +++ b/costing_package/total_operational_incomes.py @@ -0,0 +1,44 @@ +""" +Total operational incomes module +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2023 Project Coder Guille Gutierrez guillermo.gutierrezmorote@concordia.ca +Code contributor Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +Code contributor Oriol Gavalda Torrellas oriol.gavalda@concordia.ca +""" +import math +import pandas as pd +from hub.city_model_structure.building import Building +import hub.helpers.constants as cte + +from costing_package.configuration import Configuration +from costing_package.cost_base import CostBase + + +class TotalOperationalIncomes(CostBase): + """ + Total operational incomes class + """ + def __init__(self, building: Building, configuration: Configuration): + super().__init__(building, configuration) + self._yearly_operational_incomes = pd.DataFrame(index=self._rng, columns=['Incomes electricity'], dtype='float') + + def calculate(self) -> pd.DataFrame: + """ + Calculate total operational incomes + :return: pd.DataFrame + """ + building = self._building + archetype = self._archetype + if cte.YEAR not in building.onsite_electrical_production: + 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 / 3.6e6) * price_export * price_increase_electricity + ) + + self._yearly_operational_incomes.fillna(0, inplace=True) + return self._yearly_operational_incomes \ No newline at end of file diff --git a/costing_package/version.py b/costing_package/version.py new file mode 100644 index 00000000..0f4beb14 --- /dev/null +++ b/costing_package/version.py @@ -0,0 +1,8 @@ +""" +Cost version number +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2023 Project Coder Guille Gutierrez guillermo.gutierrezmorote@concordia.ca +Code contributor Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +Code contributor Oriol Gavalda Torrellas oriol.gavalda@concordia.ca +""" +__version__ = '0.1.0.5' diff --git a/data/OMHM_buildings_unique.geojson b/data/OMHM_buildings_unique.geojson new file mode 100644 index 00000000..c0e3442e --- /dev/null +++ b/data/OMHM_buildings_unique.geojson @@ -0,0 +1,2855 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "OBJECTID": 161053, + "ID_UEV": "01029234", + "CIVIQUE_DE": 6109, + "CIVIQUE_FI": 6119, + "NOM_RUE": "rue Jeanne-Mance (MTL)", + "MUNICIPALI": 50, + "CODE_UTILI": 1000, + "LIBELLE_UT": "Logement", + "CATEGORIE_": "Régulier", + "Hieght_LiD": 9, + "AREA_NEW": 118, + "MBG_Width": 8, + "MBG_Length": 16, + "MBG_Orientation": 32, + "Shape_Length": 48, + "Shape_Area": 125, + "BuildingCategory": "fully-attached", + "BuildingVolume": 1062, + "AspectRatio": 2.152, + "SurfacetoVolumeRatio": 0.111, + "FloorNu_RawTax": 3, + "FloorNu_RawTax.1": 3, + "Floor_frmHieght": 3, + "TotalFloorArea": 354, + "ANNEE_CONS": 1910, + "address": "6109 à 6119" + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.60889457783199, + 45.52627967341867 + ], + [ + -73.60878459611386, + 45.52640189543207 + ], + [ + -73.60880878559433, + 45.526414045422094 + ], + [ + -73.60881138576005, + 45.526411446008474 + ], + [ + -73.60882178517528, + 45.526397746843365 + ], + [ + -73.60884748548206, + 45.526407346140566 + ], + [ + -73.60883288525416, + 45.52642674619433 + ], + [ + -73.60886468497895, + 45.526438645964845 + ], + [ + -73.60886488468759, + 45.52643864577451 + ], + [ + -73.60887972027291, + 45.526424456462976 + ], + [ + -73.60897708070512, + 45.52631626050087 + ], + [ + -73.60889457783199, + 45.52627967341867 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "OBJECTID": 162168, + "ID_UEV": "01001944", + "CIVIQUE_DE": 4820, + "CIVIQUE_FI": 4850, + "NOM_RUE": "rue de Grand-Pré (MTL)", + "MUNICIPALI": 50, + "CODE_UTILI": 1000, + "LIBELLE_UT": "Logement", + "CATEGORIE_": "Régulier", + "Hieght_LiD": 11, + "AREA_NEW": 504, + "MBG_Width": 16, + "MBG_Length": 42, + "MBG_Orientation": 123, + "Shape_Length": 116, + "Shape_Area": 669, + "BuildingCategory": "fully-attached", + "BuildingVolume": 5544, + "AspectRatio": 2.648, + "SurfacetoVolumeRatio": 0.091, + "FloorNu_RawTax": 3, + "FloorNu_RawTax.1": 3, + "Floor_frmHieght": 3, + "TotalFloorArea": 1512, + "ANNEE_CONS": 1980, + "address": "4820 à 4850" + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.58667395105716, + 45.52483824715297 + ], + [ + -73.58656830557055, + 45.52495268609808 + ], + [ + -73.58662307714782, + 45.52497954708488 + ], + [ + -73.58673187732023, + 45.525032846618785 + ], + [ + -73.58681547812479, + 45.52507384729731 + ], + [ + -73.58682087703855, + 45.525068346972965 + ], + [ + -73.58687407774353, + 45.525094146322125 + ], + [ + -73.58689537859419, + 45.525104346900406 + ], + [ + -73.58694327730556, + 45.52512704670317 + ], + [ + -73.58701697601475, + 45.52516199344971 + ], + [ + -73.58712501598687, + 45.525044959829344 + ], + [ + -73.5871212778519, + 45.52504324668608 + ], + [ + -73.58709277727542, + 45.52507444696593 + ], + [ + -73.58703227729441, + 45.525047146948445 + ], + [ + -73.58703967802359, + 45.52503894736905 + ], + [ + -73.58703377762288, + 45.52503634682888 + ], + [ + -73.58692947789729, + 45.52499034635711 + ], + [ + -73.58664877714105, + 45.524866746179505 + ], + [ + -73.58667417779681, + 45.52483834686315 + ], + [ + -73.58667395105716, + 45.52483824715297 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "OBJECTID": 135557, + "ID_UEV": "01016997", + "CIVIQUE_DE": 4110, + "CIVIQUE_FI": 4118, + "NOM_RUE": "rue de Mentana (MTL)", + "MUNICIPALI": 50, + "CODE_UTILI": 1000, + "LIBELLE_UT": "Logement", + "CATEGORIE_": "Régulier", + "Hieght_LiD": 35, + "AREA_NEW": 368, + "MBG_Width": 9, + "MBG_Length": 40, + "MBG_Orientation": 123, + "Shape_Length": 99, + "Shape_Area": 373, + "BuildingCategory": "semi-attached", + "BuildingVolume": 12880, + "AspectRatio": 4.269, + "SurfacetoVolumeRatio": 0.029, + "FloorNu_RawTax": 9, + "FloorNu_RawTax.1": 9, + "Floor_frmHieght": 9, + "TotalFloorArea": 2772, + "ANNEE_CONS": 1974, + "address": "4110 à 4118" + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.57441887367695, + 45.5242318471938 + ], + [ + -73.57441497351891, + 45.52423604658769 + ], + [ + -73.57435327421018, + 45.52430264701829 + ], + [ + -73.57467247380185, + 45.524454446940965 + ], + [ + -73.57467717355085, + 45.5244566475763 + ], + [ + -73.57469780139083, + 45.52446508628423 + ], + [ + -73.57476405331622, + 45.524393192531484 + ], + [ + -73.5747470732633, + 45.52438524726022 + ], + [ + -73.57441887367695, + 45.5242318471938 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "OBJECTID": 139141, + "ID_UEV": "01093226", + "CIVIQUE_DE": 5170, + "CIVIQUE_FI": 5174, + "NOM_RUE": "rue Drolet (MTL)", + "MUNICIPALI": 50, + "CODE_UTILI": 1000, + "LIBELLE_UT": "Logement", + "CATEGORIE_": "Régulier", + "Hieght_LiD": 13, + "AREA_NEW": 257, + "MBG_Width": 13, + "MBG_Length": 22, + "MBG_Orientation": 122, + "Shape_Length": 69, + "Shape_Area": 280, + "BuildingCategory": "semi-attached", + "BuildingVolume": 3341, + "AspectRatio": 1.702, + "SurfacetoVolumeRatio": 0.077, + "FloorNu_RawTax": 3, + "FloorNu_RawTax.1": 3, + "Floor_frmHieght": 4, + "TotalFloorArea": 1028, + "ANNEE_CONS": 1979, + "address": "5170 à 5174" + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.59116747903097, + 45.52655654642387 + ], + [ + -73.59116467930713, + 45.52655974668784 + ], + [ + -73.5910896795654, + 45.526649446727895 + ], + [ + -73.5912955793883, + 45.52674224656648 + ], + [ + -73.5912957792612, + 45.52674234718875 + ], + [ + -73.5913146794984, + 45.52672144770766 + ], + [ + -73.59133757960647, + 45.52673174695828 + ], + [ + -73.59131607871272, + 45.526755447022836 + ], + [ + -73.59131618000944, + 45.52675554682402 + ], + [ + -73.59132031049202, + 45.526757487175246 + ], + [ + -73.59140078295663, + 45.5266568000659 + ], + [ + -73.59137787974812, + 45.52664694625084 + ], + [ + -73.59116747903097, + 45.52655654642387 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "OBJECTID": 159710, + "ID_UEV": "01031526", + "CIVIQUE_DE": 5301, + "CIVIQUE_FI": 5321, + "NOM_RUE": "rue Saint-Dominique (MTL)", + "MUNICIPALI": 50, + "CODE_UTILI": 1000, + "LIBELLE_UT": "Logement", + "CATEGORIE_": "Régulier", + "Hieght_LiD": 10, + "AREA_NEW": 426, + "MBG_Width": 15, + "MBG_Length": 29, + "MBG_Orientation": 123, + "Shape_Length": 88, + "Shape_Area": 429, + "BuildingCategory": "semi-attached", + "BuildingVolume": 4260, + "AspectRatio": 1.958, + "SurfacetoVolumeRatio": 0.1, + "FloorNu_RawTax": 3, + "FloorNu_RawTax.1": 3, + "Floor_frmHieght": 3, + "TotalFloorArea": 1278, + "ANNEE_CONS": 1983, + "address": "5301 à 5321" + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.59528017977978, + 45.52530604654458 + ], + [ + -73.59527857988931, + 45.52530774677637 + ], + [ + -73.59517668098674, + 45.525416646636465 + ], + [ + -73.59518088056568, + 45.525418547198015 + ], + [ + -73.59547958119505, + 45.52555564674248 + ], + [ + -73.59548398066913, + 45.52555764700795 + ], + [ + -73.59549828097119, + 45.52554074686311 + ], + [ + -73.59550289919896, + 45.52554265606116 + ], + [ + -73.59558361927719, + 45.52545527440496 + ], + [ + -73.59558008104706, + 45.52545384662053 + ], + [ + -73.59558698038187, + 45.52544584667189 + ], + [ + -73.5955861808828, + 45.52544544691224 + ], + [ + -73.59554998064966, + 45.525428947143965 + ], + [ + -73.59546158122959, + 45.5253886461476 + ], + [ + -73.59533388036398, + 45.5253305466666 + ], + [ + -73.59528017977978, + 45.52530604654458 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "OBJECTID": 160314, + "ID_UEV": "01027192", + "CIVIQUE_DE": 3876, + "CIVIQUE_FI": 3880, + "NOM_RUE": "rue de Mentana (MTL)", + "MUNICIPALI": 50, + "CODE_UTILI": 1000, + "LIBELLE_UT": "Logement", + "CATEGORIE_": "Régulier", + "Hieght_LiD": 12, + "AREA_NEW": 366, + "MBG_Width": 16, + "MBG_Length": 32, + "MBG_Orientation": 110, + "Shape_Length": 96, + "Shape_Area": 507, + "BuildingCategory": "semi-attached", + "BuildingVolume": 4392, + "AspectRatio": 2.009, + "SurfacetoVolumeRatio": 0.083, + "FloorNu_RawTax": 3, + "FloorNu_RawTax.1": 3, + "Floor_frmHieght": 3, + "TotalFloorArea": 1098, + "ANNEE_CONS": 1980, + "address": "3876 à 3880" + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.57080007166596, + 45.522686946460304 + ], + [ + -73.57093107256577, + 45.52272604782575 + ], + [ + -73.5710282718666, + 45.52261304669843 + ], + [ + -73.57095527236102, + 45.522582046683695 + ], + [ + -73.57092677184869, + 45.522615346769584 + ], + [ + -73.57083647166334, + 45.52257704711896 + ], + [ + -73.57068192396436, + 45.522511513915816 + ], + [ + -73.5705991607844, + 45.52260174167426 + ], + [ + -73.57068447255547, + 45.5226310461983 + ], + [ + -73.57068387203597, + 45.52263194640998 + ], + [ + -73.5706905727058, + 45.52264164703885 + ], + [ + -73.57069597126467, + 45.522651146909 + ], + [ + -73.57072017202435, + 45.52264334651211 + ], + [ + -73.57074107228954, + 45.52267404691253 + ], + [ + -73.57076887312483, + 45.52266454717734 + ], + [ + -73.57078667249642, + 45.522690747590424 + ], + [ + -73.57080077281205, + 45.52268564755646 + ], + [ + -73.57080007166596, + 45.522686946460304 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "OBJECTID": 133286, + "ID_UEV": "01042232", + "CIVIQUE_DE": 3620, + "CIVIQUE_FI": 3622, + "NOM_RUE": "rue De Bullion (MTL)", + "MUNICIPALI": 50, + "CODE_UTILI": 1000, + "LIBELLE_UT": "Logement", + "CATEGORIE_": "Régulier", + "Hieght_LiD": 12, + "AREA_NEW": 109, + "MBG_Width": 11, + "MBG_Length": 21, + "MBG_Orientation": 33, + "Shape_Length": 63, + "Shape_Area": 227, + "BuildingCategory": "fully-attached", + "BuildingVolume": 1308, + "AspectRatio": 1.922, + "SurfacetoVolumeRatio": 0.083, + "FloorNu_RawTax": 3, + "FloorNu_RawTax.1": 4, + "Floor_frmHieght": 3, + "TotalFloorArea": 327, + "ANNEE_CONS": 1973, + "address": "3620 à 3622" + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.5721643414897, + 45.515081822522674 + ], + [ + -73.57209427271673, + 45.51515771168995 + ], + [ + -73.5722098721648, + 45.515210745503666 + ], + [ + -73.57213467217159, + 45.51529174543661 + ], + [ + -73.57213523057614, + 45.51529202223508 + ], + [ + -73.57228111445389, + 45.51513478623582 + ], + [ + -73.57220477188011, + 45.51510084571349 + ], + [ + -73.57217897185836, + 45.5150894457327 + ], + [ + -73.57218047193467, + 45.51508944568466 + ], + [ + -73.5721643414897, + 45.515081822522674 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "OBJECTID": 137144, + "ID_UEV": "01026386", + "CIVIQUE_DE": 3880, + "CIVIQUE_FI": 3884, + "NOM_RUE": "rue Rivard (MTL)", + "MUNICIPALI": 50, + "CODE_UTILI": 1000, + "LIBELLE_UT": "Logement", + "CATEGORIE_": "Régulier", + "Hieght_LiD": 11, + "AREA_NEW": 279, + "MBG_Width": 12, + "MBG_Length": 24, + "MBG_Orientation": 123, + "Shape_Length": 73, + "Shape_Area": 293, + "BuildingCategory": "fully-attached", + "BuildingVolume": 3069, + "AspectRatio": 2.03, + "SurfacetoVolumeRatio": 0.091, + "FloorNu_RawTax": 3, + "FloorNu_RawTax.1": 3, + "Floor_frmHieght": 3, + "TotalFloorArea": 837, + "ANNEE_CONS": 1973, + "address": "3880 à 3884" + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.5733537088625, + 45.520196602777794 + ], + [ + -73.57327657712864, + 45.52028034554588 + ], + [ + -73.57332987218481, + 45.5203042462016 + ], + [ + -73.57333967282041, + 45.52030864642829 + ], + [ + -73.57334087335602, + 45.520307546952274 + ], + [ + -73.57338117308188, + 45.52032724647325 + ], + [ + -73.57342517297674, + 45.52034854711724 + ], + [ + -73.57353603634354, + 45.52040236004708 + ], + [ + -73.57361960889615, + 45.520311623893825 + ], + [ + -73.5733537088625, + 45.520196602777794 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "OBJECTID": 163186, + "ID_UEV": "01119792", + "CIVIQUE_DE": 4061, + "CIVIQUE_FI": 4077, + "NOM_RUE": "rue Saint-Dominique (MTL)", + "MUNICIPALI": 50, + "CODE_UTILI": 1000, + "LIBELLE_UT": "Logement", + "CATEGORIE_": "Régulier", + "Hieght_LiD": 7, + "AREA_NEW": 221, + "MBG_Width": 18, + "MBG_Length": 22, + "MBG_Orientation": 33, + "Shape_Length": 80, + "Shape_Area": 396, + "BuildingCategory": "semi-attached", + "BuildingVolume": 1547, + "AspectRatio": 1.168, + "SurfacetoVolumeRatio": 0.143, + "FloorNu_RawTax": 3, + "FloorNu_RawTax.1": 3, + "Floor_frmHieght": 2, + "TotalFloorArea": 442, + "ANNEE_CONS": 1991, + "address": "4061 à 4077" + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.57922247392104, + 45.517682445429905 + ], + [ + -73.57921957540006, + 45.51768564547461 + ], + [ + -73.57914017476726, + 45.51777754547107 + ], + [ + -73.5791762741523, + 45.5177929455269 + ], + [ + -73.579181774936, + 45.51779524620637 + ], + [ + -73.57933357470293, + 45.51785854546608 + ], + [ + -73.57928457389015, + 45.51791674609973 + ], + [ + -73.57928627226089, + 45.51791752958212 + ], + [ + -73.57943617814182, + 45.517755275011176 + ], + [ + -73.57943507532656, + 45.51775474577183 + ], + [ + -73.57943487422794, + 45.51775464602933 + ], + [ + -73.57941757418094, + 45.51777304630667 + ], + [ + -73.5792655750006, + 45.517702545869504 + ], + [ + -73.57922247392104, + 45.517682445429905 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "OBJECTID": 154720, + "ID_UEV": "05064145", + "CIVIQUE_DE": 4420, + "CIVIQUE_FI": 4430, + "NOM_RUE": "avenue Laval (MTL)", + "MUNICIPALI": 50, + "CODE_UTILI": 1000, + "LIBELLE_UT": "Logement", + "CATEGORIE_": "Régulier", + "Hieght_LiD": 17, + "AREA_NEW": 436, + "MBG_Width": 18, + "MBG_Length": 33, + "MBG_Orientation": 123, + "Shape_Length": 103, + "Shape_Area": 603, + "BuildingCategory": "fully-attached", + "BuildingVolume": 7412, + "AspectRatio": 1.857, + "SurfacetoVolumeRatio": 0.059, + "FloorNu_RawTax": 3, + "FloorNu_RawTax.1": 3, + "Floor_frmHieght": 5, + "TotalFloorArea": 2180, + "ANNEE_CONS": 1976, + "address": "4420 à 4430" + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.58341837636826, + 45.521926846073086 + ], + [ + -73.58341087121374, + 45.52193462617308 + ], + [ + -73.58341528535665, + 45.52193663859182 + ], + [ + -73.58354076395466, + 45.52180061432945 + ], + [ + -73.58353427610166, + 45.521797746784124 + ], + [ + -73.58349797659882, + 45.52183824723736 + ], + [ + -73.58345067612385, + 45.52181734702001 + ], + [ + -73.58345967552047, + 45.52180724603739 + ], + [ + -73.58343007604864, + 45.521794147089594 + ], + [ + -73.58344297589473, + 45.52177974652228 + ], + [ + -73.58341867552964, + 45.52176894639158 + ], + [ + -73.58341827614375, + 45.52176894578341 + ], + [ + -73.58340087622037, + 45.521782946505205 + ], + [ + -73.58338027647409, + 45.521770345783594 + ], + [ + -73.58336707687859, + 45.52178114630181 + ], + [ + -73.5832795764312, + 45.521727846002946 + ], + [ + -73.58323067556272, + 45.52169794665872 + ], + [ + -73.58323047616392, + 45.5216981456687 + ], + [ + -73.5831892756937, + 45.52168694657471 + ], + [ + -73.58314611449745, + 45.52167510730695 + ], + [ + -73.58306105589082, + 45.521769066618106 + ], + [ + -73.58310477661708, + 45.52178704692033 + ], + [ + -73.58310327630407, + 45.52178874601035 + ], + [ + -73.58310817585152, + 45.521790946154496 + ], + [ + -73.58314307565726, + 45.52180724655878 + ], + [ + -73.58331147623066, + 45.52188584638376 + ], + [ + -73.58331817609282, + 45.521878847039105 + ], + [ + -73.5833641764975, + 45.52190094684705 + ], + [ + -73.58341837636826, + 45.521926846073086 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "OBJECTID": 160236, + "ID_UEV": "01014249", + "CIVIQUE_DE": 4400, + "CIVIQUE_FI": 4420, + "NOM_RUE": "avenue Coloniale (MTL)", + "MUNICIPALI": 50, + "CODE_UTILI": 1000, + "LIBELLE_UT": "Logement", + "CATEGORIE_": "Régulier", + "Hieght_LiD": 10, + "AREA_NEW": 491, + "MBG_Width": 18, + "MBG_Length": 42, + "MBG_Orientation": 123, + "Shape_Length": 120, + "Shape_Area": 756, + "BuildingCategory": "semi-attached", + "BuildingVolume": 4910, + "AspectRatio": 2.332, + "SurfacetoVolumeRatio": 0.1, + "FloorNu_RawTax": 3, + "FloorNu_RawTax.1": 3, + "Floor_frmHieght": 3, + "TotalFloorArea": 1473, + "ANNEE_CONS": 1977, + "address": "4400 à 4420" + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.58447697668468, + 45.52050394605542 + ], + [ + -73.5844770763791, + 45.520503846099416 + ], + [ + -73.58451867715243, + 45.520459645758464 + ], + [ + -73.58450917643076, + 45.52045524536925 + ], + [ + -73.58454227641637, + 45.52041964568424 + ], + [ + -73.58410347634215, + 45.52021834594994 + ], + [ + -73.58414177659611, + 45.520177145542746 + ], + [ + -73.58413976427444, + 45.520176224688534 + ], + [ + -73.58401865931442, + 45.52030952711309 + ], + [ + -73.58406117662338, + 45.52032794616129 + ], + [ + -73.5840575766858, + 45.52033194590176 + ], + [ + -73.5840633770002, + 45.5203339464779 + ], + [ + -73.58412297622951, + 45.52035384666597 + ], + [ + -73.58411857668492, + 45.52035984644271 + ], + [ + -73.58415367674115, + 45.52037434671398 + ], + [ + -73.58423867629307, + 45.52040984575685 + ], + [ + -73.58427697636428, + 45.520425946246036 + ], + [ + -73.58432737670769, + 45.520447146184345 + ], + [ + -73.58432847558765, + 45.5204475458021 + ], + [ + -73.58433617645098, + 45.52044074632136 + ], + [ + -73.58443967693627, + 45.520499446536846 + ], + [ + -73.58444017645455, + 45.520499645932155 + ], + [ + -73.58445177652922, + 45.520489545646065 + ], + [ + -73.58447697668468, + 45.52050394605542 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "OBJECTID": 156402, + "ID_UEV": "01017181", + "CIVIQUE_DE": 4111, + "CIVIQUE_FI": 4111, + "NOM_RUE": "rue de Mentana (MTL)", + "MUNICIPALI": 50, + "CODE_UTILI": 1000, + "LIBELLE_UT": "Logement", + "CATEGORIE_": "Régulier", + "Hieght_LiD": 14, + "AREA_NEW": 178, + "MBG_Width": 8, + "MBG_Length": 24, + "MBG_Orientation": 33, + "Shape_Length": 62, + "Shape_Area": 180, + "BuildingCategory": "fully-attached", + "BuildingVolume": 2492, + "AspectRatio": 3.1, + "SurfacetoVolumeRatio": 0.071, + "FloorNu_RawTax": 2, + "FloorNu_RawTax.1": 2, + "Floor_frmHieght": 4, + "TotalFloorArea": 712, + "ANNEE_CONS": 1885, + "address": 4111 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.57415074984766, + 45.52441259961631 + ], + [ + -73.57398666208603, + 45.52459078071192 + ], + [ + -73.57407170803525, + 45.524624598264325 + ], + [ + -73.57423282482344, + 45.52444964400426 + ], + [ + -73.57417797289133, + 45.524424946640266 + ], + [ + -73.57417157350089, + 45.524422047912516 + ], + [ + -73.57415074984766, + 45.52441259961631 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "OBJECTID": 156386, + "ID_UEV": "01017179", + "CIVIQUE_DE": 4115, + "CIVIQUE_FI": 4115, + "NOM_RUE": "rue de Mentana (MTL)", + "MUNICIPALI": 50, + "CODE_UTILI": 1000, + "LIBELLE_UT": "Logement", + "CATEGORIE_": "Régulier", + "Hieght_LiD": 13, + "AREA_NEW": 175, + "MBG_Width": 8, + "MBG_Length": 23, + "MBG_Orientation": 33, + "Shape_Length": 62, + "Shape_Area": 177, + "BuildingCategory": "fully-attached", + "BuildingVolume": 2275, + "AspectRatio": 3.045, + "SurfacetoVolumeRatio": 0.077, + "FloorNu_RawTax": 2, + "FloorNu_RawTax.1": 3, + "Floor_frmHieght": 4, + "TotalFloorArea": 700, + "ANNEE_CONS": 1985, + "address": 4115 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.57423282482344, + 45.52444964400426 + ], + [ + -73.57407170803525, + 45.524624598264325 + ], + [ + -73.57414677269848, + 45.52465444764032 + ], + [ + -73.57415676047877, + 45.524658409450126 + ], + [ + -73.57431495870921, + 45.52448662440602 + ], + [ + -73.57423282482344, + 45.52444964400426 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "OBJECTID": 164746, + "ID_UEV": "01016729", + "CIVIQUE_DE": 4080, + "CIVIQUE_FI": 4080, + "NOM_RUE": "rue Saint-André (MTL)", + "MUNICIPALI": 50, + "CODE_UTILI": 1000, + "LIBELLE_UT": "Logement", + "CATEGORIE_": "Régulier", + "Hieght_LiD": 7, + "AREA_NEW": 62, + "MBG_Width": 7, + "MBG_Length": 9, + "MBG_Orientation": 33, + "Shape_Length": 32, + "Shape_Area": 62, + "BuildingCategory": "detached", + "BuildingVolume": 434, + "AspectRatio": 1.233, + "SurfacetoVolumeRatio": 0.143, + "FloorNu_RawTax": 2, + "FloorNu_RawTax.1": 2, + "Floor_frmHieght": 2, + "TotalFloorArea": 124, + "ANNEE_CONS": 1885, + "address": 4080 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.57432940325116, + 45.523187518837055 + ], + [ + -73.57426832646479, + 45.52325362031764 + ], + [ + -73.57434507353524, + 45.52328794698023 + ], + [ + -73.57440467947353, + 45.523221886909894 + ], + [ + -73.57432940325116, + 45.523187518837055 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "OBJECTID": 147446, + "ID_UEV": "01016731", + "CIVIQUE_DE": 4082, + "CIVIQUE_FI": 4082, + "NOM_RUE": "rue Saint-André (MTL)", + "MUNICIPALI": 50, + "CODE_UTILI": 1000, + "LIBELLE_UT": "Logement", + "CATEGORIE_": "Régulier", + "Hieght_LiD": 12, + "AREA_NEW": 48, + "MBG_Width": 5, + "MBG_Length": 14, + "MBG_Orientation": 33, + "Shape_Length": 38, + "Shape_Area": 69, + "BuildingCategory": "semi-attached", + "BuildingVolume": 576, + "AspectRatio": 2.958, + "SurfacetoVolumeRatio": 0.083, + "FloorNu_RawTax": 2, + "FloorNu_RawTax.1": 2, + "Floor_frmHieght": 3, + "TotalFloorArea": 144, + "ANNEE_CONS": 1900, + "address": 4082 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.57430353463134, + 45.52332940534067 + ], + [ + -73.57427757285474, + 45.52335974683279 + ], + [ + -73.5742352732129, + 45.52334164705406 + ], + [ + -73.57422907262172, + 45.52333894706442 + ], + [ + -73.5741636323413, + 45.52341922560192 + ], + [ + -73.57418808958231, + 45.52343039203158 + ], + [ + -73.57420507371071, + 45.52343684724963 + ], + [ + -73.57420587303008, + 45.52343724715916 + ], + [ + -73.57421137394506, + 45.52342964713647 + ], + [ + -73.57421723454537, + 45.52343176245196 + ], + [ + -73.5743093575356, + 45.5233320632795 + ], + [ + -73.57430353463134, + 45.52332940534067 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "OBJECTID": 160240, + "ID_UEV": "01016734", + "CIVIQUE_DE": 4100, + "CIVIQUE_FI": 4100, + "NOM_RUE": "rue Saint-André (MTL)", + "MUNICIPALI": 50, + "CODE_UTILI": 1000, + "LIBELLE_UT": "Logement", + "CATEGORIE_": "Régulier", + "Hieght_LiD": 13, + "AREA_NEW": 168, + "MBG_Width": 12, + "MBG_Length": 14, + "MBG_Orientation": 122, + "Shape_Length": 53, + "Shape_Area": 176, + "BuildingCategory": "semi-attached", + "BuildingVolume": 2184, + "AspectRatio": 1.138, + "SurfacetoVolumeRatio": 0.077, + "FloorNu_RawTax": 3, + "FloorNu_RawTax.1": 3, + "Floor_frmHieght": 4, + "TotalFloorArea": 672, + "ANNEE_CONS": 1980, + "address": 4100 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.57435627280609, + 45.52348194698923 + ], + [ + -73.574403173171, + 45.5234989468212 + ], + [ + -73.57439827310195, + 45.52350494654627 + ], + [ + -73.57439997402432, + 45.52350564731358 + ], + [ + -73.5744566728788, + 45.52352684659304 + ], + [ + -73.57445817323361, + 45.52352494695276 + ], + [ + -73.57453777393754, + 45.52343404629617 + ], + [ + -73.57449487351847, + 45.523415747241344 + ], + [ + -73.57442517407421, + 45.52338594768259 + ], + [ + -73.57438644546617, + 45.52336946894452 + ], + [ + -73.57430096202904, + 45.523461983137004 + ], + [ + -73.57435627280609, + 45.52348194698923 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "OBJECTID": 139578, + "ID_UEV": "01104023", + "CIVIQUE_DE": 5320, + "CIVIQUE_FI": 5320, + "NOM_RUE": "rue Saint-Dominique (MTL)", + "MUNICIPALI": 50, + "CODE_UTILI": 1000, + "LIBELLE_UT": "Logement", + "CATEGORIE_": "Condominium", + "Hieght_LiD": 14, + "AREA_NEW": 512, + "MBG_Width": 17, + "MBG_Length": 34, + "MBG_Orientation": 123, + "Shape_Length": 101, + "Shape_Area": 560, + "BuildingCategory": "fully-attached", + "BuildingVolume": 7168, + "AspectRatio": 2.031, + "SurfacetoVolumeRatio": 0.071, + "FloorNu_RawTax": 1, + "FloorNu_RawTax.1": 14, + "Floor_frmHieght": 4, + "TotalFloorArea": 2048, + "ANNEE_CONS": 1988, + "address": 5320 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.59584527043587, + 45.52512948956751 + ], + [ + -73.59572979391517, + 45.52525400993533 + ], + [ + -73.59573708111901, + 45.525257345832664 + ], + [ + -73.59593828114156, + 45.52534844686651 + ], + [ + -73.59609192406435, + 45.52541793021932 + ], + [ + -73.59620767273658, + 45.52529262459958 + ], + [ + -73.59613878016886, + 45.52527564683736 + ], + [ + -73.59604288148485, + 45.52525194654483 + ], + [ + -73.59605258023839, + 45.52523254684448 + ], + [ + -73.59605248038461, + 45.5252325460286 + ], + [ + -73.59597958048022, + 45.52520394651073 + ], + [ + -73.5959637801297, + 45.52519754662812 + ], + [ + -73.59586518074832, + 45.525156646219315 + ], + [ + -73.5958766816196, + 45.52514304718757 + ], + [ + -73.59584527043587, + 45.52512948956751 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "OBJECTID": 132548, + "ID_UEV": "01001550", + "CIVIQUE_DE": 4575, + "CIVIQUE_FI": 4575, + "NOM_RUE": "rue Saint-Dominique (MTL)", + "MUNICIPALI": 50, + "CODE_UTILI": 1000, + "LIBELLE_UT": "Logement", + "CATEGORIE_": "Régulier", + "Hieght_LiD": 11, + "AREA_NEW": 158, + "MBG_Width": 13, + "MBG_Length": 13, + "MBG_Orientation": 123, + "Shape_Length": 52, + "Shape_Area": 167, + "BuildingCategory": "fully-attached", + "BuildingVolume": 1738, + "AspectRatio": 1.006, + "SurfacetoVolumeRatio": 0.091, + "FloorNu_RawTax": 3, + "FloorNu_RawTax.1": 3, + "Floor_frmHieght": 3, + "TotalFloorArea": 474, + "ANNEE_CONS": 1973, + "address": 4575 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.58695847537616, + 45.52116453112304 + ], + [ + -73.58688980086606, + 45.521239225136924 + ], + [ + -73.58691647787222, + 45.52125194658049 + ], + [ + -73.58689937652531, + 45.521269645953026 + ], + [ + -73.58689567734643, + 45.521273946404456 + ], + [ + -73.58700998285775, + 45.521323254327505 + ], + [ + -73.58709773159634, + 45.5212278124794 + ], + [ + -73.58708327718011, + 45.521221245667 + ], + [ + -73.5869608772182, + 45.52116564599642 + ], + [ + -73.58695847537616, + 45.52116453112304 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "OBJECTID": 163117, + "ID_UEV": "01096852", + "CIVIQUE_DE": 4580, + "CIVIQUE_FI": 4580, + "NOM_RUE": "rue Saint-Dominique (MTL)", + "MUNICIPALI": 50, + "CODE_UTILI": 1000, + "LIBELLE_UT": "Logement", + "CATEGORIE_": "Régulier", + "Hieght_LiD": 9, + "AREA_NEW": 175, + "MBG_Width": 11, + "MBG_Length": 16, + "MBG_Orientation": 33, + "Shape_Length": 56, + "Shape_Area": 187, + "BuildingCategory": "fully-attached", + "BuildingVolume": 1575, + "AspectRatio": 1.42, + "SurfacetoVolumeRatio": 0.111, + "FloorNu_RawTax": 3, + "FloorNu_RawTax.1": 3, + "Floor_frmHieght": 3, + "TotalFloorArea": 525, + "ANNEE_CONS": 1988, + "address": 4580 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.58735362685337, + 45.521008262997164 + ], + [ + -73.58724605560164, + 45.52112510849238 + ], + [ + -73.58725137773222, + 45.521127345929564 + ], + [ + -73.58724587837686, + 45.521133846077404 + ], + [ + -73.58724717729102, + 45.52113444617864 + ], + [ + -73.58729517753068, + 45.52115204547708 + ], + [ + -73.58737055475392, + 45.52117967953436 + ], + [ + -73.5874702913191, + 45.52107157197342 + ], + [ + -73.58745787835551, + 45.521064846172926 + ], + [ + -73.58741377769643, + 45.521040945975884 + ], + [ + -73.58735362685337, + 45.521008262997164 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "OBJECTID": 131179, + "ID_UEV": "01001538", + "CIVIQUE_DE": 4588, + "CIVIQUE_FI": 4588, + "NOM_RUE": "rue Saint-Dominique (MTL)", + "MUNICIPALI": 50, + "CODE_UTILI": 1000, + "LIBELLE_UT": "Logement", + "CATEGORIE_": "Régulier", + "Hieght_LiD": 8, + "AREA_NEW": 165, + "MBG_Width": 12, + "MBG_Length": 14, + "MBG_Orientation": 33, + "Shape_Length": 53, + "Shape_Area": 176, + "BuildingCategory": "fully-attached", + "BuildingVolume": 1320, + "AspectRatio": 1.185, + "SurfacetoVolumeRatio": 0.125, + "FloorNu_RawTax": 3, + "FloorNu_RawTax.1": 3, + "Floor_frmHieght": 2, + "TotalFloorArea": 330, + "ANNEE_CONS": 1987, + "address": 4588 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.5874702913191, + 45.52107157197342 + ], + [ + -73.58737055475392, + 45.52117967953436 + ], + [ + -73.58739937773738, + 45.5211902468058 + ], + [ + -73.5874671777897, + 45.52121504580049 + ], + [ + -73.58746167744097, + 45.52122254658181 + ], + [ + -73.58746267779172, + 45.52122294534497 + ], + [ + -73.5875010011558, + 45.52123991037948 + ], + [ + -73.58759421253683, + 45.52113887505566 + ], + [ + -73.58759017764646, + 45.52113714595701 + ], + [ + -73.5875905776866, + 45.52113674612117 + ], + [ + -73.5874702913191, + 45.52107157197342 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "OBJECTID": 131178, + "ID_UEV": "01001537", + "CIVIQUE_DE": 4600, + "CIVIQUE_FI": 4600, + "NOM_RUE": "rue Saint-Dominique (MTL)", + "MUNICIPALI": 50, + "CODE_UTILI": 1000, + "LIBELLE_UT": "Logement", + "CATEGORIE_": "Régulier", + "Hieght_LiD": 8, + "AREA_NEW": 164, + "MBG_Width": 12, + "MBG_Length": 14, + "MBG_Orientation": 33, + "Shape_Length": 52, + "Shape_Area": 167, + "BuildingCategory": "fully-attached", + "BuildingVolume": 1312, + "AspectRatio": 1.125, + "SurfacetoVolumeRatio": 0.125, + "FloorNu_RawTax": 3, + "FloorNu_RawTax.1": 3, + "Floor_frmHieght": 2, + "TotalFloorArea": 328, + "ANNEE_CONS": 1965, + "address": 4600 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.58759421253683, + 45.52113887505566 + ], + [ + -73.5875010011558, + 45.52123991037948 + ], + [ + -73.58754467762567, + 45.52125924597719 + ], + [ + -73.58763312829778, + 45.52129831850879 + ], + [ + -73.58772745898655, + 45.5211960665343 + ], + [ + -73.58766857753656, + 45.52117074614084 + ], + [ + -73.58759421253683, + 45.52113887505566 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "OBJECTID": 153638, + "ID_UEV": "05036130", + "CIVIQUE_DE": 4605, + "CIVIQUE_FI": 4605, + "NOM_RUE": "rue Saint-Dominique (MTL)", + "MUNICIPALI": 50, + "CODE_UTILI": 1000, + "LIBELLE_UT": "Logement", + "CATEGORIE_": "Condominium", + "Hieght_LiD": 8, + "AREA_NEW": 72, + "MBG_Width": 6, + "MBG_Length": 13, + "MBG_Orientation": 33, + "Shape_Length": 40, + "Shape_Area": 87, + "BuildingCategory": "fully-attached", + "BuildingVolume": 576, + "AspectRatio": 2.064, + "SurfacetoVolumeRatio": 0.125, + "FloorNu_RawTax": 1, + "FloorNu_RawTax.1": 4, + "Floor_frmHieght": 2, + "TotalFloorArea": 144, + "ANNEE_CONS": 1885, + "address": 4605 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.587376211751, + 45.52135441339862 + ], + [ + -73.58728334264437, + 45.52145542308364 + ], + [ + -73.5872930782179, + 45.52144754564103 + ], + [ + -73.58730407808774, + 45.52143774607229 + ], + [ + -73.58736674981574, + 45.52147207745368 + ], + [ + -73.58744582751116, + 45.52138606779943 + ], + [ + -73.587376211751, + 45.52135441339862 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "OBJECTID": 131727, + "ID_UEV": "01001536", + "CIVIQUE_DE": 4610, + "CIVIQUE_FI": 4610, + "NOM_RUE": "rue Saint-Dominique (MTL)", + "MUNICIPALI": 50, + "CODE_UTILI": 1000, + "LIBELLE_UT": "Logement", + "CATEGORIE_": "Régulier", + "Hieght_LiD": 8, + "AREA_NEW": 157, + "MBG_Width": 12, + "MBG_Length": 14, + "MBG_Orientation": 32, + "Shape_Length": 50, + "Shape_Area": 158, + "BuildingCategory": "semi-attached", + "BuildingVolume": 1256, + "AspectRatio": 1.171, + "SurfacetoVolumeRatio": 0.125, + "FloorNu_RawTax": 3, + "FloorNu_RawTax.1": 3, + "Floor_frmHieght": 2, + "TotalFloorArea": 314, + "ANNEE_CONS": 1965, + "address": 4610 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.58772745898655, + 45.5211960665343 + ], + [ + -73.58763312829778, + 45.52129831850879 + ], + [ + -73.58771717774853, + 45.52133544619716 + ], + [ + -73.58775917760998, + 45.52135404603081 + ], + [ + -73.58785207766479, + 45.52125054607517 + ], + [ + -73.58777667834441, + 45.52121714617615 + ], + [ + -73.5877764786541, + 45.52121714632959 + ], + [ + -73.58772745898655, + 45.5211960665343 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "OBJECTID": 157628, + "ID_UEV": "01001554", + "CIVIQUE_DE": 4611, + "CIVIQUE_FI": 4611, + "NOM_RUE": "rue Saint-Dominique (MTL)", + "MUNICIPALI": 50, + "CODE_UTILI": 1000, + "LIBELLE_UT": "Logement", + "CATEGORIE_": "Régulier", + "Hieght_LiD": 7, + "AREA_NEW": 75, + "MBG_Width": 6, + "MBG_Length": 12, + "MBG_Orientation": 33, + "Shape_Length": 37, + "Shape_Area": 77, + "BuildingCategory": "fully-attached", + "BuildingVolume": 525, + "AspectRatio": 1.845, + "SurfacetoVolumeRatio": 0.143, + "FloorNu_RawTax": 2, + "FloorNu_RawTax.1": 2, + "Floor_frmHieght": 2, + "TotalFloorArea": 150, + "ANNEE_CONS": 1957, + "address": 4611 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.58744582751116, + 45.52138606779943 + ], + [ + -73.58736674981574, + 45.52147207745368 + ], + [ + -73.58743240416018, + 45.52150804062752 + ], + [ + -73.58749775952731, + 45.521436957192954 + ], + [ + -73.58749487845772, + 45.521435546650515 + ], + [ + -73.5875133772338, + 45.52141684549636 + ], + [ + -73.58751307738757, + 45.52141664596154 + ], + [ + -73.58744582751116, + 45.52138606779943 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "OBJECTID": 139181, + "ID_UEV": "01093152", + "CIVIQUE_DE": 5155, + "CIVIQUE_FI": 5155, + "NOM_RUE": "avenue Henri-Julien (MTL)", + "MUNICIPALI": 50, + "CODE_UTILI": 1000, + "LIBELLE_UT": "Logement", + "CATEGORIE_": "Régulier", + "Hieght_LiD": 8, + "AREA_NEW": 64, + "MBG_Width": 6, + "MBG_Length": 11, + "MBG_Orientation": 31, + "Shape_Length": 35, + "Shape_Area": 69, + "BuildingCategory": "fully-attached", + "BuildingVolume": 512, + "AspectRatio": 1.857, + "SurfacetoVolumeRatio": 0.125, + "FloorNu_RawTax": 2, + "FloorNu_RawTax.1": 2, + "Floor_frmHieght": 2, + "TotalFloorArea": 128, + "ANNEE_CONS": 1900, + "address": 5155 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.59126346415185, + 45.52628763822661 + ], + [ + -73.59119570372313, + 45.52636584520534 + ], + [ + -73.59118857579593, + 45.52637464682695 + ], + [ + -73.59124987939587, + 45.52639644687538 + ], + [ + -73.59127027985483, + 45.52637234637264 + ], + [ + -73.59127770650177, + 45.52637569593996 + ], + [ + -73.59132815781368, + 45.52631826565003 + ], + [ + -73.59127107939297, + 45.526291246797555 + ], + [ + -73.59126346415185, + 45.52628763822661 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "OBJECTID": 166967, + "ID_UEV": "01093155", + "CIVIQUE_DE": 5159, + "CIVIQUE_FI": 5159, + "NOM_RUE": "avenue Henri-Julien (MTL)", + "MUNICIPALI": 50, + "CODE_UTILI": 1000, + "LIBELLE_UT": "Logement", + "CATEGORIE_": "Régulier", + "Hieght_LiD": 8, + "AREA_NEW": 51, + "MBG_Width": 7, + "MBG_Length": 7, + "MBG_Orientation": 34, + "Shape_Length": 29, + "Shape_Area": 52, + "BuildingCategory": "semi-attached", + "BuildingVolume": 408, + "AspectRatio": 1.071, + "SurfacetoVolumeRatio": 0.125, + "FloorNu_RawTax": 2, + "FloorNu_RawTax.1": 2, + "Floor_frmHieght": 2, + "TotalFloorArea": 102, + "ANNEE_CONS": 1900, + "address": 5159 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.59132527958762, + 45.52639714698301 + ], + [ + -73.59134947945634, + 45.52640854749608 + ], + [ + -73.59140247950297, + 45.52635344661957 + ], + [ + -73.59132815781368, + 45.52631826565003 + ], + [ + -73.59127770650177, + 45.52637569593996 + ], + [ + -73.59132527958762, + 45.52639714698301 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "OBJECTID": 139056, + "ID_UEV": "01093163", + "CIVIQUE_DE": 5179, + "CIVIQUE_FI": 5179, + "NOM_RUE": "avenue Henri-Julien (MTL)", + "MUNICIPALI": 50, + "CODE_UTILI": 1000, + "LIBELLE_UT": "Logement", + "CATEGORIE_": "Régulier", + "Hieght_LiD": 11, + "AREA_NEW": 114, + "MBG_Width": 9, + "MBG_Length": 14, + "MBG_Orientation": 33, + "Shape_Length": 45, + "Shape_Area": 119, + "BuildingCategory": "semi-attached", + "BuildingVolume": 1254, + "AspectRatio": 1.565, + "SurfacetoVolumeRatio": 0.091, + "FloorNu_RawTax": 3, + "FloorNu_RawTax.1": 3, + "Floor_frmHieght": 3, + "TotalFloorArea": 342, + "ANNEE_CONS": 1959, + "address": 5179 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.5915142796455, + 45.52655184730195 + ], + [ + -73.59157837902634, + 45.52658124677747 + ], + [ + -73.591673979158, + 45.52647844711825 + ], + [ + -73.59158051856348, + 45.526435507058615 + ], + [ + -73.59149342764714, + 45.52654219433043 + ], + [ + -73.5915142796455, + 45.52655184730195 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "OBJECTID": 161696, + "ID_UEV": "01128422", + "CIVIQUE_DE": 220, + "CIVIQUE_FI": 220, + "NOM_RUE": "avenue Laurier Est (MTL)", + "MUNICIPALI": 50, + "CODE_UTILI": 1000, + "LIBELLE_UT": "Logement", + "CATEGORIE_": "Régulier", + "Hieght_LiD": 14, + "AREA_NEW": 98, + "MBG_Width": 7, + "MBG_Length": 14, + "MBG_Orientation": 123, + "Shape_Length": 42, + "Shape_Area": 101, + "BuildingCategory": "fully-attached", + "BuildingVolume": 1372, + "AspectRatio": 1.961, + "SurfacetoVolumeRatio": 0.071, + "FloorNu_RawTax": 3, + "FloorNu_RawTax.1": 3, + "Floor_frmHieght": 4, + "TotalFloorArea": 392, + "ANNEE_CONS": 1995, + "address": 220 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.59055762474266, + 45.525262900234246 + ], + [ + -73.59050472066473, + 45.5253154559067 + ], + [ + -73.59065548171333, + 45.52538423528219 + ], + [ + -73.59070251298645, + 45.52532889740045 + ], + [ + -73.59055762474266, + 45.525262900234246 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "OBJECTID": 139317, + "ID_UEV": "01117392", + "CIVIQUE_DE": 3850, + "CIVIQUE_FI": 3850, + "NOM_RUE": "rue Rivard (MTL)", + "MUNICIPALI": 50, + "CODE_UTILI": 1000, + "LIBELLE_UT": "Logement", + "CATEGORIE_": "Régulier", + "Hieght_LiD": 11, + "AREA_NEW": 222, + "MBG_Width": 18, + "MBG_Length": 19, + "MBG_Orientation": 33, + "Shape_Length": 75, + "Shape_Area": 354, + "BuildingCategory": "fully-attached", + "BuildingVolume": 2442, + "AspectRatio": 1.059, + "SurfacetoVolumeRatio": 0.091, + "FloorNu_RawTax": 3, + "FloorNu_RawTax.1": 3, + "Floor_frmHieght": 3, + "TotalFloorArea": 666, + "ANNEE_CONS": 1976, + "address": 3850 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.57295066832759, + 45.51992769609667 + ], + [ + -73.57281611225557, + 45.52007378287291 + ], + [ + -73.57293037356249, + 45.52012504677861 + ], + [ + -73.57301344918426, + 45.52016231549183 + ], + [ + -73.5730921418857, + 45.52007687930088 + ], + [ + -73.57301457262967, + 45.52004124663418 + ], + [ + -73.57303477290998, + 45.52001944588219 + ], + [ + -73.57302717262223, + 45.52001594586261 + ], + [ + -73.5730083735848, + 45.52000774593498 + ], + [ + -73.57298747288253, + 45.52003134591106 + ], + [ + -73.57290607263843, + 45.51999574621277 + ], + [ + -73.57296197234393, + 45.51993264609835 + ], + [ + -73.57295066832759, + 45.51992769609667 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "OBJECTID": 162708, + "ID_UEV": "01042262", + "CIVIQUE_DE": 3800, + "CIVIQUE_FI": 3800, + "NOM_RUE": "rue De Bullion (MTL)", + "MUNICIPALI": 50, + "CODE_UTILI": 1000, + "LIBELLE_UT": "Logement", + "CATEGORIE_": "Régulier", + "Hieght_LiD": 10, + "AREA_NEW": 127, + "MBG_Width": 10, + "MBG_Length": 18, + "MBG_Orientation": 34, + "Shape_Length": 57, + "Shape_Area": 191, + "BuildingCategory": "semi-attached", + "BuildingVolume": 1270, + "AspectRatio": 1.756, + "SurfacetoVolumeRatio": 0.1, + "FloorNu_RawTax": 3, + "FloorNu_RawTax.1": 3, + "Floor_frmHieght": 3, + "TotalFloorArea": 381, + "ANNEE_CONS": 1980, + "address": 3800 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.57508577341095, + 45.51670754522258 + ], + [ + -73.57500597326535, + 45.51679564619446 + ], + [ + -73.57500607310273, + 45.516795646128905 + ], + [ + -73.57511235197389, + 45.51684902774917 + ], + [ + -73.57524184013074, + 45.5167117977588 + ], + [ + -73.57523387374518, + 45.51670804526983 + ], + [ + -73.57523087305128, + 45.51671154582807 + ], + [ + -73.57519167303518, + 45.516754945775844 + ], + [ + -73.57508577341095, + 45.51670754522258 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "OBJECTID": 162718, + "ID_UEV": "01042296", + "CIVIQUE_DE": 3995, + "CIVIQUE_FI": 3995, + "NOM_RUE": "rue De Bullion (MTL)", + "MUNICIPALI": 50, + "CODE_UTILI": 1000, + "LIBELLE_UT": "Logement", + "CATEGORIE_": "Régulier", + "Hieght_LiD": 17, + "AREA_NEW": 100, + "MBG_Width": 9, + "MBG_Length": 11, + "MBG_Orientation": 34, + "Shape_Length": 40, + "Shape_Area": 101, + "BuildingCategory": "semi-attached", + "BuildingVolume": 1700, + "AspectRatio": 1.197, + "SurfacetoVolumeRatio": 0.059, + "FloorNu_RawTax": 3, + "FloorNu_RawTax.1": 3, + "Floor_frmHieght": 5, + "TotalFloorArea": 500, + "ANNEE_CONS": 1980, + "address": 3995 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.57736211303495, + 45.5180978947712 + ], + [ + -73.57728503141776, + 45.51818095636402 + ], + [ + -73.57738017381428, + 45.518225846199066 + ], + [ + -73.57738287333515, + 45.51822304586547 + ], + [ + -73.57745927404018, + 45.51814474572666 + ], + [ + -73.57736211303495, + 45.5180978947712 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "OBJECTID": 162726, + "ID_UEV": "01001802", + "CIVIQUE_DE": 4530, + "CIVIQUE_FI": 4530, + "NOM_RUE": "avenue Henri-Julien (MTL)", + "MUNICIPALI": 50, + "CODE_UTILI": 1000, + "LIBELLE_UT": "Logement", + "CATEGORIE_": "Régulier", + "Hieght_LiD": 9, + "AREA_NEW": 158, + "MBG_Width": 12, + "MBG_Length": 13, + "MBG_Orientation": 122, + "Shape_Length": 50, + "Shape_Area": 158, + "BuildingCategory": "detached", + "BuildingVolume": 1422, + "AspectRatio": 1.034, + "SurfacetoVolumeRatio": 0.111, + "FloorNu_RawTax": 3, + "FloorNu_RawTax.1": 3, + "Floor_frmHieght": 3, + "TotalFloorArea": 474, + "ANNEE_CONS": 1980, + "address": 4530 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.58452997610767, + 45.52303594658955 + ], + [ + -73.58466847666706, + 45.523096845969846 + ], + [ + -73.58475237701808, + 45.5230026469856 + ], + [ + -73.58461387659825, + 45.52294174680608 + ], + [ + -73.58452997610767, + 45.52303594658955 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "OBJECTID": 162727, + "ID_UEV": "01001804", + "CIVIQUE_DE": 4535, + "CIVIQUE_FI": 4535, + "NOM_RUE": "avenue Henri-Julien (MTL)", + "MUNICIPALI": 50, + "CODE_UTILI": 1000, + "LIBELLE_UT": "Logement", + "CATEGORIE_": "Régulier", + "Hieght_LiD": 11, + "AREA_NEW": 161, + "MBG_Width": 12, + "MBG_Length": 14, + "MBG_Orientation": 118, + "Shape_Length": 52, + "Shape_Area": 169, + "BuildingCategory": "semi-attached", + "BuildingVolume": 1771, + "AspectRatio": 1.118, + "SurfacetoVolumeRatio": 0.091, + "FloorNu_RawTax": 2, + "FloorNu_RawTax.1": 2, + "Floor_frmHieght": 3, + "TotalFloorArea": 483, + "ANNEE_CONS": 1980, + "address": 4535 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.58450432810098, + 45.52331827507302 + ], + [ + -73.58447006311711, + 45.52335752894066 + ], + [ + -73.58443087735714, + 45.52340964694795 + ], + [ + -73.58442807687385, + 45.52341334646033 + ], + [ + -73.584574755506, + 45.52346817048485 + ], + [ + -73.58465645600157, + 45.52337479655955 + ], + [ + -73.5845745774021, + 45.5233467462103 + ], + [ + -73.584576876816, + 45.523343546476525 + ], + [ + -73.58450537707058, + 45.523318645930914 + ], + [ + -73.58450432810098, + 45.52331827507302 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "OBJECTID": 131611, + "ID_UEV": "01042169", + "CIVIQUE_DE": 3625, + "CIVIQUE_FI": 3625, + "NOM_RUE": "avenue Coloniale (MTL)", + "MUNICIPALI": 50, + "CODE_UTILI": 1000, + "LIBELLE_UT": "Logement", + "CATEGORIE_": "Régulier", + "Hieght_LiD": 13, + "AREA_NEW": 108, + "MBG_Width": 10, + "MBG_Length": 11, + "MBG_Orientation": 123, + "Shape_Length": 42, + "Shape_Area": 110, + "BuildingCategory": "semi-attached", + "BuildingVolume": 1404, + "AspectRatio": 1.049, + "SurfacetoVolumeRatio": 0.077, + "FloorNu_RawTax": 2, + "FloorNu_RawTax.1": 3, + "Floor_frmHieght": 4, + "TotalFloorArea": 432, + "ANNEE_CONS": 1973, + "address": 3625 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.57205237165654, + 45.51502834545008 + ], + [ + -73.57204877235876, + 45.51503224583288 + ], + [ + -73.57197897192349, + 45.515104845840966 + ], + [ + -73.5719842726097, + 45.51510724597676 + ], + [ + -73.57209427271673, + 45.51515771168995 + ], + [ + -73.5721643414897, + 45.515081822522674 + ], + [ + -73.5721421725414, + 45.51507134617621 + ], + [ + -73.57205237165654, + 45.51502834545008 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "OBJECTID": 134547, + "ID_UEV": "01042129", + "CIVIQUE_DE": 3875, + "CIVIQUE_FI": 3875, + "NOM_RUE": "avenue Coloniale (MTL)", + "MUNICIPALI": 50, + "CODE_UTILI": 1000, + "LIBELLE_UT": "Logement", + "CATEGORIE_": "Régulier", + "Hieght_LiD": 27, + "AREA_NEW": 112, + "MBG_Width": 11, + "MBG_Length": 14, + "MBG_Orientation": 33, + "Shape_Length": 50, + "Shape_Area": 155, + "BuildingCategory": "fully-attached", + "BuildingVolume": 3024, + "AspectRatio": 1.289, + "SurfacetoVolumeRatio": 0.037, + "FloorNu_RawTax": 3, + "FloorNu_RawTax.1": 3, + "Floor_frmHieght": 7, + "TotalFloorArea": 784, + "ANNEE_CONS": 1973, + "address": 3875 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -73.57600009852351, + 45.51682149480924 + ], + [ + -73.57590854825408, + 45.516921029508254 + ], + [ + -73.57593577350661, + 45.51689364634819 + ], + [ + -73.576049597105, + 45.51694951913854 + ], + [ + -73.57612428015403, + 45.516868322630735 + ], + [ + -73.57611917371327, + 45.51686594595422 + ], + [ + -73.57605227264045, + 45.51683484521904 + ], + [ + -73.5760449729797, + 45.51684244563685 + ], + [ + -73.57600009852351, + 45.51682149480924 + ] + ] + ] + } + } + ] +} \ No newline at end of file diff --git a/energy_system_modelling_package/energy_system_modelling_factories/archetypes/montreal/archetype_cluster_1.py b/energy_system_modelling_package/energy_system_modelling_factories/archetypes/montreal/archetype_cluster_1.py new file mode 100644 index 00000000..e66140d8 --- /dev/null +++ b/energy_system_modelling_package/energy_system_modelling_factories/archetypes/montreal/archetype_cluster_1.py @@ -0,0 +1,147 @@ +import csv + +from energy_system_modelling_package.energy_system_modelling_factories.hvac_dhw_systems_simulation_models.heat_pump_boiler_tes_heating import \ + HeatPumpBoilerTesHeating +from energy_system_modelling_package.energy_system_modelling_factories.hvac_dhw_systems_simulation_models.heat_pump_cooling import \ + HeatPumpCooling +from energy_system_modelling_package.energy_system_modelling_factories.hvac_dhw_systems_simulation_models.domestic_hot_water_heat_pump_with_tes import \ + DomesticHotWaterHeatPumpTes +from energy_system_modelling_package.energy_system_modelling_factories.pv_assessment.pv_model import PVModel +from energy_system_modelling_package.energy_system_modelling_factories.pv_assessment.electricity_demand_calculator import HourlyElectricityDemand +import hub.helpers.constants as cte +from hub.helpers.monthly_values import MonthlyValues + + +class ArchetypeCluster1: + def __init__(self, building, dt, output_path, csv_output=True): + self.building = building + self.dt = dt + self.output_path = output_path + self.csv_output = csv_output + self.heating_results, self.building_heating_hourly_consumption = self.heating_system_simulation() + self.cooling_results, self.total_cooling_consumption_hourly = self.cooling_system_simulation() + self.dhw_results, self.total_dhw_consumption_hourly = self.dhw_system_simulation() + if 'PV' in self.building.energy_systems_archetype_name: + self.pv_results = self.pv_system_simulation() + else: + self.pv_results = None + + def heating_system_simulation(self): + building_heating_hourly_consumption = [] + boiler = self.building.energy_systems[1].generation_systems[0] + hp = self.building.energy_systems[1].generation_systems[1] + tes = self.building.energy_systems[1].generation_systems[0].energy_storage_systems[0] + heating_demand_joules = self.building.heating_demand[cte.HOUR] + heating_peak_load_watts = self.building.heating_peak_load[cte.YEAR][0] + upper_limit_tes_heating = 55 + outdoor_temperature = self.building.external_temperature[cte.HOUR] + results = HeatPumpBoilerTesHeating(hp=hp, + boiler=boiler, + tes=tes, + hourly_heating_demand_joules=heating_demand_joules, + heating_peak_load_watts=heating_peak_load_watts, + upper_limit_tes=upper_limit_tes_heating, + outdoor_temperature=outdoor_temperature, + dt=self.dt).simulation() + number_of_ts = int(cte.HOUR_TO_SECONDS/self.dt) + heating_consumption_joules = [(x * cte.WATTS_HOUR_TO_JULES) / number_of_ts for x in + results['Total Heating Power Consumption (W)']] + heating_consumption = 0 + for i in range(1, len(heating_consumption_joules)): + heating_consumption += heating_consumption_joules[i] + if (i - 1) % number_of_ts == 0: + building_heating_hourly_consumption.append(heating_consumption) + heating_consumption = 0 + return results, building_heating_hourly_consumption + + def cooling_system_simulation(self): + hp = self.building.energy_systems[2].generation_systems[0] + cooling_demand_joules = self.building.cooling_demand[cte.HOUR] + cooling_peak_load = self.building.cooling_peak_load[cte.YEAR][0] + cutoff_temperature = 11 + outdoor_temperature = self.building.external_temperature[cte.HOUR] + results = HeatPumpCooling(hp=hp, + hourly_cooling_demand_joules=cooling_demand_joules, + cooling_peak_load_watts=cooling_peak_load, + cutoff_temperature=cutoff_temperature, + outdoor_temperature=outdoor_temperature, + dt=self.dt).simulation() + building_cooling_hourly_consumption = hp.energy_consumption[cte.COOLING][cte.HOUR] + return results, building_cooling_hourly_consumption + + def dhw_system_simulation(self): + building_dhw_hourly_consumption = [] + hp = self.building.energy_systems[-1].generation_systems[0] + tes = self.building.energy_systems[-1].generation_systems[0].energy_storage_systems[0] + dhw_demand_joules = self.building.domestic_hot_water_heat_demand[cte.HOUR] + upper_limit_tes = 65 + outdoor_temperature = self.building.external_temperature[cte.HOUR] + results = DomesticHotWaterHeatPumpTes(hp=hp, + tes=tes, + hourly_dhw_demand_joules=dhw_demand_joules, + upper_limit_tes=upper_limit_tes, + outdoor_temperature=outdoor_temperature, + dt=self.dt).simulation() + number_of_ts = int(cte.HOUR_TO_SECONDS/self.dt) + dhw_consumption_joules = [(x * cte.WATTS_HOUR_TO_JULES) / number_of_ts for x in + results['Total DHW Power Consumption (W)']] + dhw_consumption = 0 + for i in range(1, len(dhw_consumption_joules)): + dhw_consumption += dhw_consumption_joules[i] + if (i - 1) % number_of_ts == 0: + building_dhw_hourly_consumption.append(dhw_consumption) + dhw_consumption = 0 + return results, building_dhw_hourly_consumption + + def pv_system_simulation(self): + results = None + pv = self.building.energy_systems[0].generation_systems[0] + hourly_electricity_demand = HourlyElectricityDemand(self.building).calculate() + model_type = 'fixed_efficiency' + if model_type == 'fixed_efficiency': + results = PVModel(pv=pv, + hourly_electricity_demand_joules=hourly_electricity_demand, + solar_radiation=self.building.roofs[0].global_irradiance_tilted[cte.HOUR], + installed_pv_area=self.building.roofs[0].installed_solar_collector_area, + model_type='fixed_efficiency').fixed_efficiency() + return results + + def enrich_building(self): + results = self.heating_results | self.cooling_results | self.dhw_results + self.building.heating_consumption[cte.HOUR] = self.building_heating_hourly_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] = self.total_cooling_consumption_hourly + 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] = self.total_dhw_consumption_hourly + 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])] + if self.pv_results is not None: + self.building.onsite_electrical_production[cte.HOUR] = [x * cte.WATTS_HOUR_TO_JULES for x in + self.pv_results['PV Output (W)']] + 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])] + if self.csv_output: + file_name = f'pv_system_simulation_results_{self.building.name}.csv' + with open(self.output_path / file_name, 'w', newline='') as csvfile: + output_file = csv.writer(csvfile) + # Write header + output_file.writerow(self.pv_results.keys()) + # Write data + output_file.writerows(zip(*self.pv_results.values())) + if self.csv_output: + file_name = f'energy_system_simulation_results_{self.building.name}.csv' + with open(self.output_path / file_name, 'w', newline='') as csvfile: + output_file = csv.writer(csvfile) + # Write header + output_file.writerow(results.keys()) + # Write data + output_file.writerows(zip(*results.values())) + + + diff --git a/energy_system_modelling_package/energy_system_modelling_factories/energy_system_sizing_factory.py b/energy_system_modelling_package/energy_system_modelling_factories/energy_system_sizing_factory.py new file mode 100644 index 00000000..8ea92fae --- /dev/null +++ b/energy_system_modelling_package/energy_system_modelling_factories/energy_system_sizing_factory.py @@ -0,0 +1,79 @@ +""" +EnergySystemSizingSimulationFactory retrieve the energy system archetype sizing and simulation module +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2024 Concordia CERC group +Project Coder Saeed Ranjbar saeed.ranjbar@mail.concordia.ca +""" +from energy_system_modelling_package.energy_system_modelling_factories.system_sizing_methods.optimal_sizing import \ + OptimalSizing +from energy_system_modelling_package.energy_system_modelling_factories.system_sizing_methods.peak_load_sizing import \ + PeakLoadSizing +from energy_system_modelling_package.energy_system_modelling_factories.pv_assessment.pv_sizing import PVSizing + + +class EnergySystemsSizingFactory: + """ + EnergySystemsFactory class + """ + + def __init__(self, handler, city): + self._handler = '_' + handler.lower() + self._city = city + + def _peak_load_sizing(self): + """ + Size Energy Systems based on a Load Matching method using the heating, cooling, and dhw peak loads + """ + PeakLoadSizing(self._city).enrich_buildings() + self._city.level_of_detail.energy_systems = 1 + for building in self._city.buildings: + building.level_of_detail.energy_systems = 1 + + def _optimal_sizing(self): + """ + Size Energy Systems using a Single or Multi Objective GA + """ + OptimalSizing(self._city, optimization_scenario='cost_energy_consumption').enrich_buildings() + self._city.level_of_detail.energy_systems = 1 + for building in self._city.buildings: + building.level_of_detail.energy_systems = 1 + + def _pv_sizing(self): + """ + Size rooftop, facade or mixture of them for buildings + """ + system_type = 'rooftop' + results = {} + if system_type == 'rooftop': + surface_azimuth = 180 + maintenance_factor = 0.1 + mechanical_equipment_factor = 0.3 + orientation_factor = 0.1 + tilt_angle = self._city.latitude + pv_sizing = PVSizing(self._city, + tilt_angle=tilt_angle, + surface_azimuth=surface_azimuth, + mechanical_equipment_factor=mechanical_equipment_factor, + maintenance_factor=maintenance_factor, + orientation_factor=orientation_factor, + system_type=system_type) + results = pv_sizing.rooftop_sizing() + pv_sizing.rooftop_tilted_radiation() + + self._city.level_of_detail.energy_systems = 1 + for building in self._city.buildings: + building.level_of_detail.energy_systems = 1 + return results + + def _district_heating_cooling_sizing(self): + """ + Size District Heating and Cooling Network + """ + pass + + def enrich(self): + """ + Enrich the city given to the class using the class given handler + :return: None + """ + return getattr(self, self._handler, lambda: None)() diff --git a/energy_system_modelling_package/energy_system_modelling_factories/hvac_dhw_systems_simulation_models/domestic_hot_water_heat_pump_with_tes.py b/energy_system_modelling_package/energy_system_modelling_factories/hvac_dhw_systems_simulation_models/domestic_hot_water_heat_pump_with_tes.py new file mode 100644 index 00000000..1034ce63 --- /dev/null +++ b/energy_system_modelling_package/energy_system_modelling_factories/hvac_dhw_systems_simulation_models/domestic_hot_water_heat_pump_with_tes.py @@ -0,0 +1,120 @@ +import hub.helpers.constants as cte +from energy_system_modelling_package.energy_system_modelling_factories.hvac_dhw_systems_simulation_models.heat_pump_characteristics import HeatPump +from energy_system_modelling_package.energy_system_modelling_factories.hvac_dhw_systems_simulation_models.thermal_storage_tank import StorageTank +from hub.helpers.monthly_values import MonthlyValues + + +class DomesticHotWaterHeatPumpTes: + def __init__(self, hp, tes, hourly_dhw_demand_joules, upper_limit_tes, + outdoor_temperature, dt=None): + self.hp = hp + self.tes = tes + self.dhw_demand = [demand / cte.WATTS_HOUR_TO_JULES for demand in hourly_dhw_demand_joules] + self.upper_limit_tes = upper_limit_tes + self.hp_characteristics = HeatPump(self.hp, outdoor_temperature) + self.t_out = outdoor_temperature + self.dt = dt + self.results = {} + + def simulation(self): + hp = self.hp + tes = self.tes + heating_coil_nominal_output = 0 + if tes.heating_coil_capacity is not None: + heating_coil_nominal_output = float(tes.heating_coil_capacity) + storage_tank = StorageTank(volume=float(tes.volume), + height=float(tes.height), + material_layers=tes.layers, + heating_coil_capacity=heating_coil_nominal_output) + + hp_delta_t = 8 + number_of_ts = int(cte.HOUR_TO_SECONDS / self.dt) + source_temperature_hourly = self.hp_characteristics.hp_source_temperature() + source_temperature = [0] + [x for x in source_temperature_hourly for _ in range(number_of_ts)] + demand = [0] + [x for x in self.dhw_demand 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)", "total_consumption"] + 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, total_consumption) = \ + [variables[name] for name in variable_names] + freshwater_temperature = 18 + t_tank[0] = 65 + for i in range(len(demand) - 1): + delta_t_demand = demand[i] * (self.dt / (cte.WATER_DENSITY * cte.WATER_HEAT_CAPACITY * + storage_tank.volume)) + if t_tank[i] < self.upper_limit_tes: + q_hp[i] = hp.nominal_heat_output + delta_t_hp = q_hp[i] * (self.dt / (cte.WATER_DENSITY * cte.WATER_HEAT_CAPACITY * storage_tank.volume)) + 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 / (storage_tank.volume * + cte.WATER_DENSITY)) + if t_tank[i] < 60: + q_coil[i] = float(storage_tank.heating_coil_capacity) + delta_t_coil = q_coil[i] * (self.dt / (cte.WATER_DENSITY * cte.WATER_HEAT_CAPACITY * storage_tank.volume)) + 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] + if q_hp[i] > 0: + if hp.source_medium == cte.AIR and hp.supply_medium == cte.WATER: + hp_cop[i] = self.hp_characteristics.air_to_water_cop(source_temperature[i], t_tank[i], + mode=cte.DOMESTIC_HOT_WATER) + 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) + total_consumption[i] = hp_electricity[i] + q_coil[i] + 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 + if len(self.dhw_demand) == 8760: + 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])] + if self.tes.heating_coil_capacity is not None: + tes.heating_coil_energy_consumption[cte.DOMESTIC_HOT_WATER] = {} + tes.heating_coil_energy_consumption[cte.DOMESTIC_HOT_WATER][cte.HOUR] = coil_hourly + if len(self.dhw_demand) == 8760: + tes.heating_coil_energy_consumption[cte.DOMESTIC_HOT_WATER][cte.MONTH] = MonthlyValues.get_total_month( + tes.heating_coil_energy_consumption[cte.DOMESTIC_HOT_WATER][cte.HOUR]) + tes.heating_coil_energy_consumption[cte.DOMESTIC_HOT_WATER][cte.YEAR] = [ + sum(tes.heating_coil_energy_consumption[cte.DOMESTIC_HOT_WATER][cte.MONTH])] + + 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'] = source_temperature + 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 + self.results['Total DHW Power Consumption (W)'] = total_consumption + return self.results diff --git a/energy_system_modelling_package/energy_system_modelling_factories/hvac_dhw_systems_simulation_models/heat_pump_boiler_tes_heating.py b/energy_system_modelling_package/energy_system_modelling_factories/hvac_dhw_systems_simulation_models/heat_pump_boiler_tes_heating.py new file mode 100644 index 00000000..affaa871 --- /dev/null +++ b/energy_system_modelling_package/energy_system_modelling_factories/hvac_dhw_systems_simulation_models/heat_pump_boiler_tes_heating.py @@ -0,0 +1,171 @@ +import hub.helpers.constants as cte +from hub.helpers.monthly_values import MonthlyValues +from energy_system_modelling_package.energy_system_modelling_factories.hvac_dhw_systems_simulation_models.heat_pump_characteristics import \ + HeatPump +from energy_system_modelling_package.energy_system_modelling_factories.hvac_dhw_systems_simulation_models.thermal_storage_tank import \ + StorageTank + + +class HeatPumpBoilerTesHeating: + def __init__(self, hp, boiler, tes, hourly_heating_demand_joules, heating_peak_load_watts, upper_limit_tes, + outdoor_temperature, dt=None): + self.hp = hp + self.boiler = boiler + self.tes = tes + self.heating_demand = [demand / cte.WATTS_HOUR_TO_JULES for demand in hourly_heating_demand_joules] + if heating_peak_load_watts is not None: + self.heating_peak_load = heating_peak_load_watts + else: + self.heating_peak_load = max(hourly_heating_demand_joules) / cte.HOUR_TO_SECONDS + self.upper_limit_tes = upper_limit_tes + self.hp_characteristics = HeatPump(self.hp, outdoor_temperature) + self.t_out = outdoor_temperature + self.dt = dt + self.results = {} + + def simulation(self): + hp, boiler, tes = self.hp, self.boiler, self.tes + heating_coil_nominal_output = 0 + if tes.heating_coil_capacity is not None: + heating_coil_nominal_output = float(tes.heating_coil_capacity) + storage_tank = StorageTank(volume=float(tes.volume), + height=float(tes.height), + material_layers=tes.layers, + heating_coil_capacity=heating_coil_nominal_output) + number_of_ts = int(cte.HOUR_TO_SECONDS / self.dt) + demand = [0] + [x for x in self.heating_demand for _ in range(number_of_ts)] + t_out = [0] + [x for x in self.t_out for _ in range(number_of_ts)] + source_temperature_hourly = self.hp_characteristics.hp_source_temperature() + source_temperature = [0] + [x for x in source_temperature_hourly for _ in range(number_of_ts)] + 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", "heating_coil_output", "total_heating_energy_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_fuel_consumption, t_sup_boiler, boiler_energy_consumption, heating_consumption, q_coil, + total_consumption) = [variables[name] for name in variable_names] + t_tank[0] = self.upper_limit_tes + hp_delta_t = 5 + # storage temperature prediction + for i in range(len(demand) - 1): + t_tank[i + 1] = storage_tank.calculate_space_heating_fully_mixed(charging_flow_rate=m_ch[i], + discharging_flow_rate=m_dis[i], + supply_temperature=t_sup_boiler[i], + return_temperature=t_ret[i], + current_tank_temperature=t_tank[i], + heat_generator_input=q_coil[i], + ambient_temperature=t_out[i], + dt=self.dt) + # hp operation + if t_tank[i + 1] < 40: + q_hp[i + 1] = hp.nominal_heat_output + 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] < self.upper_limit_tes 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] < self.upper_limit_tes and q_hp[i] > 0: + q_hp[i + 1] = hp.nominal_heat_output + 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] + if q_hp[i + 1] > 0: + if hp.source_medium == cte.AIR and self.hp.supply_medium == cte.WATER: + hp_cop[i + 1] = self.hp_characteristics.air_to_water_cop(source_temperature[i + 1], t_tank[i + 1], mode=cte.HEATING) + 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.nominal_heat_output + elif demand[i + 1] > 0.5 * self.heating_peak_load / self.dt: + q_boiler[i + 1] = 0.5 * boiler.nominal_heat_output + boiler_energy_consumption[i + 1] = q_boiler[i + 1] / float(boiler.heat_efficiency) + if boiler.fuel_type == cte.ELECTRICITY: + boiler_fuel_consumption[i + 1] = boiler_energy_consumption[i + 1] + else: + # TODO: Other fuels should be considered + boiler_fuel_consumption[i + 1] = (q_boiler[i + 1] * self.dt) / ( + float(boiler.heat_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)) + # heating coil operation + if t_tank[i + 1] < 35: + q_coil[i + 1] = heating_coil_nominal_output + # 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: + factor = 8 + else: + factor = 4 + 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) + # total consumption + total_consumption[i + 1] = hp_electricity[i + 1] + boiler_energy_consumption[i + 1] + q_coil[i + 1] + 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] + heating_coil_j = [(x * cte.WATTS_HOUR_TO_JULES) / number_of_ts for x in q_coil] + hp_hourly = [] + boiler_hourly = [] + coil_hourly = [] + boiler_sum = 0 + hp_sum = 0 + coil_sum = 0 + for i in range(1, len(demand)): + hp_sum += hp_electricity_j[i] + boiler_sum += boiler_consumption_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) + boiler_hourly.append(boiler_sum) + coil_hourly.append(coil_sum) + hp_sum = 0 + boiler_sum = 0 + coil_sum = 0 + hp.energy_consumption[cte.HEATING] = {} + hp.energy_consumption[cte.HEATING][cte.HOUR] = hp_hourly + boiler.energy_consumption[cte.HEATING] = {} + boiler.energy_consumption[cte.HEATING][cte.HOUR] = boiler_hourly + if len(self.heating_demand) == 8760: + 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][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])] + if tes.heating_coil_capacity is not None: + tes.heating_coil_energy_consumption[cte.HEATING] = {} + if len(self.heating_demand) == 8760: + tes.heating_coil_energy_consumption[cte.HEATING][cte.HOUR] = coil_hourly + tes.heating_coil_energy_consumption[cte.HEATING][cte.MONTH] = MonthlyValues.get_total_month( + tes.heating_coil_energy_consumption[cte.HEATING][cte.HOUR]) + tes.heating_coil_energy_consumption[cte.HEATING][cte.YEAR] = [ + sum(tes.heating_coil_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'] = source_temperature + 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 Power Consumption (W)'] = boiler_energy_consumption + self.results['Boiler Supply Temperature'] = t_sup_boiler + self.results['Boiler Fuel Consumption'] = boiler_fuel_consumption + self.results['TES Temperature'] = t_tank + self.results['Heating Coil heat input'] = q_coil + 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 + self.results['Total Heating Power Consumption (W)'] = total_consumption + return self.results diff --git a/energy_system_modelling_package/energy_system_modelling_factories/hvac_dhw_systems_simulation_models/heat_pump_characteristics.py b/energy_system_modelling_package/energy_system_modelling_factories/hvac_dhw_systems_simulation_models/heat_pump_characteristics.py new file mode 100644 index 00000000..944c6094 --- /dev/null +++ b/energy_system_modelling_package/energy_system_modelling_factories/hvac_dhw_systems_simulation_models/heat_pump_characteristics.py @@ -0,0 +1,54 @@ +import hub.helpers.constants as cte + + +class HeatPump: + def __init__(self, hp, t_out): + self.hp = hp + self.t_out = t_out + + def hp_source_temperature(self): + if self.hp.source_medium == cte.AIR: + self.hp.source_temperature = self.t_out + elif self.hp.source_medium == cte.GROUND: + average_air_temperature = sum(self.t_out) / len(self.t_out) + self.hp.source_temperature = [average_air_temperature + 10] * len(self.t_out) + elif self.hp.source_medium == cte.WATER: + self.hp.source_temperature = [15] * len(self.t_out) + return self.hp.source_temperature + + def air_to_water_cop(self, source_temperature, inlet_water_temperature, mode=cte.HEATING): + cop_coefficient = 1 + t_inlet_water_fahrenheit = 1.8 * inlet_water_temperature + 32 + t_source_fahrenheit = 1.8 * source_temperature + 32 + if mode == cte.HEATING: + if self.hp.heat_efficiency_curve is not None: + cop_curve_coefficients = [float(coefficient) for coefficient in self.hp.heat_efficiency_curve.coefficients] + cop_coefficient = (1 / (cop_curve_coefficients[0] + + cop_curve_coefficients[1] * t_inlet_water_fahrenheit + + cop_curve_coefficients[2] * t_inlet_water_fahrenheit ** 2 + + cop_curve_coefficients[3] * t_source_fahrenheit + + cop_curve_coefficients[4] * t_source_fahrenheit ** 2 + + cop_curve_coefficients[5] * t_inlet_water_fahrenheit * t_source_fahrenheit)) + hp_efficiency = float(self.hp.heat_efficiency) + elif mode == cte.COOLING: + if self.hp.cooling_efficiency_curve is not None: + cop_curve_coefficients = [float(coefficient) for coefficient in self.hp.cooling_efficiency_curve.coefficients] + cop_coefficient = (1 / (cop_curve_coefficients[0] + + cop_curve_coefficients[1] * t_inlet_water_fahrenheit + + cop_curve_coefficients[2] * t_inlet_water_fahrenheit ** 2 + + cop_curve_coefficients[3] * t_source_fahrenheit + + cop_curve_coefficients[4] * t_source_fahrenheit ** 2 + + cop_curve_coefficients[5] * t_inlet_water_fahrenheit * t_source_fahrenheit)) / 3.41214 + hp_efficiency = float(self.hp.cooling_efficiency) + else: + if self.hp.heat_efficiency_curve is not None: + cop_curve_coefficients = [float(coefficient) for coefficient in self.hp.heat_efficiency_curve.coefficients] + cop_coefficient = (cop_curve_coefficients[0] + + cop_curve_coefficients[1] * source_temperature + + cop_curve_coefficients[2] * source_temperature ** 2 + + cop_curve_coefficients[3] * inlet_water_temperature + + cop_curve_coefficients[4] * inlet_water_temperature ** 2 + + cop_curve_coefficients[5] * inlet_water_temperature * source_temperature) + hp_efficiency = float(self.hp.heat_efficiency) + hp_cop = cop_coefficient * hp_efficiency + return hp_cop diff --git a/energy_system_modelling_package/energy_system_modelling_factories/hvac_dhw_systems_simulation_models/heat_pump_cooling.py b/energy_system_modelling_package/energy_system_modelling_factories/hvac_dhw_systems_simulation_models/heat_pump_cooling.py new file mode 100644 index 00000000..cf838bdf --- /dev/null +++ b/energy_system_modelling_package/energy_system_modelling_factories/hvac_dhw_systems_simulation_models/heat_pump_cooling.py @@ -0,0 +1,89 @@ +import hub.helpers.constants as cte +from hub.helpers.monthly_values import MonthlyValues +from energy_system_modelling_package.energy_system_modelling_factories.hvac_dhw_systems_simulation_models.heat_pump_characteristics import HeatPump + + +class HeatPumpCooling: + def __init__(self, hp, hourly_cooling_demand_joules, cooling_peak_load_watts, cutoff_temperature, outdoor_temperature, + dt=900): + self.hp = hp + self.cooling_demand = [demand / cte.WATTS_HOUR_TO_JULES for demand in hourly_cooling_demand_joules] + self.cooling_peak_load = cooling_peak_load_watts + self.cutoff_temperature = cutoff_temperature + self.dt = dt + self.results = {} + self.heat_pump_characteristics = HeatPump(self.hp, outdoor_temperature) + + def simulation(self): + source_temperature_hourly = self.heat_pump_characteristics.hp_source_temperature() + cooling_efficiency = float(self.hp.cooling_efficiency) + number_of_ts = int(cte.HOUR_TO_SECONDS / self.dt) + demand = [0] + [x for x in self.cooling_demand for _ in range(number_of_ts)] + source_temperature = [0] + [x for x in source_temperature_hourly for _ in range(number_of_ts)] + variable_names = ["t_sup_hp", "t_ret", "m", "q_hp", "hp_electricity", "hp_cop"] + 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_cop) = [variables[name] for name in variable_names] + t_ret[0] = self.cutoff_temperature + + for i in range(1, len(demand)): + if demand[i] > 0.15 * self.cooling_peak_load: + m[i] = self.hp.nominal_cooling_output / (cte.WATER_HEAT_CAPACITY * 5) + if t_ret[i - 1] >= self.cutoff_temperature: + if demand[i] < 0.25 * self.cooling_peak_load: + q_hp[i] = 0.25 * self.hp.nominal_cooling_output + elif demand[i] < 0.5 * self.cooling_peak_load: + q_hp[i] = 0.5 * self.hp.nominal_cooling_output + else: + q_hp[i] = self.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] + if q_hp[i] > 0: + if self.hp.source_medium == cte.AIR and self.hp.supply_medium == cte.WATER: + hp_cop[i] = self.heat_pump_characteristics.air_to_water_cop(source_temperature[i], t_ret[i], + mode=cte.COOLING) + hp_electricity[i] = q_hp[i] / hp_cop[i] + else: + hp_cop[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_supply_temperature_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_supply_temperature_hourly.append(t_sup_hp[i]) + hp_sum = 0 + self.hp.cooling_supply_temperature = hp_supply_temperature_hourly + self.hp.energy_consumption[cte.COOLING] = {} + self.hp.energy_consumption[cte.COOLING][cte.HOUR] = hp_hourly + self.hp.energy_consumption[cte.COOLING][cte.MONTH] = MonthlyValues.get_total_month( + self.hp.energy_consumption[cte.COOLING][cte.HOUR]) + self.hp.energy_consumption[cte.COOLING][cte.YEAR] = [ + sum(self.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 Source Temperature'] = source_temperature + self.results['HP Cooling Supply Temperature'] = t_sup_hp + self.results['HP Cooling COP'] = hp_cop + 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 self.results + + + + diff --git a/energy_system_modelling_package/energy_system_modelling_factories/hvac_dhw_systems_simulation_models/thermal_storage_tank.py b/energy_system_modelling_package/energy_system_modelling_factories/hvac_dhw_systems_simulation_models/thermal_storage_tank.py new file mode 100644 index 00000000..552e1807 --- /dev/null +++ b/energy_system_modelling_package/energy_system_modelling_factories/hvac_dhw_systems_simulation_models/thermal_storage_tank.py @@ -0,0 +1,47 @@ +import math + +import hub.helpers.constants as cte + + +class StorageTank: + """ + Calculation of the temperature inside a hot water storage tank in the next time step + """ + + def __init__(self, volume, height, material_layers, heating_coil_capacity, stratification_layer=1): + self.volume = volume + self.height = height + self.materials = material_layers + self.number_of_vertical_layers = stratification_layer + self.heating_coil_capacity = heating_coil_capacity + + def heat_loss_coefficient(self): + r_tot = sum(float(layer.thickness) / float(layer.material.conductivity) for layer in + self.materials) + u_tot = 1 / r_tot + d = math.sqrt((4 * self.volume) / (math.pi * self.height)) + a_side = math.pi * d * self.height + a_top = math.pi * d ** 2 / 4 + if self.number_of_vertical_layers == 1: + ua = u_tot * (2 * a_top + a_side) + return ua + else: + ua_side = u_tot * a_side + ua_top_bottom = u_tot * (a_top + a_side) + return ua_side, ua_top_bottom + + + def calculate_space_heating_fully_mixed(self, charging_flow_rate, discharging_flow_rate, supply_temperature, + return_temperature, + current_tank_temperature, heat_generator_input, ambient_temperature, dt): + ua = self.heat_loss_coefficient() + t_tank = (current_tank_temperature + + (charging_flow_rate * (supply_temperature - current_tank_temperature) + + (ua * (ambient_temperature - current_tank_temperature)) / cte.WATER_HEAT_CAPACITY - + discharging_flow_rate * (current_tank_temperature - return_temperature) + + heat_generator_input / cte.WATER_HEAT_CAPACITY) * (dt / (cte.WATER_DENSITY * self.volume))) + return t_tank + + def calculate_dhw_fully_mixed(self, charging_flow_rate, discharging_flow_rate, supply_temperature, return_temperature, + current_tank_temperature, heat_generator_input, ambient_temperature, dt): + pass diff --git a/energy_system_modelling_package/energy_system_modelling_factories/montreal_energy_system_archetype_modelling_factory.py b/energy_system_modelling_package/energy_system_modelling_factories/montreal_energy_system_archetype_modelling_factory.py new file mode 100644 index 00000000..9fe1176b --- /dev/null +++ b/energy_system_modelling_package/energy_system_modelling_factories/montreal_energy_system_archetype_modelling_factory.py @@ -0,0 +1,36 @@ +""" +EnergySystemSizingSimulationFactory retrieve the energy system archetype sizing and simulation module +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2024 Concordia CERC group +Project Coder Saeed Ranjbar saeed.ranjbar@mail.concordia.ca +""" + +from energy_system_modelling_package.energy_system_modelling_factories.archetypes.montreal.archetype_cluster_1 import ArchetypeCluster1 + + +class MontrealEnergySystemArchetypesSimulationFactory: + """ + EnergySystemsFactory class + """ + + def __init__(self, handler, building, output_path, csv_output=True): + self._output_path = output_path + self._handler = '_' + handler.lower() + self._building = building + self._csv_output = csv_output + + def _archetype_cluster_1(self): + """ + Enrich the city by using the sizing and simulation model developed for archetype13 of montreal_future_systems + """ + dt = 900 + ArchetypeCluster1(self._building, dt, self._output_path, self._csv_output).enrich_building() + 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 + :return: None + """ + getattr(self, self._handler, lambda: None)() diff --git a/energy_system_modelling_package/energy_system_modelling_factories/pv_assessment/electricity_demand_calculator.py b/energy_system_modelling_package/energy_system_modelling_factories/pv_assessment/electricity_demand_calculator.py new file mode 100644 index 00000000..175f367e --- /dev/null +++ b/energy_system_modelling_package/energy_system_modelling_factories/pv_assessment/electricity_demand_calculator.py @@ -0,0 +1,73 @@ +import hub.helpers.constants as cte +class HourlyElectricityDemand: + def __init__(self, building): + self.building = building + + def calculate(self): + hourly_electricity_consumption = [] + energy_systems = self.building.energy_systems + 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] + hourly += lighting[i] + if heating is not None: + hourly += heating[i] + if cooling is not None: + hourly += cooling[i] + if dhw is not None: + hourly += dhw[i] + hourly_electricity_consumption.append(hourly) + return hourly_electricity_consumption diff --git a/energy_system_modelling_package/energy_system_modelling_factories/pv_assessment/pv_feasibility.py b/energy_system_modelling_package/energy_system_modelling_factories/pv_assessment/pv_feasibility.py new file mode 100644 index 00000000..9278b16c --- /dev/null +++ b/energy_system_modelling_package/energy_system_modelling_factories/pv_assessment/pv_feasibility.py @@ -0,0 +1,37 @@ +from pathlib import Path +import subprocess +from hub.imports.geometry_factory import GeometryFactory +from building_modelling.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): + input_files_path = (Path(__file__).parent.parent.parent.parent / 'input_files') + output_path = (Path(__file__).parent.parent.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 = input_files_path / 'output_buildings.geojson' + 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, sra_output_path).export() + sra_path = (sra_output_path / f'{city.name}_sra.xml').resolve() + subprocess.run(['sra', str(sra_path)]) + ResultFactory('sra', city, sra_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/energy_system_modelling_package/energy_system_modelling_factories/pv_assessment/pv_model.py b/energy_system_modelling_package/energy_system_modelling_factories/pv_assessment/pv_model.py new file mode 100644 index 00000000..2714befa --- /dev/null +++ b/energy_system_modelling_package/energy_system_modelling_factories/pv_assessment/pv_model.py @@ -0,0 +1,42 @@ +import math +import hub.helpers.constants as cte +from hub.helpers.monthly_values import MonthlyValues + + +class PVModel: + def __init__(self, pv, hourly_electricity_demand_joules, solar_radiation, installed_pv_area, model_type, ns=None, + np=None): + self.pv = pv + self.hourly_electricity_demand = [demand / cte.WATTS_HOUR_TO_JULES for demand in hourly_electricity_demand_joules] + self.solar_radiation = solar_radiation + self.installed_pv_area = installed_pv_area + self._model_type = '_' + model_type.lower() + self.ns = ns + self.np = np + self.results = {} + + def fixed_efficiency(self): + module_efficiency = float(self.pv.electricity_efficiency) + variable_names = ["pv_output", "import", "export", "self_sufficiency_ratio"] + variables = {name: [0] * len(self.hourly_electricity_demand) for name in variable_names} + (pv_out, grid_import, grid_export, self_sufficiency_ratio) = [variables[name] for name in variable_names] + for i in range(len(self.hourly_electricity_demand)): + pv_out[i] = module_efficiency * self.installed_pv_area * self.solar_radiation[i] / cte.WATTS_HOUR_TO_JULES + if pv_out[i] < self.hourly_electricity_demand[i]: + grid_import[i] = self.hourly_electricity_demand[i] - pv_out[i] + else: + grid_export[i] = pv_out[i] - self.hourly_electricity_demand[i] + self_sufficiency_ratio[i] = pv_out[i] / self.hourly_electricity_demand[i] + self.results['Electricity Demand (W)'] = self.hourly_electricity_demand + self.results['PV Output (W)'] = pv_out + self.results['Imported from Grid (W)'] = grid_import + self.results['Exported to Grid (W)'] = grid_export + self.results['Self Sufficiency Ratio'] = self_sufficiency_ratio + return self.results + + def enrich(self): + """ + Enrich the city given to the class using the class given handler + :return: None + """ + return getattr(self, self._model_type, lambda: None)() diff --git a/energy_system_modelling_package/energy_system_modelling_factories/pv_assessment/pv_sizing.py b/energy_system_modelling_package/energy_system_modelling_factories/pv_assessment/pv_sizing.py new file mode 100644 index 00000000..b53ba465 --- /dev/null +++ b/energy_system_modelling_package/energy_system_modelling_factories/pv_assessment/pv_sizing.py @@ -0,0 +1,70 @@ +import math +import hub.helpers.constants as cte +from energy_system_modelling_package.energy_system_modelling_factories.pv_assessment.solar_angles import CitySolarAngles +from energy_system_modelling_package.energy_system_modelling_factories.pv_assessment.radiation_tilted import RadiationTilted + + +class PVSizing(CitySolarAngles): + def __init__(self, city, tilt_angle, surface_azimuth=180, maintenance_factor=0.1, mechanical_equipment_factor=0.3, + orientation_factor=0.1, system_type='rooftop'): + super().__init__(location_latitude=city.latitude, + location_longitude=city.longitude, + tilt_angle=tilt_angle, + surface_azimuth_angle=surface_azimuth) + self.city = city + self.maintenance_factor = maintenance_factor + self.mechanical_equipment_factor = mechanical_equipment_factor + self.orientation_factor = orientation_factor + self.angles = self.calculate + self.system_type = system_type + + def rooftop_sizing(self): + results = {} + # Available Roof Area + for building in self.city.buildings: + for energy_system in building.energy_systems: + for generation_system in energy_system.generation_systems: + if generation_system.system_type == cte.PHOTOVOLTAIC: + module_width = float(generation_system.width) + module_height = float(generation_system.height) + roof_area = 0 + for roof in building.roofs: + roof_area += roof.perimeter_area + pv_module_area = module_width * module_height + available_roof = ((self.maintenance_factor + self.orientation_factor + self.mechanical_equipment_factor) * + roof_area) + # Inter-Row Spacing + winter_solstice = self.angles[(self.angles['AST'].dt.month == 12) & + (self.angles['AST'].dt.day == 21) & + (self.angles['AST'].dt.hour == 12)] + solar_altitude = winter_solstice['solar altitude'].values[0] + solar_azimuth = winter_solstice['solar azimuth'].values[0] + distance = ((module_height * abs(math.cos(math.radians(solar_azimuth)))) / + math.tan(math.radians(solar_altitude))) + distance = float(format(distance, '.1f')) + # Calculation of the number of panels + space_dimension = math.sqrt(available_roof) + space_dimension = float(format(space_dimension, '.2f')) + panels_per_row = math.ceil(space_dimension / module_width) + number_of_rows = math.ceil(space_dimension / distance) + total_number_of_panels = panels_per_row * number_of_rows + total_pv_area = panels_per_row * number_of_rows * pv_module_area + building.roofs[0].installed_solar_collector_area = total_pv_area + results[f'Building {building.name}'] = {'total_roof_area': roof_area, + 'PV dedicated area': available_roof, + 'total_pv_area': total_pv_area, + 'total_number_of_panels': total_number_of_panels, + 'number_of_rows': number_of_rows, + 'panels_per_row': panels_per_row} + return results + + def rooftop_tilted_radiation(self): + for building in self.city.buildings: + RadiationTilted(building=building, + solar_angles=self.angles, + tilt_angle=self.tilt_angle, + ghi=building.roofs[0].global_irradiance[cte.HOUR], + ).enrich() + + def facade_sizing(self): + pass diff --git a/energy_system_modelling_package/energy_system_modelling_factories/pv_assessment/pv_sizing_and_simulation.py b/energy_system_modelling_package/energy_system_modelling_factories/pv_assessment/pv_sizing_and_simulation.py new file mode 100644 index 00000000..fc26fe7e --- /dev/null +++ b/energy_system_modelling_package/energy_system_modelling_factories/pv_assessment/pv_sizing_and_simulation.py @@ -0,0 +1,59 @@ +import math + +from energy_system_modelling_package.energy_system_modelling_factories.pv_assessment.radiation_tilted import RadiationTilted +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_constant_efficiency(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) + 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 * + 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])] \ No newline at end of file diff --git a/energy_system_modelling_package/energy_system_modelling_factories/pv_assessment/radiation_tilted.py b/energy_system_modelling_package/energy_system_modelling_factories/pv_assessment/radiation_tilted.py new file mode 100644 index 00000000..31bd5636 --- /dev/null +++ b/energy_system_modelling_package/energy_system_modelling_factories/pv_assessment/radiation_tilted.py @@ -0,0 +1,110 @@ +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] + 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 + 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 + self.building.roofs[0].global_irradiance_tilted[cte.HOUR] = 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/energy_system_modelling_package/energy_system_modelling_factories/pv_assessment/solar_angles.py b/energy_system_modelling_package/energy_system_modelling_factories/pv_assessment/solar_angles.py new file mode 100644 index 00000000..560bd27c --- /dev/null +++ b/energy_system_modelling_package/energy_system_modelling_factories/pv_assessment/solar_angles.py @@ -0,0 +1,145 @@ +import math +import pandas as pd +from datetime import datetime +from pathlib import Path + + +class CitySolarAngles: + def __init__(self, location_latitude, location_longitude, tilt_angle, surface_azimuth_angle, + standard_meridian=-75): + 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/energy_system_modelling_package/energy_system_modelling_factories/system_sizing_methods/genetic_algorithm/individual.py b/energy_system_modelling_package/energy_system_modelling_factories/system_sizing_methods/genetic_algorithm/individual.py new file mode 100644 index 00000000..5a25e72e --- /dev/null +++ b/energy_system_modelling_package/energy_system_modelling_factories/system_sizing_methods/genetic_algorithm/individual.py @@ -0,0 +1,470 @@ +import math +import random +from hub.helpers.dictionaries import Dictionaries +from hub.catalog_factories.costs_catalog_factory import CostsCatalogFactory +import hub.helpers.constants as cte +from energy_system_modelling_package.energy_system_modelling_factories.hvac_dhw_systems_simulation_models.domestic_hot_water_heat_pump_with_tes import \ + DomesticHotWaterHeatPumpTes +from energy_system_modelling_package.energy_system_modelling_factories.hvac_dhw_systems_simulation_models.heat_pump_boiler_tes_heating import \ + HeatPumpBoilerTesHeating +import numpy_financial as npf + + +class Individual: + def __init__(self, building, energy_system, design_period_energy_demands, optimization_scenario, + heating_design_load=None, cooling_design_load=None, dt=900, fuel_price_index=0.05, + electricity_tariff_type='fixed', consumer_price_index=0.04, interest_rate=0.04, + discount_rate=0.03, percentage_credit=0, credit_years=15): + """ + :param building: building object + :param energy_system: energy system to be optimized + :param design_period_energy_demands: A dictionary of design period heating, cooling and dhw demands. Design period + is the day with the highest total demand and the two days before and after it + :param optimization_scenario: a string indicating the objective function from minimization of cost, + energy consumption, and both together + :param heating_design_load: heating design load in W + :param cooling_design_load: cooling design load in W + :param dt the time step size used for simulations + :param fuel_price_index the price increase index of all fuels. A single value is used for all fuels. + :param electricity_tariff_type the electricity tariff type between 'fixed' and 'variable' for economic optimization + :param consumer_price_index + """ + self.building = building + self.energy_system = energy_system + self.design_period_energy_demands = design_period_energy_demands + self.demand_types = energy_system.demand_types + self.optimization_scenario = optimization_scenario + self.heating_design_load = heating_design_load + self.cooling_design_load = cooling_design_load + self.available_space = building.volume / building.storeys_above_ground + self.dt = dt + self.fuel_price_index = fuel_price_index + self.electricity_tariff_type = electricity_tariff_type + self.consumer_price_index = consumer_price_index + self.interest_rate = interest_rate + self.discount_rate = discount_rate + self.credit_years = credit_years + self.percentage_credit = percentage_credit + self.costs = self.costs_archetype() + self.feasibility = True + self.fitness_score = 0 + self.individual = {} + + def system_components(self): + """ + Extracts system components (generation and storage) for a given energy system. + :return: Dictionary of system components. + """ + self.individual['Generation Components'] = [] + self.individual['Energy Storage Components'] = [] + self.individual['End of Life Cost'] = self.costs.end_of_life_cost + for generation_component in self.energy_system.generation_systems: + investment_cost, reposition_cost, lifetime = self.unit_investment_cost('Generation', + generation_component.system_type) + maintenance_cost = self.unit_maintenance_cost(generation_component) + if generation_component.system_type == cte.PHOTOVOLTAIC: + heating_capacity = None + cooling_capacity = None + heat_efficiency = None + cooling_efficiency = None + unit_fuel_cost = 0 + else: + heating_capacity = 0 + cooling_capacity = 0 + heat_efficiency = generation_component.heat_efficiency + cooling_efficiency = generation_component.cooling_efficiency + unit_fuel_cost = self.fuel_cost_per_kwh(generation_component.fuel_type, 'fixed') + self.individual['Generation Components'].append({ + 'type': generation_component.system_type, + 'heating_capacity': heating_capacity, + 'cooling_capacity': cooling_capacity, + 'electricity_capacity': 0, + 'nominal_heating_efficiency': heat_efficiency, + 'nominal_cooling_efficiency': cooling_efficiency, + 'nominal_electricity_efficiency': generation_component.electricity_efficiency, + 'fuel_type': generation_component.fuel_type, + 'unit_investment_cost': investment_cost, + 'unit_reposition_cost': reposition_cost, + 'unit_fuel_cost(CAD/kWh)': unit_fuel_cost, + 'unit_maintenance_cost': maintenance_cost, + 'lifetime': lifetime + }) + if generation_component.energy_storage_systems is not None: + for energy_storage_system in generation_component.energy_storage_systems: + investment_cost, reposition_cost, lifetime = ( + self.unit_investment_cost('Storage', + f'{energy_storage_system.type_energy_stored}_storage')) + if energy_storage_system.type_energy_stored == cte.THERMAL: + heating_coil_capacity = energy_storage_system.heating_coil_capacity + heating_coil_fuel_cost = self.fuel_cost_per_kwh(f'{cte.ELECTRICITY}', 'fixed') + volume = 0 + capacity = None + else: + heating_coil_capacity = None + heating_coil_fuel_cost = None + volume = None + capacity = 0 + self.individual['Energy Storage Components'].append( + {'type': f'{energy_storage_system.type_energy_stored}_storage', + 'capacity': capacity, + 'volume': volume, + 'heating_coil_capacity': heating_coil_capacity, + 'unit_investment_cost': investment_cost, + 'unit_reposition_cost': reposition_cost, + 'heating_coil_fuel_cost': heating_coil_fuel_cost, + 'unit_maintenance_cost': 0, + 'lifetime': lifetime + }) + + def initialization(self): + """ + Assigns initial sizes to generation and storage components based on heating and cooling design loads and + available space in the building. + :return: + """ + self.system_components() + generation_components = self.individual['Generation Components'] + storage_components = self.individual['Energy Storage Components'] + for generation_component in generation_components: + if generation_component[ + 'nominal_heating_efficiency'] is not None and cte.HEATING or cte.DOMESTIC_HOT_WATER in self.demand_types: + if self.heating_design_load is not None: + generation_component['heating_capacity'] = random.uniform(0, self.heating_design_load) + else: + if cte.HEATING in self.demand_types: + generation_component['heating_capacity'] = random.uniform(0, max( + self.design_period_energy_demands[cte.HEATING]['demands']) / cte.WATTS_HOUR_TO_JULES) + else: + generation_component['heating_capacity'] = random.uniform(0, max( + self.design_period_energy_demands[cte.DOMESTIC_HOT_WATER]['demands']) / cte.WATTS_HOUR_TO_JULES) + else: + generation_component['heating_capacity'] = None + if generation_component['nominal_cooling_efficiency'] is not None and cte.COOLING in self.demand_types: + if self.cooling_design_load is not None: + generation_component['cooling_capacity'] = random.uniform(0, self.cooling_design_load) + else: + generation_component['cooling_capacity'] = random.uniform(0, max( + self.design_period_energy_demands[cte.COOLING]['demands']) / cte.WATTS_HOUR_TO_JULES) + else: + generation_component['cooling_capacity'] = None + if generation_component['nominal_electricity_efficiency'] is None: + generation_component['electricity_capacity'] = None + for storage_component in storage_components: + if storage_component['type'] == f'{cte.THERMAL}_storage': + storage_component['volume'] = random.uniform(0, 0.01 * self.available_space) + if storage_component['heating_coil_capacity'] is not None: + if self.heating_design_load is not None: + storage_component['heating_coil_capacity'] = random.uniform(0, self.heating_design_load) + else: + if cte.HEATING in self.demand_types: + storage_component['heating_coil_capacity'] = random.uniform(0, max( + self.design_period_energy_demands[cte.HEATING]['demands']) / cte.WATTS_HOUR_TO_JULES) + else: + storage_component['heating_coil_capacity'] = random.uniform(0, max( + self.design_period_energy_demands[cte.DOMESTIC_HOT_WATER]['demands']) / cte.WATTS_HOUR_TO_JULES) + + def score_evaluation(self): + self.system_simulation() + self.individual['feasible'] = self.feasibility + lcc = 0 + total_energy_consumption = 0 + if self.feasibility: + if 'cost' in self.optimization_scenario: + investment_cost = 0 + operation_cost_year_0 = 0 + maintenance_cost_year_0 = 0 + for generation_system in self.individual['Generation Components']: + heating_capacity = 0 if generation_system['heating_capacity'] is None else generation_system[ + 'heating_capacity'] + cooling_capacity = 0 if generation_system['cooling_capacity'] is None else generation_system[ + 'cooling_capacity'] + capacity = max(heating_capacity, cooling_capacity) + investment_cost += capacity * generation_system['unit_investment_cost'] / 1000 + maintenance_cost_year_0 += capacity * generation_system['unit_maintenance_cost'] / 1000 + operation_cost_year_0 += (generation_system['total_energy_consumption(kWh)'] * + generation_system['unit_fuel_cost(CAD/kWh)']) + for storage_system in self.individual['Energy Storage Components']: + if cte.THERMAL in storage_system['type']: + investment_cost += storage_system['volume'] * storage_system['unit_investment_cost'] + if storage_system['heating_coil_capacity'] is not None: + operation_cost_year_0 += (storage_system['total_energy_consumption(kWh)'] * + storage_system['heating_coil_fuel_cost']) + lcc = self.life_cycle_cost_calculation(investment_cost=investment_cost, + operation_cost_year_0=operation_cost_year_0, + maintenance_cost_year_0=maintenance_cost_year_0) + self.individual['lcc'] = lcc + if 'energy_consumption' in self.optimization_scenario: + total_energy_consumption = 0 + for generation_system in self.individual['Generation Components']: + total_energy_consumption += generation_system['total_energy_consumption(kWh)'] + for storage_system in self.individual['Energy Storage Components']: + if cte.THERMAL in storage_system['type'] and storage_system['heating_coil_capacity'] is not None: + total_energy_consumption += storage_system['total_energy_consumption(kWh)'] + self.individual['total_energy_consumption'] = total_energy_consumption + # Fitness score based on the optimization scenario + if self.optimization_scenario == 'cost': + self.fitness_score = lcc + self.individual['fitness_score'] = lcc + elif self.optimization_scenario == 'energy_consumption': + self.fitness_score = total_energy_consumption + self.individual['fitness_score'] = total_energy_consumption + elif self.optimization_scenario == 'cost_energy_consumption' or self.optimization_scenario == 'energy_consumption_cost': + self.fitness_score = (lcc, total_energy_consumption) + self.individual['fitness_score'] = (lcc, total_energy_consumption) + else: + lcc = float('inf') + total_energy_consumption = float('inf') + self.individual['lcc'] = lcc + self.individual['total_energy_consumption'] = total_energy_consumption + if self.optimization_scenario == 'cost_energy_consumption' or self.optimization_scenario == 'energy_consumption_cost': + self.individual['fitness_score'] = (float('inf'), float('inf')) + self.fitness_score = (float('inf'), float('inf')) + else: + self.individual['fitness_score'] = float('inf') + self.fitness_score = float('inf') + + def system_simulation(self): + """ + The method to run the energy system model using the existing models in the energy_system_modelling_package. + Based on cluster id and demands, model is imported and run. + :return: dictionary of results + """ + if self.building.energy_systems_archetype_cluster_id == '1': + if cte.HEATING in self.demand_types: + boiler = self.energy_system.generation_systems[0] + boiler.nominal_heat_output = self.individual['Generation Components'][0]['heating_capacity'] + hp = self.energy_system.generation_systems[1] + hp.nominal_heat_output = self.individual['Generation Components'][1]['heating_capacity'] + tes = self.energy_system.generation_systems[0].energy_storage_systems[0] + tes.volume = self.individual['Energy Storage Components'][0]['volume'] + tes.height = self.building.average_storey_height - 1 + tes.heating_coil_capacity = self.individual['Energy Storage Components'][0]['heating_coil_capacity'] \ + if self.individual['Energy Storage Components'][0]['heating_coil_capacity'] is not None else None + heating_demand_joules = self.design_period_energy_demands[cte.HEATING]['demands'] + heating_peak_load_watts = max(self.design_period_energy_demands[cte.HEATING]) if \ + (self.heating_design_load is not None) else self.building.heating_peak_load[cte.YEAR][0] + upper_limit_tes_heating = 55 + design_period_start_index = self.design_period_energy_demands[cte.HEATING]['start_index'] + design_period_end_index = self.design_period_energy_demands[cte.HEATING]['end_index'] + outdoor_temperature = self.building.external_temperature[cte.HOUR][ + design_period_start_index:design_period_end_index] + results = HeatPumpBoilerTesHeating(hp=hp, + boiler=boiler, + tes=tes, + hourly_heating_demand_joules=heating_demand_joules, + heating_peak_load_watts=heating_peak_load_watts, + upper_limit_tes=upper_limit_tes_heating, + outdoor_temperature=outdoor_temperature, + dt=self.dt).simulation() + if min(results['TES Temperature']) < 35: + self.feasibility = False + elif cte.DOMESTIC_HOT_WATER in self.demand_types: + hp = self.energy_system.generation_systems[0] + hp.nominal_heat_output = self.individual['Generation Components'][0]['heating_capacity'] + tes = self.energy_system.generation_systems[0].energy_storage_systems[0] + tes.volume = self.individual['Energy Storage Components'][0]['volume'] + tes.height = self.building.average_storey_height - 1 + tes.heating_coil_capacity = self.individual['Energy Storage Components'][0]['heating_coil_capacity'] \ + if self.individual['Energy Storage Components'][0]['heating_coil_capacity'] is not None else None + dhw_demand_joules = self.design_period_energy_demands[cte.DOMESTIC_HOT_WATER]['demands'] + upper_limit_tes = 65 + design_period_start_index = self.design_period_energy_demands[cte.DOMESTIC_HOT_WATER]['start_index'] + design_period_end_index = self.design_period_energy_demands[cte.DOMESTIC_HOT_WATER]['end_index'] + outdoor_temperature = self.building.external_temperature[cte.HOUR][ + design_period_start_index:design_period_end_index] + results = DomesticHotWaterHeatPumpTes(hp=hp, + tes=tes, + hourly_dhw_demand_joules=dhw_demand_joules, + upper_limit_tes=upper_limit_tes, + outdoor_temperature=outdoor_temperature, + dt=self.dt).simulation() + if min(results['DHW TES Temperature']) < 55: + self.feasibility = False + if self.feasibility: + generation_system_types = [generation_system.system_type for generation_system in + self.energy_system.generation_systems] + for generation_component in self.individual['Generation Components']: + if generation_component['type'] in generation_system_types: + index = generation_system_types.index(generation_component['type']) + for demand_type in self.demand_types: + if demand_type in self.energy_system.generation_systems[index].energy_consumption: + generation_component['total_energy_consumption(kWh)'] = (sum( + self.energy_system.generation_systems[index].energy_consumption[demand_type][cte.HOUR]) / 3.6e6) + for storage_component in self.individual['Energy Storage Components']: + if storage_component['type'] == f'{cte.THERMAL}_storage' and storage_component[ + 'heating_coil_capacity'] is not None: + for generation_system in self.energy_system.generation_systems: + if generation_system.energy_storage_systems is not None: + for storage_system in generation_system.energy_storage_systems: + if storage_system.type_energy_stored == cte.THERMAL: + for demand_type in self.demand_types: + if demand_type in storage_system.heating_coil_energy_consumption: + storage_component['total_energy_consumption(kWh)'] = (sum( + storage_system.heating_coil_energy_consumption[demand_type][cte.HOUR]) / 3.6e6) + + def life_cycle_cost_calculation(self, investment_cost, operation_cost_year_0, maintenance_cost_year_0, + life_cycle_duration=41): + """ + Calculating the life cycle cost of the energy system. The original cost workflow in hub is not used to reduce + computation time.Here are the steps: + 1- Costs catalog and different components are imported. + 2- Capital costs (investment and reposition) are calculated and appended to a list to have the capital cash + flow. + 3- + :param maintenance_cost_year_0: + :param operation_cost_year_0: + :param investment_cost: + :param life_cycle_duration: + :return: + """ + capital_costs_cash_flow = [investment_cost] + operational_costs_cash_flow = [0] + maintenance_costs_cash_flow = [0] + end_of_life_costs = [0] * (life_cycle_duration + 1) + for i in range(1, life_cycle_duration + 1): + yearly_operational_cost = math.pow(1 + self.fuel_price_index, i) * operation_cost_year_0 + yearly_maintenance_cost = math.pow(1 + self.consumer_price_index, i) * maintenance_cost_year_0 + yearly_capital_cost = 0 + for generation_system in self.individual['Generation Components']: + if (i % generation_system['lifetime']) == 0 and i != (life_cycle_duration - 1): + cost_increase = math.pow(1 + self.consumer_price_index, i) + heating_capacity = 0 if generation_system['heating_capacity'] is None else generation_system[ + 'heating_capacity'] + cooling_capacity = 0 if generation_system['cooling_capacity'] is None else generation_system[ + 'cooling_capacity'] + capacity = max(heating_capacity, cooling_capacity) + yearly_capital_cost += generation_system['unit_reposition_cost'] * capacity * cost_increase / 1000 + yearly_capital_cost += -npf.pmt(self.interest_rate, self.credit_years, + investment_cost * self.percentage_credit) + for storage_system in self.individual['Energy Storage Components']: + if (i % storage_system['lifetime']) == 0 and i != (life_cycle_duration - 1): + cost_increase = math.pow(1 + self.consumer_price_index, i) + capacity = storage_system['volume'] if cte.THERMAL in storage_system['type'] else storage_system['capacity'] + yearly_capital_cost += storage_system['unit_reposition_cost'] * capacity * cost_increase + yearly_capital_cost += -npf.pmt(self.interest_rate, self.credit_years, + investment_cost * self.percentage_credit) + capital_costs_cash_flow.append(yearly_capital_cost) + operational_costs_cash_flow.append(yearly_operational_cost) + maintenance_costs_cash_flow.append(yearly_maintenance_cost) + for year in range(1, life_cycle_duration + 1): + price_increase = math.pow(1 + self.consumer_price_index, year) + if year == life_cycle_duration: + end_of_life_costs[year] = ( + self.building.thermal_zones_from_internal_zones[0].total_floor_area * + self.individual['End of Life Cost'] * price_increase + ) + + life_cycle_capital_cost = npf.npv(self.discount_rate, capital_costs_cash_flow) + life_cycle_operational_cost = npf.npv(self.discount_rate, operational_costs_cash_flow) + life_cycle_maintenance_cost = npf.npv(self.discount_rate, maintenance_costs_cash_flow) + life_cycle_end_of_life_cost = npf.npv(self.discount_rate, end_of_life_costs) + total_life_cycle_cost = life_cycle_capital_cost + life_cycle_operational_cost + life_cycle_maintenance_cost + life_cycle_end_of_life_cost + return total_life_cycle_cost + + def costs_archetype(self): + costs_catalogue = CostsCatalogFactory('montreal_new').catalog.entries().archetypes + dictionary = Dictionaries().hub_function_to_montreal_custom_costs_function + costs_archetype = None + for archetype in costs_catalogue: + if dictionary[str(self.building.function)] == str(archetype.function): + costs_archetype = archetype + return costs_archetype + + def unit_investment_cost(self, component_category, component_type): + """ + Reading the investment and reposition costs of any component from costs catalogue + :param component_category: Due to the categorizations in the cost catalogue, we need this parameter to realize if + the component is a generation or storage component + :param component_type: Type of the component + :return: + """ + investment_cost = 0 + reposition_cost = 0 + lifetime = 0 + name = '' + capital_costs_chapter = self.costs.capital_cost.chapter('D_services') + if component_category == 'Generation': + generation_systems = self.energy_system.generation_systems + for generation_system in generation_systems: + if component_type == generation_system.system_type: + if generation_system.system_type == cte.HEAT_PUMP: + name += ( + generation_system.source_medium.lower() + '_to_' + generation_system.supply_medium.lower() + '_' + + generation_system.system_type.lower().replace(' ', '_')) + elif generation_system.system_type == cte.BOILER: + if generation_system.fuel_type == cte.ELECTRICITY: + name += cte.ELECTRICAL.lower() + f'_{generation_system.system_type}'.lower() + else: + name += generation_system.fuel_type.lower() + f'_{generation_system.system_type}'.lower() + elif generation_system.system_type == cte.PHOTOVOLTAIC: + name += 'D2010_photovoltaic_system' + else: + if cte.HEATING or cte.DOMESTIC_HOT_WATER in self.demand_types: + name += 'template_heat' + else: + name += 'template_cooling' + for item in capital_costs_chapter.items: + if name in item.type: + investment_cost += float(capital_costs_chapter.item(item.type).initial_investment[0]) + reposition_cost += float(capital_costs_chapter.item(item.type).reposition[0]) + lifetime += float(capital_costs_chapter.item(item.type).lifetime) + elif component_category == 'Storage': + for generation_system in self.energy_system.generation_systems: + if generation_system.energy_storage_systems is not None: + for energy_storage_system in generation_system.energy_storage_systems: + if energy_storage_system.type_energy_stored == cte.THERMAL: + if energy_storage_system.heating_coil_capacity is not None: + investment_cost += float(capital_costs_chapter.item('D306010_storage_tank').initial_investment[0]) + reposition_cost += float(capital_costs_chapter.item('D306010_storage_tank').reposition[0]) + lifetime += float(capital_costs_chapter.item('D306010_storage_tank').lifetime) + else: + investment_cost += float( + capital_costs_chapter.item('D306020_storage_tank_with_coil').initial_investment[0]) + reposition_cost += float(capital_costs_chapter.item('D306020_storage_tank_with_coil').reposition[0]) + lifetime += float(capital_costs_chapter.item('D306020_storage_tank_with_coil').lifetime) + + return investment_cost, reposition_cost, lifetime + + def unit_maintenance_cost(self, generation_system): + hvac_maintenance = self.costs.operational_cost.maintenance_hvac + pv_maintenance = self.costs.operational_cost.maintenance_pv + maintenance_cost = 0 + component = None + if generation_system.system_type == cte.HEAT_PUMP and generation_system.source_medium == cte.AIR: + component = 'air_source_heat_pump' + elif generation_system.system_type == cte.HEAT_PUMP and generation_system.source_medium == cte.GROUND: + component = 'ground_source_heat_pump' + elif generation_system.system_type == cte.HEAT_PUMP and generation_system.source_medium == cte.WATER: + component = 'water_source_heat_pump' + elif generation_system.system_type == cte.BOILER and generation_system.fuel_type == cte.GAS: + component = 'gas_boiler' + elif generation_system.system_type == cte.BOILER and generation_system.fuel_type == cte.ELECTRICITY: + component = 'electric_boiler' + elif generation_system.system_type == cte.PHOTOVOLTAIC: + maintenance_cost += pv_maintenance + else: + if cte.HEATING or cte.DOMESTIC_HOT_WATER in self.demand_types: + component = 'general_heating_equipment' + else: + component = 'general_cooling_equipment' + for item in hvac_maintenance: + if item.type == component: + maintenance_cost += item.maintenance[0] + return maintenance_cost + + def fuel_cost_per_kwh(self, fuel_type, fuel_cost_tariff_type): + fuel_cost = 0 + catalog_fuels = self.costs.operational_cost.fuels + for fuel in catalog_fuels: + if fuel_type == fuel.type and fuel_cost_tariff_type == fuel.variable.rate_type: + if fuel.type == cte.ELECTRICITY and fuel_cost_tariff_type == 'fixed': + fuel_cost = fuel.variable.values[0] + elif fuel.type == cte.ELECTRICITY and fuel_cost_tariff_type == 'variable': + fuel_cost = fuel.variable.values[0] + else: + if fuel.type == cte.BIOMASS: + conversion_factor = 1 + else: + conversion_factor = fuel.density[0] + fuel_cost = fuel.variable.values[0] / (conversion_factor * fuel.lower_heating_value[0] * 0.277) + return fuel_cost diff --git a/energy_system_modelling_package/energy_system_modelling_factories/system_sizing_methods/genetic_algorithm/multi_objective_genetic_algorithm.py b/energy_system_modelling_package/energy_system_modelling_factories/system_sizing_methods/genetic_algorithm/multi_objective_genetic_algorithm.py new file mode 100644 index 00000000..a063cb48 --- /dev/null +++ b/energy_system_modelling_package/energy_system_modelling_factories/system_sizing_methods/genetic_algorithm/multi_objective_genetic_algorithm.py @@ -0,0 +1,418 @@ +import copy +import math +import random +import hub.helpers.constants as cte +from energy_system_modelling_package.energy_system_modelling_factories.system_sizing_methods.genetic_algorithm.individual import \ + Individual +import matplotlib.pyplot as plt + + +class MultiObjectiveGeneticAlgorithm: + def __init__(self, population_size=100, generations=50, crossover_rate=0.8, mutation_rate=0.1, + optimization_scenario=None, output_path=None): + self.population_size = population_size + self.population = [] + self.generations = generations + self.crossover_rate = crossover_rate + self.mutation_rate = mutation_rate + self.optimization_scenario = optimization_scenario + self.list_of_solutions = [] + self.best_solution = None + self.best_solution_generation = None + self.output_path = output_path + +# Initialize Population + def initialize_population(self, building, energy_system): + design_period_energy_demands = self.design_period_identification(building) + attempts = 0 + max_attempts = self.population_size * 5 + while len(self.population) < self.population_size and attempts < max_attempts: + individual = Individual(building=building, + energy_system=energy_system, + design_period_energy_demands=design_period_energy_demands, + optimization_scenario=self.optimization_scenario) + individual.initialization() + attempts += 1 + if self.initial_population_feasibility_check(individual, energy_system.demand_types, + design_period_energy_demands): + self.population.append(individual) + if len(self.population) < self.population_size: + raise RuntimeError(f"Could not generate a feasible population of size {self.population_size}. " + f"Only {len(self.population)} feasible individuals were generated.") + + @staticmethod + def initial_population_feasibility_check(individual, demand_types, design_period_demands): + total_heating_capacity = sum( + component['heating_capacity'] for component in individual.individual['Generation Components'] + if component['heating_capacity'] is not None + ) + total_cooling_capacity = sum( + component['cooling_capacity'] for component in individual.individual['Generation Components'] + if component['cooling_capacity'] is not None + ) + max_heating_demand = max(design_period_demands[cte.HEATING]['demands']) / cte.WATTS_HOUR_TO_JULES + max_cooling_demand = max(design_period_demands[cte.COOLING]['demands']) / cte.WATTS_HOUR_TO_JULES + max_dhw_demand = max(design_period_demands[cte.DOMESTIC_HOT_WATER]['demands']) / cte.WATTS_HOUR_TO_JULES + if cte.HEATING in demand_types and total_heating_capacity < 0.5 * max_heating_demand: + return False + if cte.DOMESTIC_HOT_WATER in demand_types and total_heating_capacity < 0.5 * max_dhw_demand: + return False + if cte.COOLING in demand_types and total_cooling_capacity < 0.5 * max_cooling_demand: + return False + total_volume = sum( + component['volume'] for component in individual.individual['Energy Storage Components'] + if component['volume'] is not None + ) + max_storage_volume = individual.available_space * 0.1 + if total_volume > max_storage_volume: + return False + return True + + def nsga2_selection(self, population, fronts, crowding_distances): + new_population = [] + i = 0 + while len(new_population) + len(fronts[i]) <= self.population_size: + for index in fronts[i]: + # Skip individuals with infinite fitness values to avoid propagation + if not math.isinf(self.population[index].individual['fitness_score'][0]) and \ + not math.isinf(self.population[index].individual['fitness_score'][1]): + new_population.append(population[index]) + i += 1 + if i >= len(fronts): + break + if len(new_population) < self.population_size and i < len(fronts): + sorted_front = sorted(fronts[i], key=lambda x: crowding_distances[i][x], reverse=True) + for index in sorted_front: + if len(new_population) < self.population_size: + if not math.isinf(self.population[index].individual['fitness_score'][0]) and \ + not math.isinf(self.population[index].individual['fitness_score'][1]): + new_population.append(population[index]) + else: + break + return new_population + + def fast_non_dominated_sort(self): + population_size = self.population_size + s = [[] for _ in range(population_size)] + front = [[]] + n = [0] * population_size + rank = [0] * population_size + for p in range(population_size): + s[p] = [] + n[p] = 0 + for q in range(population_size): + if self.dominates(self.population[p], self.population[q]): + s[p].append(q) + elif self.dominates(self.population[q], self.population[p]): + n[p] += 1 + if n[p] == 0: + rank[p] = 0 + front[0].append(p) + i = 0 + while front[i]: + next_front = set() + for p in front[i]: + for q in s[p]: + n[q] -= 1 + if n[q] == 0: + rank[q] = i + 1 + next_front.add(q) + i += 1 + front.append(list(next_front)) + del front[-1] + return front + + @staticmethod + def dominates(individual1, individual2): + lcc1, lce1 = individual1.individual['fitness_score'] + lcc2, lce2 = individual2.individual['fitness_score'] + return (lcc1 <= lcc2 and lce1 <= lce2) and (lcc1 < lcc2 or lce1 < lce2) + + def calculate_crowding_distance(self, front): + crowding_distance = [0] * len(self.population) + for objective in ['lcc', 'total_energy_consumption']: + sorted_front = sorted(front, key=lambda x: self.population[x].individual[objective]) + # Set distances to finite large numbers rather than `inf` + crowding_distance[sorted_front[0]] = float(1e9) + crowding_distance[sorted_front[-1]] = float(1e9) + objective_min = self.population[sorted_front[0]].individual[objective] + objective_max = self.population[sorted_front[-1]].individual[objective] + if objective_max != objective_min: + for i in range(1, len(sorted_front) - 1): + crowding_distance[sorted_front[i]] += ( + (self.population[sorted_front[i + 1]].individual[objective] - + self.population[sorted_front[i - 1]].individual[objective]) / + (objective_max - objective_min)) + return crowding_distance + + def crossover(self, parent1, parent2): + """ + Crossover between two parents to produce two children. + swaps generation components and storage components between the two parents with a 50% chance. + :param parent1: First parent individual. + :param parent2: second parent individual. + :return: Two child individuals (child1 and child2). + """ + if random.random() < self.crossover_rate: + # Deep copy of the parents to create children + child1, child2 = copy.deepcopy(parent1), copy.deepcopy(parent2) + # Crossover for Generation Components + for i in range(len(parent1.individual['Generation Components'])): + if random.random() < 0.5: + # swap the entire generation component + child1.individual['Generation Components'][i], child2.individual['Generation Components'][i] = ( + child2.individual['Generation Components'][i], + child1.individual['Generation Components'][i] + ) + + # Crossover for Energy storage Components + for i in range(len(parent1.individual['Energy Storage Components'])): + if random.random() < 0.5: + # swap the entire storage component + child1.individual['Energy Storage Components'][i], child2.individual['Energy Storage Components'][i] = ( + child2.individual['Energy Storage Components'][i], + child1.individual['Energy Storage Components'][i] + ) + + return child1, child2 + else: + # If crossover does not happen, return copies of the original parents + return copy.deepcopy(parent1), copy.deepcopy(parent2) + + def mutate(self, individual, building, energy_system): + """ + Mutates the individual's generation and storage components. + + - `individual`: The individual to mutate (contains generation and storage components). + - `building`: Building data that contains constraints such as peak heating load and available space. + + Returns the mutated individual. + """ + design_period_energy_demands = self.design_period_identification(building) + # Mutate Generation Components + for generation_component in individual['Generation Components']: + if random.random() < self.mutation_rate: + if (generation_component['nominal_heating_efficiency'] is not None and cte.HEATING or cte.DOMESTIC_HOT_WATER in + energy_system.demand_types): + # Mutate heating capacity + if cte.HEATING in energy_system.demand_types: + generation_component['heating_capacity'] = random.uniform( + 0, max(design_period_energy_demands[cte.HEATING]['demands']) / cte.WATTS_HOUR_TO_JULES) + else: + generation_component['heating_capacity'] = random.uniform( + 0, max(design_period_energy_demands[cte.DOMESTIC_HOT_WATER]['demands']) / cte.WATTS_HOUR_TO_JULES) + if generation_component['nominal_cooling_efficiency'] is not None and cte.COOLING in energy_system.demand_types: + # Mutate cooling capacity + generation_component['cooling_capacity'] = random.uniform( + 0, max(design_period_energy_demands[cte.COOLING]['demands']) / cte.WATTS_HOUR_TO_JULES) + # Mutate storage Components + for storage_component in individual['Energy Storage Components']: + if random.random() < self.mutation_rate: + if storage_component['type'] == f'{cte.THERMAL}_storage': + # Mutate the volume of thermal storage + max_available_space = 0.01 * building.volume / building.storeys_above_ground + storage_component['volume'] = random.uniform(0, max_available_space) + if storage_component['heating_coil_capacity'] is not None: + if cte.HEATING in energy_system.demand_types: + storage_component['heating_coil_capacity'] = random.uniform(0, max( + design_period_energy_demands[cte.HEATING]['demands']) / cte.WATTS_HOUR_TO_JULES) + else: + storage_component['heating_coil_capacity'] = random.uniform(0, max( + design_period_energy_demands[cte.DOMESTIC_HOT_WATER]['demands']) / cte.WATTS_HOUR_TO_JULES) + return individual + + def solve_ga(self, building, energy_system): + self.initialize_population(building, energy_system) + for individual in self.population: + individual.score_evaluation() + pareto_population = [] + + for generation in range(1, self.generations + 1): + print(f"Generation {generation}") + fronts = self.fast_non_dominated_sort() + + # Ensure the front calculation is valid + if not fronts or not fronts[0]: + print("Warning: No valid non-dominated front found.") + continue + + # Calculate crowding distances and select individuals + crowding_distances = [self.calculate_crowding_distance(front) for front in fronts] + self.population = self.nsga2_selection(self.population, fronts, crowding_distances) + + # Add only valid indices to pareto_population + pareto_population.extend([self.population[i] for i in fronts[0] if i < len(self.population)]) + + # Check population bounds + if len(pareto_population) > self.population_size: + pareto_population = pareto_population[:self.population_size] + + # Generate the next population through crossover and mutation + next_population = [] + while len(next_population) < self.population_size: + parent1, parent2 = random.choice(self.population), random.choice(self.population) + child1, child2 = self.crossover(parent1, parent2) + self.mutate(child1.individual, building, energy_system) + self.mutate(child2.individual, building, energy_system) + child1.score_evaluation() + child2.score_evaluation() + next_population.extend([child1, child2]) + + self.population = next_population[:self.population_size] + + # Ensure pareto_population contains the correct non-dominated individuals before TOPSIS + if not pareto_population: + print("No Pareto solutions found during optimization.") + return None + + # Recalculate pareto front indices based on updated pareto_population + fronts = self.fast_non_dominated_sort() + pareto_front_indices = fronts[0] if fronts else [] + pareto_front_indices = [i for i in pareto_front_indices if i < len(pareto_population)] + print(f"Final pareto_front_indices: {pareto_front_indices}, pareto_population size: {len(pareto_population)}") + + if not pareto_front_indices: + print("No valid solution found after TOPSIS due to empty pareto front indices.") + return None + + global_pareto_front = [pareto_population[i] for i in pareto_front_indices] + + # Get the top N solutions with TOPSIS + top_n = 3 # Adjust this value based on how many top solutions you want to explore + self.best_solution = self.topsis_decision_making(global_pareto_front, top_n=top_n) + + # Print the top N solutions + if self.best_solution: + print("Top solutions after TOPSIS:") + for idx, solution in enumerate(self.best_solution, 1): + print(f"Solution {idx}: LCC = {solution.individual['lcc']}, " + f"LCE = {solution.individual['total_energy_consumption']}") + else: + print("No valid solutions found after TOPSIS.") + + if pareto_population: + self.plot_pareto_front(pareto_population) + + return self.best_solution + + @staticmethod + def design_period_identification(building): + def get_start_end_indices(max_day_index, total_days): + if max_day_index > 0 and max_day_index < total_days - 1: + start_index = (max_day_index - 1) * 24 + end_index = (max_day_index + 2) * 24 + elif max_day_index == 0: + start_index = 0 + end_index = (max_day_index + 2) * 24 + else: + start_index = (max_day_index - 1) * 24 + end_index = total_days * 24 + return start_index, end_index + + # Calculate daily demands + heating_daily_demands = [sum(building.heating_demand[cte.HOUR][i:i + 24]) for i in + range(0, len(building.heating_demand[cte.HOUR]), 24)] + cooling_daily_demands = [sum(building.cooling_demand[cte.HOUR][i:i + 24]) for i in + range(0, len(building.cooling_demand[cte.HOUR]), 24)] + dhw_daily_demands = [sum(building.domestic_hot_water_heat_demand[cte.HOUR][i:i + 24]) for i in + range(0, len(building.domestic_hot_water_heat_demand[cte.HOUR]), 24)] + # Get the day with maximum demand for each type + heating_max_day = heating_daily_demands.index(max(heating_daily_demands)) + cooling_max_day = cooling_daily_demands.index(max(cooling_daily_demands)) + dhw_max_day = dhw_daily_demands.index(max(dhw_daily_demands)) + # Get the start and end indices for each demand type + heating_start, heating_end = get_start_end_indices(heating_max_day, len(heating_daily_demands)) + cooling_start, cooling_end = get_start_end_indices(cooling_max_day, len(cooling_daily_demands)) + dhw_start, dhw_end = get_start_end_indices(dhw_max_day, len(dhw_daily_demands)) + # Return the design period energy demands + return { + f'{cte.HEATING}': {'demands': building.heating_demand[cte.HOUR][heating_start:heating_end], + 'start_index': heating_start, 'end_index': heating_end}, + f'{cte.COOLING}': {'demands': building.cooling_demand[cte.HOUR][cooling_start:cooling_end], + 'start_index': cooling_start, 'end_index': cooling_end}, + f'{cte.DOMESTIC_HOT_WATER}': {'demands': building.domestic_hot_water_heat_demand[cte.HOUR][dhw_start:dhw_end], + 'start_index': dhw_start, 'end_index': dhw_end} + } + + def topsis_decision_making(self, pareto_front, top_n=5): + """ + Perform TOPSIS decision-making to choose the best solutions from the Pareto front. + + :param pareto_front: List of individuals in the Pareto front + :param top_n: Number of top solutions to select based on TOPSIS ranking + :return: List of top N individuals based on TOPSIS ranking + """ + # Filter out infinite values from the pareto front before processing + pareto_front = [ind for ind in pareto_front if all(math.isfinite(v) for v in ind.individual['fitness_score'])] + if not pareto_front: + return None + + # Step 1: Normalize the objective functions (cost and energy consumption) + min_lcc = min([ind.individual['lcc'] for ind in pareto_front]) + max_lcc = max([ind.individual['lcc'] for ind in pareto_front]) + min_lce = min([ind.individual['total_energy_consumption'] for ind in pareto_front]) + max_lce = max([ind.individual['total_energy_consumption'] for ind in pareto_front]) + + normalized_pareto_front = [] + for ind in pareto_front: + normalized_lcc = (ind.individual['lcc'] - min_lcc) / (max_lcc - min_lcc) if max_lcc > min_lcc else 0 + normalized_lce = (ind.individual['total_energy_consumption'] - min_lce) / ( + max_lce - min_lce) if max_lce > min_lce else 0 + normalized_pareto_front.append((ind, normalized_lcc, normalized_lce)) + + # Step 2: Calculate the ideal and worst solutions + ideal_solution = (0, 0) # Ideal is minimum LCC and minimum LCE (0, 0 after normalization) + worst_solution = (1, 1) # Worst is maximum LCC and maximum LCE (1, 1 after normalization) + + # Step 3: Calculate the distance to the ideal and worst solutions + best_distances = [] + worst_distances = [] + for ind, normalized_lcc, normalized_lce in normalized_pareto_front: + distance_to_ideal = math.sqrt( + (normalized_lcc - ideal_solution[0]) ** 2 + (normalized_lce - ideal_solution[1]) ** 2) + distance_to_worst = math.sqrt( + (normalized_lcc - worst_solution[0]) ** 2 + (normalized_lce - worst_solution[1]) ** 2) + best_distances.append(distance_to_ideal) + worst_distances.append(distance_to_worst) + + # Step 4: Calculate relative closeness to the ideal solution + similarity = [worst / (best + worst) for best, worst in zip(best_distances, worst_distances)] + + # Step 5: Select the top N individuals with the highest similarity scores + top_indices = sorted(range(len(similarity)), key=lambda i: similarity[i], reverse=True)[:top_n] + top_solutions = [pareto_front[i] for i in top_indices] + + # Plot the similarity scores + self.plot_topsis_similarity(similarity) + + return top_solutions + + @staticmethod + def plot_topsis_similarity(similarity): + """ + Plot the TOPSIS similarity scores for visualization. + + :param similarity: List of similarity scores for each individual in the Pareto front + """ + plt.figure(figsize=(10, 6)) + plt.plot(range(len(similarity)), similarity, 'bo-', label='TOPSIS Similarity Scores') + plt.xlabel('Pareto Front Solution Index') + plt.ylabel('Similarity Score') + plt.title('TOPSIS Similarity Scores for Pareto Front Solutions') + plt.legend() + plt.grid(True) + plt.show() + + @staticmethod + def plot_pareto_front(pareto_population): + # Extract LCC and LCE for plotting + lcc_values = [individual.individual['lcc'] for individual in pareto_population] + lce_values = [individual.individual['total_energy_consumption'] for individual in pareto_population] + plt.figure(figsize=(10, 6)) + plt.scatter(lcc_values, lce_values, color='blue', label='Pareto Front', alpha=0.6, edgecolors='w', s=80) + plt.title('Pareto Front for Life Cycle Cost vs Life Cycle Energy') + plt.xlabel('Life Cycle Cost (LCC)') + plt.ylabel('Life Cycle Energy (LCE)') + plt.grid(True) + plt.legend() + plt.show() diff --git a/energy_system_modelling_package/energy_system_modelling_factories/system_sizing_methods/genetic_algorithm/multi_objective_genetic_algorithm_rethinking.py b/energy_system_modelling_package/energy_system_modelling_factories/system_sizing_methods/genetic_algorithm/multi_objective_genetic_algorithm_rethinking.py new file mode 100644 index 00000000..23ef7ff2 --- /dev/null +++ b/energy_system_modelling_package/energy_system_modelling_factories/system_sizing_methods/genetic_algorithm/multi_objective_genetic_algorithm_rethinking.py @@ -0,0 +1,94 @@ +import copy +import math +import random +import hub.helpers.constants as cte +from energy_system_modelling_package.energy_system_modelling_factories.system_sizing_methods.genetic_algorithm.individual import \ + Individual +import matplotlib.pyplot as plt +import time + + +class MultiObjectiveGeneticAlgorithm: + def __init__(self, population_size=100, generations=50, crossover_rate=0.8, mutation_rate=0.1, + optimization_scenario=None, output_path=None): + self.population_size = population_size + self.population = [] + self.generations = generations + self.crossover_rate = crossover_rate + self.mutation_rate = mutation_rate + self.optimization_scenario = optimization_scenario + self.list_of_solutions = [] + self.best_solution = None + self.best_solution_generation = None + self.output_path = output_path + +# Initialize Population + def initialize_population(self, building, energy_system): + design_period_start_time = time.time() + design_period_energy_demands = self.design_period_identification(building) + design_period_time = time.time() - design_period_start_time + print(f"design period identification took {design_period_time:.2f} seconds") + initializing_time_start = time.time() + attempts = 0 + max_attempts = self.population_size * 20 + while len(self.population) < self.population_size and attempts < max_attempts: + individual = Individual(building=building, + energy_system=energy_system, + design_period_energy_demands=design_period_energy_demands, + optimization_scenario=self.optimization_scenario) + individual.initialization() + individual.score_evaluation() + attempts += 1 + if individual.feasibility: + self.population.append(individual) + if len(self.population) < self.population_size: + raise RuntimeError(f"Could not generate a feasible population of size {self.population_size}. " + f"Only {len(self.population)} feasible individuals were generated.") + initializing_time = time.time() - initializing_time_start + print(f"initializing took {initializing_time:.2f} seconds") + + + @staticmethod + def design_period_identification(building): + def get_start_end_indices(max_day_index, total_days): + if max_day_index > 0 and max_day_index < total_days - 1: + start_index = (max_day_index - 1) * 24 + end_index = (max_day_index + 2) * 24 + elif max_day_index == 0: + start_index = 0 + end_index = (max_day_index + 2) * 24 + else: + start_index = (max_day_index - 1) * 24 + end_index = total_days * 24 + return start_index, end_index + + # Calculate daily demands + heating_daily_demands = [sum(building.heating_demand[cte.HOUR][i:i + 24]) for i in + range(0, len(building.heating_demand[cte.HOUR]), 24)] + cooling_daily_demands = [sum(building.cooling_demand[cte.HOUR][i:i + 24]) for i in + range(0, len(building.cooling_demand[cte.HOUR]), 24)] + dhw_daily_demands = [sum(building.domestic_hot_water_heat_demand[cte.HOUR][i:i + 24]) for i in + range(0, len(building.domestic_hot_water_heat_demand[cte.HOUR]), 24)] + # Get the day with maximum demand for each type + heating_max_day = heating_daily_demands.index(max(heating_daily_demands)) + cooling_max_day = cooling_daily_demands.index(max(cooling_daily_demands)) + dhw_max_day = dhw_daily_demands.index(max(dhw_daily_demands)) + # Get the start and end indices for each demand type + heating_start, heating_end = get_start_end_indices(heating_max_day, len(heating_daily_demands)) + cooling_start, cooling_end = get_start_end_indices(cooling_max_day, len(cooling_daily_demands)) + dhw_start, dhw_end = get_start_end_indices(dhw_max_day, len(dhw_daily_demands)) + # Return the design period energy demands + return { + f'{cte.HEATING}': {'demands': building.heating_demand[cte.HOUR][heating_start:heating_end], + 'start_index': heating_start, 'end_index': heating_end}, + f'{cte.COOLING}': {'demands': building.cooling_demand[cte.HOUR][cooling_start:cooling_end], + 'start_index': cooling_start, 'end_index': cooling_end}, + f'{cte.DOMESTIC_HOT_WATER}': {'demands': building.domestic_hot_water_heat_demand[cte.HOUR][dhw_start:dhw_end], + 'start_index': dhw_start, 'end_index': dhw_end} + } + + def solve_ga(self, building, energy_system): + self.initialize_population(building, energy_system) + for individual in self.population: + print(individual.individual) + print([ind.fitness_score for ind in self.population]) diff --git a/energy_system_modelling_package/energy_system_modelling_factories/system_sizing_methods/genetic_algorithm/single_objective_genetic_algorithm.py b/energy_system_modelling_package/energy_system_modelling_factories/system_sizing_methods/genetic_algorithm/single_objective_genetic_algorithm.py new file mode 100644 index 00000000..b7e5f7c3 --- /dev/null +++ b/energy_system_modelling_package/energy_system_modelling_factories/system_sizing_methods/genetic_algorithm/single_objective_genetic_algorithm.py @@ -0,0 +1,342 @@ +import copy +import math +import random +import hub.helpers.constants as cte +from energy_system_modelling_package.energy_system_modelling_factories.system_sizing_methods.genetic_algorithm.individual import \ + Individual + + +class SingleObjectiveGeneticAlgorithm: + def __init__(self, population_size=100, generations=20, crossover_rate=0.8, mutation_rate=0.1, + optimization_scenario=None, output_path=None): + self.population_size = population_size + self.population = [] + self.generations = generations + self.crossover_rate = crossover_rate + self.mutation_rate = mutation_rate + self.optimization_scenario = optimization_scenario + self.list_of_solutions = [] + self.best_solution = None + self.best_solution_generation = None + self.output_path = output_path + + # Initialize Population + def initialize_population(self, building, energy_system): + """ + Initialize a population of individuals with feasible configurations for optimizing the sizes of + generation and storage components of an energy system. + + :param building: Building object with associated data + :param energy_system: Energy system to optimize + """ + design_period_energy_demands = self.design_period_identification(building) + attempts = 0 # Track attempts to avoid an infinite loop in rare cases + max_attempts = self.population_size * 5 + + while len(self.population) < self.population_size and attempts < max_attempts: + individual = Individual(building=building, + energy_system=energy_system, + design_period_energy_demands=design_period_energy_demands, + optimization_scenario=self.optimization_scenario) + + individual.initialization() + attempts += 1 + + # Enhanced feasibility check + if self.initial_population_feasibility_check(individual, energy_system.demand_types, design_period_energy_demands): + self.population.append(individual) + + # Raise an error or print a warning if the population size goal is not met after max_attempts + if len(self.population) < self.population_size: + raise RuntimeError(f"Could not generate a feasible population of size {self.population_size}. " + f"Only {len(self.population)} feasible individuals were generated.") + + @staticmethod + def initial_population_feasibility_check(individual, demand_types, design_period_demands): + """ + Check if the individual meets basic feasibility requirements for heating, cooling, and DHW capacities + and storage volume. + + :param individual: Individual to check + :param demand_types: List of demand types (e.g., heating, cooling, DHW) + :param design_period_demands: Design period demand values for heating, cooling, and DHW + :return: True if feasible, False otherwise + """ + # Calculate total heating and cooling capacities + total_heating_capacity = sum( + component['heating_capacity'] for component in individual.individual['Generation Components'] + if component['heating_capacity'] is not None + ) + total_cooling_capacity = sum( + component['cooling_capacity'] for component in individual.individual['Generation Components'] + if component['cooling_capacity'] is not None + ) + + # Maximum demands for each demand type (converted to kW) + max_heating_demand = max(design_period_demands[cte.HEATING]['demands']) / cte.WATTS_HOUR_TO_JULES + max_cooling_demand = max(design_period_demands[cte.COOLING]['demands']) / cte.WATTS_HOUR_TO_JULES + max_dhw_demand = max(design_period_demands[cte.DOMESTIC_HOT_WATER]['demands']) / cte.WATTS_HOUR_TO_JULES + + # Check heating capacity feasibility + if cte.HEATING in demand_types and total_heating_capacity < 0.5 * max_heating_demand: + return False + + # Check DHW capacity feasibility + if cte.DOMESTIC_HOT_WATER in demand_types and total_heating_capacity < 0.5 * max_dhw_demand: + return False + + # Check cooling capacity feasibility + if cte.COOLING in demand_types and total_cooling_capacity < 0.5 * max_cooling_demand: + return False + + # Check storage volume feasibility + total_volume = sum( + component['volume'] for component in individual.individual['Energy Storage Components'] + if component['volume'] is not None + ) + # Limit storage to 10% of building's available space + max_storage_volume = individual.available_space * 0.1 + if total_volume > max_storage_volume: + return False + + return True # Feasible if all checks are passed + + def order_population(self): + """ + ordering the population based on the fitness score in ascending order + :return: + """ + self.population = sorted(self.population, key=lambda x: x.fitness_score) + + def tournament_selection(self): + selected = [] + for _ in range(len(self.population)): + i, j = random.sample(range(self.population_size), 2) + if self.population[i].individual['fitness_score'] < self.population[j].individual['fitness_score']: + selected.append(copy.deepcopy(self.population[i])) + else: + selected.append(copy.deepcopy(self.population[j])) + return selected + + def crossover(self, parent1, parent2): + """ + Crossover between two parents to produce two children. + + swaps generation components and storage components between the two parents with a 50% chance. + + :param parent1: First parent individual. + :param parent2: second parent individual. + :return: Two child individuals (child1 and child2). + """ + if random.random() < self.crossover_rate: + # Deep copy of the parents to create children + child1, child2 = copy.deepcopy(parent1), copy.deepcopy(parent2) + # Crossover for Generation Components + for i in range(len(parent1.individual['Generation Components'])): + if random.random() < 0.5: + # swap the entire generation component + child1.individual['Generation Components'][i], child2.individual['Generation Components'][i] = ( + child2.individual['Generation Components'][i], + child1.individual['Generation Components'][i] + ) + + # Crossover for Energy storage Components + for i in range(len(parent1.individual['Energy Storage Components'])): + if random.random() < 0.5: + # swap the entire storage component + child1.individual['Energy Storage Components'][i], child2.individual['Energy Storage Components'][i] = ( + child2.individual['Energy Storage Components'][i], + child1.individual['Energy Storage Components'][i] + ) + + return child1, child2 + else: + # If crossover does not happen, return copies of the original parents + return copy.deepcopy(parent1), copy.deepcopy(parent2) + + def mutate(self, individual, building, energy_system): + """ + Mutates the individual's generation and storage components. + + - `individual`: The individual to mutate (contains generation and storage components). + - `building`: Building data that contains constraints such as peak heating load and available space. + + Returns the mutated individual. + """ + design_period_energy_demands = self.design_period_identification(building) + # Mutate Generation Components + for generation_component in individual['Generation Components']: + if random.random() < self.mutation_rate: + if (generation_component['nominal_heating_efficiency'] is not None and cte.HEATING or cte.DOMESTIC_HOT_WATER in + energy_system.demand_types): + # Mutate heating capacity + if cte.HEATING in energy_system.demand_types: + generation_component['heating_capacity'] = random.uniform( + 0, max(design_period_energy_demands[cte.HEATING]['demands']) / cte.WATTS_HOUR_TO_JULES) + else: + generation_component['heating_capacity'] = random.uniform( + 0, max(design_period_energy_demands[cte.DOMESTIC_HOT_WATER]['demands']) / cte.WATTS_HOUR_TO_JULES) + if generation_component['nominal_cooling_efficiency'] is not None and cte.COOLING in energy_system.demand_types: + # Mutate cooling capacity + generation_component['cooling_capacity'] = random.uniform( + 0, max(design_period_energy_demands[cte.COOLING]['demands']) / cte.WATTS_HOUR_TO_JULES) + # Mutate storage Components + for storage_component in individual['Energy Storage Components']: + if random.random() < self.mutation_rate: + if storage_component['type'] == f'{cte.THERMAL}_storage': + # Mutate the volume of thermal storage + max_available_space = 0.01 * building.volume / building.storeys_above_ground + storage_component['volume'] = random.uniform(0, max_available_space) + if storage_component['heating_coil_capacity'] is not None: + if cte.HEATING in energy_system.demand_types: + storage_component['heating_coil_capacity'] = random.uniform(0, max( + design_period_energy_demands[cte.HEATING]['demands']) / cte.WATTS_HOUR_TO_JULES) + else: + storage_component['heating_coil_capacity'] = random.uniform(0, max( + design_period_energy_demands[cte.DOMESTIC_HOT_WATER]['demands']) / cte.WATTS_HOUR_TO_JULES) + return individual + + def solve_ga(self, building, energy_system): + """ + solving GA for a single energy system. Here are the steps: + 1. Initialize population using the "initialize_population" method in this class. + 2. Evaluate the initial population using the "score_evaluation" method in the Individual class. + 3. sort population based on fitness score. + 4. Repeat selection, crossover, and mutation for a fixed number of generations. + 5. Track the best solution found during the optimization process. + + :param building: Building object for the energy system. + :param energy_system: Energy system to optimize. + :return: Best solution after running the GA. + """ + # step 1: Initialize the population + self.initialize_population(building, energy_system) + # step 2: Evaluate the initial population + for individual in self.population: + individual.score_evaluation() + # step 3: Order population based on fitness scores + self.order_population() + print([individual.fitness_score for individual in self.population]) + # Track the best solution + self.best_solution = self.population[0] + self.best_solution_generation = 0 + self.list_of_solutions.append(copy.deepcopy(self.best_solution.individual)) + # step 4: Run GA for a fixed number of generations + for generation in range(1, self.generations): + print(f"Generation {generation}") + # selection (using tournament selection) + selected_population = self.tournament_selection() + # Create the next generation through crossover and mutation + next_population = [] + for i in range(0, self.population_size, 2): + parent1 = selected_population[i] + parent2 = selected_population[i + 1] if (i + 1) < len(selected_population) else selected_population[0] + # step 5: Apply crossover + child1, child2 = self.crossover(parent1, parent2) + # step 6: Apply mutation + self.mutate(child1.individual, building, energy_system) + self.mutate(child2.individual, building, energy_system) + # step 7: Evaluate the children + child1.score_evaluation() + child2.score_evaluation() + next_population.extend([child1, child2]) + # Replace old population with the new one + self.population = next_population + # step 8: sort the new population based on fitness + self.order_population() + print([individual.fitness_score for individual in self.population]) + # Track the best solution found in this generation + if self.population[0].individual['fitness_score'] < self.best_solution.individual['fitness_score']: + self.best_solution = self.population[0] + self.best_solution_generation = generation + # store the best solution in the list of solutions + self.list_of_solutions.append(copy.deepcopy(self.population[0].individual)) + print(f"Best solution found in generation {self.best_solution_generation}") + print(f"Best solution: {self.best_solution.individual}") + return self.best_solution + + @staticmethod + def topsis_decision_making(pareto_front): + """ + Perform TOPSIS decision-making to choose the best solution from the Pareto front. + + :param pareto_front: List of individuals in the Pareto front + :return: The best individual based on TOPSIS ranking + """ + # Step 1: Normalize the objective functions (cost and energy consumption) + min_lcc = min([ind.individual['lcc'] for ind in pareto_front]) + max_lcc = max([ind.individual['lcc'] for ind in pareto_front]) + min_lce = min([ind.individual['total_energy_consumption'] for ind in pareto_front]) + max_lce = max([ind.individual['total_energy_consumption'] for ind in pareto_front]) + + normalized_pareto_front = [] + for ind in pareto_front: + normalized_lcc = (ind.individual['lcc'] - min_lcc) / (max_lcc - min_lcc) if max_lcc > min_lcc else 0 + normalized_lce = (ind.individual['total_energy_consumption'] - min_lce) / ( + max_lce - min_lce) if max_lce > min_lce else 0 + normalized_pareto_front.append((ind, normalized_lcc, normalized_lce)) + + # Step 2: Calculate the ideal and worst solutions + ideal_solution = (0, 0) # Ideal is minimum LCC and minimum LCE (0, 0 after normalization) + worst_solution = (1, 1) # Worst is maximum LCC and maximum LCE (1, 1 after normalization) + + # Step 3: Calculate the distance to the ideal and worst solutions + best_distances = [] + worst_distances = [] + + for ind, normalized_lcc, normalized_lce in normalized_pareto_front: + distance_to_ideal = math.sqrt( + (normalized_lcc - ideal_solution[0]) ** 2 + (normalized_lce - ideal_solution[1]) ** 2) + distance_to_worst = math.sqrt( + (normalized_lcc - worst_solution[0]) ** 2 + (normalized_lce - worst_solution[1]) ** 2) + best_distances.append(distance_to_ideal) + worst_distances.append(distance_to_worst) + + # Step 4: Calculate relative closeness to the ideal solution + similarity = [worst / (best + worst) for best, worst in zip(best_distances, worst_distances)] + + # Step 5: Select the individual with the highest similarity score + best_index = similarity.index(max(similarity)) + best_solution = pareto_front[best_index] + + return best_solution + + @staticmethod + def design_period_identification(building): + def get_start_end_indices(max_day_index, total_days): + if max_day_index > 0 and max_day_index < total_days - 1: + start_index = (max_day_index - 1) * 24 + end_index = (max_day_index + 2) * 24 + elif max_day_index == 0: + start_index = 0 + end_index = (max_day_index + 2) * 24 + else: + start_index = (max_day_index - 1) * 24 + end_index = total_days * 24 + return start_index, end_index + + # Calculate daily demands + heating_daily_demands = [sum(building.heating_demand[cte.HOUR][i:i + 24]) for i in + range(0, len(building.heating_demand[cte.HOUR]), 24)] + cooling_daily_demands = [sum(building.cooling_demand[cte.HOUR][i:i + 24]) for i in + range(0, len(building.cooling_demand[cte.HOUR]), 24)] + dhw_daily_demands = [sum(building.domestic_hot_water_heat_demand[cte.HOUR][i:i + 24]) for i in + range(0, len(building.domestic_hot_water_heat_demand[cte.HOUR]), 24)] + # Get the day with maximum demand for each type + heating_max_day = heating_daily_demands.index(max(heating_daily_demands)) + cooling_max_day = cooling_daily_demands.index(max(cooling_daily_demands)) + dhw_max_day = dhw_daily_demands.index(max(dhw_daily_demands)) + # Get the start and end indices for each demand type + heating_start, heating_end = get_start_end_indices(heating_max_day, len(heating_daily_demands)) + cooling_start, cooling_end = get_start_end_indices(cooling_max_day, len(cooling_daily_demands)) + dhw_start, dhw_end = get_start_end_indices(dhw_max_day, len(dhw_daily_demands)) + # Return the design period energy demands + return { + f'{cte.HEATING}': {'demands': building.heating_demand[cte.HOUR][heating_start:heating_end], + 'start_index': heating_start, 'end_index': heating_end}, + f'{cte.COOLING}': {'demands': building.cooling_demand[cte.HOUR][cooling_start:cooling_end], + 'start_index': cooling_start, 'end_index': cooling_end}, + f'{cte.DOMESTIC_HOT_WATER}': {'demands': building.domestic_hot_water_heat_demand[cte.HOUR][dhw_start:dhw_end], + 'start_index': dhw_start, 'end_index': dhw_end} + } + diff --git a/energy_system_modelling_package/energy_system_modelling_factories/system_sizing_methods/optimal_sizing.py b/energy_system_modelling_package/energy_system_modelling_factories/system_sizing_methods/optimal_sizing.py new file mode 100644 index 00000000..2127dcc4 --- /dev/null +++ b/energy_system_modelling_package/energy_system_modelling_factories/system_sizing_methods/optimal_sizing.py @@ -0,0 +1,42 @@ +import hub.helpers.constants as cte +from energy_system_modelling_package.energy_system_modelling_factories.system_sizing_methods.genetic_algorithm.single_objective_genetic_algorithm import \ + SingleObjectiveGeneticAlgorithm + + +class OptimalSizing: + def __init__(self, city, optimization_scenario): + self.city = city + self.optimization_scenario = optimization_scenario + + def enrich_buildings(self): + for building in self.city.buildings: + for energy_system in building.energy_systems: + if len(energy_system.generation_systems) == 1 and energy_system.generation_systems[0].energy_storage_systems is None: + if energy_system.generation_systems[0].system_type == cte.PHOTOVOLTAIC: + pass + else: + if cte.HEATING in energy_system.demand_types: + if cte.DOMESTIC_HOT_WATER in energy_system.demand_types: + design_load = max([building.heating_demand[cte.HOUR][i] + + building.domestic_hot_water_heat_demand[cte.HOUR][i] for i in + range(len(building.heating_demand))]) / cte.WATTS_HOUR_TO_JULES + else: + design_load = building.heating_peak_load[cte.YEAR][0] + energy_system.generation_systems[0].nominal_heat_output = design_load + elif cte.COOLING in energy_system.demand_types: + energy_system.generation_systems[0].nominal_cooling_output = building.cooling_peak_load[cte.YEAR][0] + else: + optimized_system = SingleObjectiveGeneticAlgorithm(optimization_scenario=self.optimization_scenario).solve_ga(building, energy_system) + for generation_system in energy_system.generation_systems: + system_type = generation_system.system_type + for generation_component in optimized_system.individual['Generation Components']: + if generation_component['type'] == system_type: + generation_system.nominal_heat_output = generation_component['heating_capacity'] + generation_system.nominal_cooling_output = generation_component['cooling_capacity'] + if generation_system.energy_storage_systems is not None: + for storage_system in generation_system.energy_storage_systems: + storage_type = f'{storage_system.type_energy_stored}_storage' + for storage_component in optimized_system.individual['Energy Storage Components']: + if storage_component['type'] == storage_type: + storage_system.nominal_capacity = storage_component['capacity'] + storage_system.volume = storage_component['volume'] diff --git a/energy_system_modelling_package/energy_system_modelling_factories/system_sizing_methods/peak_load_sizing.py b/energy_system_modelling_package/energy_system_modelling_factories/system_sizing_methods/peak_load_sizing.py new file mode 100644 index 00000000..1f1fe74a --- /dev/null +++ b/energy_system_modelling_package/energy_system_modelling_factories/system_sizing_methods/peak_load_sizing.py @@ -0,0 +1,99 @@ +import hub.helpers.constants as cte + + +class PeakLoadSizing: + def __init__(self, city, default_primary_unit_percentage=0.7, storage_peak_coverage=3): + self.city = city + self.default_primary_unit_percentage = default_primary_unit_percentage + self.storage_peak_coverage = storage_peak_coverage + + def enrich_buildings(self): + total_demand = 0 + for building in self.city.buildings: + energy_systems = building.energy_systems + for energy_system in energy_systems: + if cte.HEATING in energy_system.demand_types: + if cte.DOMESTIC_HOT_WATER in energy_system.demand_types: + total_demand = [(building.heating_demand[cte.HOUR][i] + + building.domestic_hot_water_heat_demand[cte.HOUR][i]) / cte.WATTS_HOUR_TO_JULES + for i in range(len(building.heating_demand[cte.HOUR]))] + else: + total_demand = building.heating_peak_load[cte.YEAR] + design_load = max(total_demand) + self.allocate_capacity(energy_system, design_load, cte.HEATING, self.default_primary_unit_percentage) + if cte.COOLING in energy_system.demand_types: + cooling_design_load = building.cooling_peak_load[cte.YEAR][0] + self.allocate_capacity(energy_system, cooling_design_load, cte.COOLING, + self.default_primary_unit_percentage) + + elif cte.COOLING in energy_system.demand_types: + design_load = building.cooling_peak_load[cte.YEAR][0] + self.allocate_capacity(energy_system, design_load, cte.COOLING, self.default_primary_unit_percentage) + elif cte.DOMESTIC_HOT_WATER in energy_system.demand_types: + design_load = building.domestic_hot_water_peak_load[cte.YEAR][0] + self.allocate_capacity(energy_system, design_load, cte.DOMESTIC_HOT_WATER, + self.default_primary_unit_percentage) + + for generation_system in energy_system.generation_systems: + storage_systems = generation_system.energy_storage_systems + if storage_systems is not None: + if cte.DOMESTIC_HOT_WATER in energy_system.demand_types: + operation_range = 10 + else: + operation_range = 20 + for storage_system in storage_systems: + if storage_system.type_energy_stored == cte.THERMAL: + self.tes_sizing(storage_system, max(total_demand), self.storage_peak_coverage, operation_range) + + def allocate_capacity(self, energy_system, design_load, demand_type, default_primary_unit_percentage): + if len(energy_system.generation_systems) == 1: + # If there's only one generation system, it gets the full design load. + if demand_type == cte.HEATING or demand_type == cte.DOMESTIC_HOT_WATER: + energy_system.generation_systems[0].nominal_heat_output = design_load + elif demand_type == cte.COOLING: + energy_system.generation_systems[0].nominal_cooling_output = design_load + else: + cooling_equipments_number = 0 + # Distribute the load among generation systems. + max_efficiency = 0 + main_generation_unit = None + for generation_system in energy_system.generation_systems: + if demand_type == cte.HEATING or demand_type == cte.DOMESTIC_HOT_WATER: + if max_efficiency < float(generation_system.heat_efficiency): + max_efficiency = float(generation_system.heat_efficiency) + main_generation_unit = generation_system + elif demand_type == cte.COOLING and generation_system.fuel_type == cte.ELECTRICITY: + cooling_equipments_number += 1 + if max_efficiency < float(generation_system.cooling_efficiency): + max_efficiency = float(generation_system.heat_efficiency) + main_generation_unit = generation_system + + for generation_system in energy_system.generation_systems: + if generation_system.system_type == main_generation_unit.system_type: + if demand_type == cte.HEATING or demand_type == cte.DOMESTIC_HOT_WATER: + generation_system.nominal_heat_output = round(default_primary_unit_percentage * design_load) + elif demand_type == cte.COOLING and cooling_equipments_number > 1: + generation_system.nominal_cooling_output = round(default_primary_unit_percentage * design_load) + else: + generation_system.nominal_cooling_output = design_load + else: + if demand_type == cte.HEATING or demand_type == cte.DOMESTIC_HOT_WATER: + generation_system.nominal_heat_output = round(((1 - default_primary_unit_percentage) * design_load / + (len(energy_system.generation_systems) - 1))) + elif demand_type == cte.COOLING and cooling_equipments_number > 1: + generation_system.nominal_cooling_output = round(((1 - default_primary_unit_percentage) * design_load / + (len(energy_system.generation_systems) - 1))) + + + @staticmethod + def tes_sizing(storage, peak_load, coverage, operational_temperature_range): + storage.volume = round((peak_load * coverage * cte.WATTS_HOUR_TO_JULES) / + (cte.WATER_HEAT_CAPACITY * cte.WATER_DENSITY * operational_temperature_range)) + + @staticmethod + def cooling_equipments(energy_system): + counter = 0 + for generation_system in energy_system.generation_systems: + if generation_system.fuel_type == cte.ELECTRICITY: + counter += 1 + return counter diff --git a/energy_system_modelling_package/energy_system_retrofit/energy_system_retrofit_report.py b/energy_system_modelling_package/energy_system_retrofit/energy_system_retrofit_report.py new file mode 100644 index 00000000..1c211eba --- /dev/null +++ b/energy_system_modelling_package/energy_system_retrofit/energy_system_retrofit_report.py @@ -0,0 +1,595 @@ +import os +import hub.helpers.constants as cte +import matplotlib.pyplot as plt +from matplotlib import cm +from energy_system_modelling_package.report_creation import LatexReport +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, current_status_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 + self.report = LatexReport('energy_system_retrofit_report', + 'Energy System Retrofit Report', self.retrofit_scenario, output_path) + self.system_schemas_path = (Path(__file__).parent.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 $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 + 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')) + ] + 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 plot_monthly_energy_demands(self, data, file_name, title): + # Data preparation + months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] + 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_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(4, 1, figsize=(20, 16), dpi=96) + fig.suptitle(title, fontsize=16, weight='bold', alpha=.8) + + 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, 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 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', 'Central 4 Pipes Air to Water Heat Pump and Gas Boiler with Independent Water Heating and PV']: + 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] / 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, 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], + 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 = 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 ' + '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 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 + 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'] + 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_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 - 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), + } + retrofitted_lcc_components_sqm = { + 'Capital Cost': retrofitted_capex / 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), + } + + labels = ['Current Status', 'Retrofitted Status'] + categories = ['Capital Cost', 'Operational Cost', 'Maintenance Cost', 'End of Life Cost'] + colors = ['#2196f3', '#ff5a5f', '#4caf50', '#ffc107'] # Added new color + + # 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(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 + + # 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') + 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() + + # 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) + 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 ' + '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') + 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() + self.report.compile_to_pdf() diff --git a/energy_system_modelling_package/energy_system_retrofit/energy_system_retrofit_results.py b/energy_system_modelling_package/energy_system_retrofit/energy_system_retrofit_results.py new file mode 100644 index 00000000..1b84601a --- /dev/null +++ b/energy_system_modelling_package/energy_system_retrofit/energy_system_retrofit_results.py @@ -0,0 +1,176 @@ +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): + energy_consumption_data = {} + for building in city.buildings: + 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 = [] + 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_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 + 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) + + 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 + yearly_appliance = 0 + yearly_lighting = 0 + for building in city.buildings: + 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 + 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 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/energy_system_modelling_package/random_assignation.py b/energy_system_modelling_package/random_assignation.py new file mode 100644 index 00000000..605def1c --- /dev/null +++ b/energy_system_modelling_package/random_assignation.py @@ -0,0 +1,123 @@ +""" +This project aims to assign energy systems archetype names to Montreal buildings. +The random assignation is based on statistical information extracted from different sources, being: +- For residential buildings: + - SHEU 2015: https://oee.nrcan.gc.ca/corporate/statistics/neud/dpa/menus/sheu/2015/tables.cfm +- For non-residential buildings: + - Montreal dataportal: https://dataportalforcities.org/north-america/canada/quebec/montreal + - https://www.eia.gov/consumption/commercial/data/2018/ +""" +import json +import random + +from hub.city_model_structure.building import Building + +energy_systems_format = 'montreal_custom' + +# parameters: +residential_systems_percentage = {'system 1 gas': 15, + 'system 1 electricity': 35, + 'system 2 gas': 0, + 'system 2 electricity': 0, + 'system 3 and 4 gas': 0, + 'system 3 and 4 electricity': 0, + 'system 5 gas': 0, + 'system 5 electricity': 0, + 'system 6 gas': 0, + 'system 6 electricity': 0, + 'system 8 gas': 15, + 'system 8 electricity': 35} + +residential_new_systems_percentage = { + 'Central Hydronic Air and Gas Source Heating System with Unitary Split Cooling and Air Source HP DHW and PV': 100, + 'Central Hydronic Air and Electricity Source Heating System with Unitary Split Cooling and Air Source HP DHW and PV': 0, + 'Central Hydronic Ground and Gas Source Heating System with Unitary Split Cooling and Air Source HP DHW and PV': 0, + 'Central Hydronic Ground and Electricity Source Heating System with Unitary Split Cooling and Air Source HP DHW ' + 'and PV': 0, + 'Central Hydronic Water and Gas Source Heating System with Unitary Split Cooling and Air Source HP DHW and PV': 0, + 'Central Hydronic Water and Electricity Source Heating System with Unitary Split Cooling and Air Source HP DHW ' + 'and PV': 0, + 'Central Hydronic Air and Gas Source Heating System with Unitary Split and Air Source HP DHW': 0, + 'Central Hydronic Air and Electricity Source Heating System with Unitary Split and Air Source HP DHW': 0, + 'Central Hydronic Ground and Gas Source Heating System with Unitary Split and Air Source HP DHW': 0, + 'Central Hydronic Ground and Electricity Source Heating System with Unitary Split and Air Source HP DHW': 0, + 'Central Hydronic Water and Gas Source Heating System with Unitary Split and Air Source HP DHW': 0, + 'Central Hydronic Water and Electricity Source Heating System with Unitary Split and Air Source HP DHW': 0, + 'Rooftop PV System': 0 +} + +non_residential_systems_percentage = {'system 1 gas': 0, + 'system 1 electricity': 0, + 'system 2 gas': 0, + 'system 2 electricity': 0, + 'system 3 and 4 gas': 39, + 'system 3 and 4 electricity': 36, + 'system 5 gas': 0, + 'system 5 electricity': 0, + 'system 6 gas': 13, + 'system 6 electricity': 12, + 'system 8 gas': 0, + 'system 8 electricity': 0} + + +def _retrieve_buildings(path, year_of_construction_field=None, + function_field=None, function_to_hub=None, aliases_field=None): + _buildings = [] + with open(path, 'r', encoding='utf8') as json_file: + _geojson = json.loads(json_file.read()) + for feature in _geojson['features']: + _building = {} + year_of_construction = None + if year_of_construction_field is not None: + year_of_construction = int(feature['properties'][year_of_construction_field]) + function = None + if function_field is not None: + function = feature['properties'][function_field] + if function_to_hub is not None: + # use the transformation dictionary to retrieve the proper function + if function in function_to_hub: + function = function_to_hub[function] + building_name = '' + building_aliases = [] + if 'id' in feature: + building_name = feature['id'] + if aliases_field is not None: + for alias_field in aliases_field: + building_aliases.append(feature['properties'][alias_field]) + _building['year_of_construction'] = year_of_construction + _building['function'] = function + _building['building_name'] = building_name + _building['building_aliases'] = building_aliases + _buildings.append(_building) + return _buildings + + +def call_random(_buildings: [Building], _systems_percentage): + _buildings_with_systems = [] + _systems_distribution = [] + _selected_buildings = list(range(0, len(_buildings))) + random.shuffle(_selected_buildings) + total = 0 + maximum = 0 + add_to = 0 + for _system in _systems_percentage: + if _systems_percentage[_system] > 0: + number_of_buildings = round(_systems_percentage[_system] / 100 * len(_selected_buildings)) + _systems_distribution.append({'system': _system, 'number': _systems_percentage[_system], + 'number_of_buildings': number_of_buildings}) + if number_of_buildings > maximum: + maximum = number_of_buildings + add_to = len(_systems_distribution) - 1 + total += number_of_buildings + missing = 0 + if total != len(_selected_buildings): + missing = len(_selected_buildings) - total + if missing != 0: + _systems_distribution[add_to]['number_of_buildings'] += missing + _position = 0 + for case in _systems_distribution: + for i in range(0, case['number_of_buildings']): + _buildings[_selected_buildings[_position]].energy_systems_archetype_name = case['system'] + _position += 1 + return _buildings + diff --git a/energy_system_modelling_package/report_creation.py b/energy_system_modelling_package/report_creation.py new file mode 100644 index 00000000..6298d232 --- /dev/null +++ b/energy_system_modelling_package/report_creation.py @@ -0,0 +1,119 @@ +import subprocess +import datetime +import os +from pathlib import Path + + +class LatexReport: + 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'\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") + + 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'}') + + 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, 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' + total_width -= first_column_width / 16.0 + + if caption: + self.content.append(r'\begin{table}[htbp]') + self.content.append(r'\caption{' + caption + r'}') + self.content.append(r'\centering') + + 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') + + 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, placement='ht'): + if caption: + 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}[' + 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: + f.write('\n'.join(self.content)) + + def compile_to_pdf(self): + subprocess.run(['pdflatex', '-output-directory', str(self.output_path), str(self.file_path)]) + diff --git a/hub/data/energy_systems/montreal_custom_systems.xml b/hub/data/energy_systems/montreal_custom_systems.xml index f3b0466f..258fe86a 100644 --- a/hub/data/energy_systems/montreal_custom_systems.xml +++ b/hub/data/energy_systems/montreal_custom_systems.xml @@ -22,7 +22,7 @@ Air cooled DX with external condenser - 3.23 + 2.5 false @@ -32,8 +32,8 @@ Heat Pump - 2.79 - 3.23 + 2.5 + 3 false @@ -198,7 +198,7 @@ 3 8 -g + Single zone packaged rooftop unit with electrical resistance furnace and baseboards and fuel boiler for acs diff --git a/hub/data/energy_systems/montreal_future_systems.xml b/hub/data/energy_systems/montreal_future_systems.xml index b51c9488..e687524f 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 + 4 @@ -1411,7 +1411,7 @@ 23 - 16 + 17 diff --git a/monthly_dhw.py b/monthly_dhw.py new file mode 100644 index 00000000..754e464a --- /dev/null +++ b/monthly_dhw.py @@ -0,0 +1,56 @@ +import json +from pathlib import Path +import matplotlib.pyplot as plt +import numpy as np +from matplotlib.ticker import MaxNLocator + +output_path = (Path(__file__).parent / 'out_files').resolve() + +# File paths for the three JSON files +file1 = output_path / 'base_case_buildings_data.json' +file2 = output_path / 'air_to_air_hp_buildings_data.json' +file3 = output_path / 'air_to_water_hp_buildings_data.json' + +# Opening and reading all three JSON files at the same time +with open(file1) as f1, open(file2) as f2, open(file3) as f3: + base_case = json.load(f1) + air = json.load(f2) + water = json.load(f3) + +month_names = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] +x = np.arange(len(month_names)) # the label locations +width = 0.25 # the width of the bars + +# Prettier colors for each scenario +colors = ['#66B2FF', '#e74c3c'] # Blue, Red, Green + + +# Plotting heating data for all buildings in a 2x5 grid +fig, axes = plt.subplots(2, 5, figsize=(20, 10), dpi=96) +fig.suptitle('Monthly DHW Consumption Comparison Across Buildings', fontsize=16, weight='bold', alpha=0.8) +axes = axes.flatten() + +for idx, building_name in enumerate(base_case.keys()): + heating_data = [list(data["monthly_dhw_consumption_kWh"].values()) for data in + [base_case[building_name], water[building_name]]] + + ax = axes[idx] + for i, data in enumerate(heating_data): + ax.bar(x + (i - 1) * width, data, width, label=f'Scenario {i+1}', color=colors[i], zorder=2) + + # Grid settings + 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) + + # Axis labels and title + ax.set_title(building_name, fontsize=14, weight='bold', alpha=0.8, pad=10) + ax.set_xticks(x) + ax.set_xticklabels(month_names, rotation=45, ha='right') + ax.xaxis.set_major_locator(MaxNLocator(integer=True)) + ax.yaxis.set_major_locator(MaxNLocator(integer=True)) + if idx % 5 == 0: + ax.set_ylabel('DHW Consumption (kWh)', fontsize=12, labelpad=10) + +fig.legend(['Base Case', 'Scenario 1&2'], loc='upper right', ncol=3) +plt.tight_layout(rect=[0, 0.03, 1, 0.95]) +plt.savefig(output_path / 'monthly_dhw.png') diff --git a/monthly_hvac_plots.py b/monthly_hvac_plots.py new file mode 100644 index 00000000..869430bc --- /dev/null +++ b/monthly_hvac_plots.py @@ -0,0 +1,87 @@ +import json +from pathlib import Path +import matplotlib.pyplot as plt +import numpy as np +from matplotlib.ticker import MaxNLocator + +output_path = (Path(__file__).parent / 'out_files').resolve() + +# File paths for the three JSON files +file1 = output_path / 'base_case_buildings_data.json' +file2 = output_path / 'air_to_air_hp_buildings_data.json' +file3 = output_path / 'air_to_water_hp_buildings_data.json' + +# Opening and reading all three JSON files at the same time +with open(file1) as f1, open(file2) as f2, open(file3) as f3: + base_case = json.load(f1) + air = json.load(f2) + water = json.load(f3) + +month_names = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] +x = np.arange(len(month_names)) # the label locations +width = 0.25 # the width of the bars + +# Prettier colors for each scenario +colors = ['#66B2FF', '#e74c3c', '#2ecc71'] # Blue, Red, Green + + +# Plotting heating data for all buildings in a 2x5 grid +fig, axes = plt.subplots(2, 5, figsize=(20, 10), dpi=96) +fig.suptitle('Monthly Heating Consumption Comparison Across Buildings', fontsize=16, weight='bold', alpha=0.8) +axes = axes.flatten() + +for idx, building_name in enumerate(base_case.keys()): + heating_data = [list(data["monthly_heating_consumption_kWh"].values()) for data in + [base_case[building_name], air[building_name], water[building_name]]] + + ax = axes[idx] + for i, data in enumerate(heating_data): + ax.bar(x + (i - 1) * width, data, width, label=f'Scenario {i+1}', color=colors[i], zorder=2) + + # Grid settings + 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) + + # Axis labels and title + ax.set_title(building_name, fontsize=14, weight='bold', alpha=0.8, pad=10) + ax.set_xticks(x) + ax.set_xticklabels(month_names, rotation=45, ha='right') + ax.xaxis.set_major_locator(MaxNLocator(integer=True)) + ax.yaxis.set_major_locator(MaxNLocator(integer=True)) + if idx % 5 == 0: + ax.set_ylabel('Heating Consumption (kWh)', fontsize=12, labelpad=10) + +fig.legend(['Base Case', 'Scenario 1', 'Scenario 2'], loc='upper right', ncol=3) +plt.tight_layout(rect=[0, 0.03, 1, 0.95]) +plt.savefig(output_path / 'monthly_heating.png') + +# Plotting cooling data for all buildings in a 2x5 grid +# Plotting cooling data for all buildings in a 2x5 grid +fig, axes = plt.subplots(2, 5, figsize=(20, 10), dpi=96) +fig.suptitle('Monthly Cooling Consumption Comparison Across Buildings', fontsize=16, weight='bold', alpha=0.8) +axes = axes.flatten() + +for idx, building_name in enumerate(base_case.keys()): + cooling_data = [list(data["monthly_cooling_consumption_kWh"].values()) for data in + [base_case[building_name], air[building_name], water[building_name]]] + + ax = axes[idx] + for i, data in enumerate(cooling_data): + ax.bar(x + (i - 1) * width, data, width, label=f'Scenario {i+1}', color=colors[i], zorder=2) + + # Grid settings + 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) + + # Axis labels and title + ax.set_title(building_name, fontsize=14, weight='bold', alpha=0.8, pad=10) + ax.set_xticks(x) + ax.set_xticklabels(month_names, rotation=45, ha='right') + ax.xaxis.set_major_locator(MaxNLocator(integer=True)) + ax.yaxis.set_major_locator(MaxNLocator(integer=True)) + if idx % 5 == 0: + ax.set_ylabel('Cooling Consumption (kWh)', fontsize=12, labelpad=10) + +fig.legend(['Base Case', 'Scenario 1', 'Scenario 2'], loc='upper right', ncol=3) +plt.tight_layout(rect=[0, 0.03, 1, 0.95]) +plt.savefig(output_path / 'monthly_cooling.png') \ No newline at end of file diff --git a/peak_load.py b/peak_load.py new file mode 100644 index 00000000..b7e38d4a --- /dev/null +++ b/peak_load.py @@ -0,0 +1,73 @@ +import json +from pathlib import Path +import matplotlib.pyplot as plt +import numpy as np +from matplotlib.ticker import MaxNLocator +from matplotlib.patches import Patch + +output_path = (Path(__file__).parent / 'out_files').resolve() + +# File paths for the three JSON files +file1 = output_path / 'base_case_buildings_data.json' +file2 = output_path / 'air_to_air_hp_buildings_data.json' +file3 = output_path / 'air_to_water_hp_buildings_data.json' + +# Opening and reading all three JSON files at the same time +with open(file1) as f1, open(file2) as f2, open(file3) as f3: + base_case = json.load(f1) + air = json.load(f2) + water = json.load(f3) + +month_names = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] +x = np.arange(len(month_names)) # the label locations + + +# Scenario labels and color palette +scenarios = ['Scenario 1', 'Scenario 2'] +colors = ['#66B2FF', '#e74c3c'] # Blue for Scenario 1, Red for Scenario 2 +width = 0.25 # Width for each bar + +# Creating the grid for peak load comparisons across buildings +fig, axes = plt.subplots(2, 5, figsize=(20, 10), dpi=96) +fig.suptitle('Yearly Heating and Cooling Peak Load Comparison Across Buildings', fontsize=16, weight='bold', alpha=0.8) +axes = axes.flatten() + +for idx, building_name in enumerate(base_case.keys()): + # Extracting heating and cooling peak loads for each scenario + heating_peak_load = [ + air[building_name]["heating_peak_load_kW"], + water[building_name]["heating_peak_load_kW"] + ] + cooling_peak_load = [ + air[building_name]["cooling_peak_load_kW"], + water[building_name]["cooling_peak_load_kW"] + ] + + ax = axes[idx] + x = np.arange(2) # X locations for the "Heating" and "Cooling" groups + + # Plotting each scenario for heating and cooling + for i in range(len(scenarios)): + ax.bar(x[0] - width + i * width, heating_peak_load[i], width, color=colors[i], zorder=2) + ax.bar(x[1] - width + i * width, cooling_peak_load[i], width, color=colors[i], zorder=2) + + # Grid and styling + 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) + + # Axis and title settings + ax.set_title(building_name, fontsize=14, weight='bold', alpha=0.8, pad=10) + ax.set_xticks(x) + ax.set_xticklabels(['Heating Peak Load', 'Cooling Peak Load']) + ax.xaxis.set_major_locator(MaxNLocator(integer=True)) + ax.yaxis.set_major_locator(MaxNLocator(integer=True)) + if idx % 5 == 0: + ax.set_ylabel('Peak Load (kW)', fontsize=12, labelpad=10) + +# Custom legend handles to ensure color match with scenarios +legend_handles = [Patch(color=colors[i], label=scenarios[i]) for i in range(len(scenarios))] + +# Global legend and layout adjustments +fig.legend(handles=legend_handles, loc='upper right', ncol=1) +plt.tight_layout(rect=[0, 0.03, 1, 0.95]) +plt.savefig(output_path / 'peak_loads.png') \ No newline at end of file diff --git a/scripts/energy_system_sizing_and_simulation_factory.py b/scripts/energy_system_sizing_and_simulation_factory.py new file mode 100644 index 00000000..86f0c5ad --- /dev/null +++ b/scripts/energy_system_sizing_and_simulation_factory.py @@ -0,0 +1,35 @@ +""" +EnergySystemSizingSimulationFactory retrieve the energy system archetype sizing and simulation module +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2022 Concordia CERC group +Project Coder Saeed Ranjbar saeed.ranjbar@mail.concordia.ca +""" + +from scripts.system_simulation_models.archetype13 import Archetype13 + + + +class EnergySystemsSimulationFactory: + """ + EnergySystemsFactory class + """ + + def __init__(self, handler, building, output_path): + self._output_path = output_path + self._handler = '_' + handler.lower() + self._building = building + + def _archetype13(self): + """ + Enrich the city by using the sizing and simulation model developed for archetype13 of montreal_future_systems + """ + Archetype13(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 + :return: None + """ + getattr(self, self._handler, lambda: None)() diff --git a/scripts/system_simulation_models/archetype13.py b/scripts/system_simulation_models/archetype13.py new file mode 100644 index 00000000..75c04f4d --- /dev/null +++ b/scripts/system_simulation_models/archetype13.py @@ -0,0 +1,391 @@ +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 = [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 = [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 hvac_sizing(self): + storage_factor = 1.5 + 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(self._heating_peak_load) + heat_pump.nominal_cooling_output = round(self._cooling_peak_load) + boiler.nominal_heat_output = 0 + 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 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)) + if dhw_tes.volume == 0: + dhw_tes.volume = 1 + return dhw_hp, dhw_tes + + def heating_system_simulation(self): + hp, boiler, tes = self.hvac_sizing() + heat_efficiency = float(hp.heat_efficiency) + 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 = 7 + 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] / heat_efficiency + 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.hvac_sizing()[0] + 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] / cooling_efficiency + 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_sizing() + heat_efficiency = float(hp.heat_efficiency) + 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] / heat_efficiency + 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()))