From dbcab5bcb995a309842a331f54b6d5d2321693a2 Mon Sep 17 00:00:00 2001 From: p_monsalvete Date: Thu, 4 May 2023 10:39:23 -0400 Subject: [PATCH] energy systems factory test added and solved some bugs. Not working yet --- hub/city_model_structure/building.py | 117 ++++++++++++++++++ .../building_demand/surface.py | 49 +++++++- .../energy_systems/generic_energy_system.py | 4 +- .../generic_generation_system.py | 25 +++- hub/helpers/constants.py | 17 +++ ..._system_to_hub_energy_generation_system.py | 26 ++++ hub/helpers/dictionaries.py | 7 ++ ...ontreal_custom_energy_system_parameters.py | 15 ++- hub/unittests/test_systems_factory.py | 58 +++++++++ 9 files changed, 303 insertions(+), 15 deletions(-) create mode 100644 hub/helpers/data/montreal_system_to_hub_energy_generation_system.py create mode 100644 hub/unittests/test_systems_factory.py diff --git a/hub/city_model_structure/building.py b/hub/city_model_structure/building.py index 16aabf00..24243925 100644 --- a/hub/city_model_structure/building.py +++ b/hub/city_model_structure/building.py @@ -45,6 +45,12 @@ class Building(CityObject): self._lighting_electrical_demand = dict() self._appliances_electrical_demand = dict() self._domestic_hot_water_heat_demand = dict() + self._heating_consumption = dict() + self._cooling_consumption = dict() + self._lighting_electrical_consumption = dict() + self._appliances_electrical_consumption = dict() + self._domestic_hot_water_consumption = dict() + self._onsite_electrical_production = dict() self._eave_height = None self._energy_systems = None self._systems_archetype_name = None @@ -485,3 +491,114 @@ class Building(CityObject): :param value: str """ self._systems_archetype_name = value + + @property + def heating_consumption(self): + """ + Get energy consumption for heating according to the heating system installed + return: dict + """ + for heating_demand_key in self.heating: + demand = self.heating[heating_demand_key][0] + consumption_type = cte.HEATING + final_energy_consumed = self._calculate_consumption(consumption_type, demand) + self._heating_consumption[heating_demand_key] = final_energy_consumed + return self._heating_consumption + + @property + def cooling_consumption(self): + """ + Get energy consumption for cooling according to the cooling system installed + return: dict + """ + for cooling_demand_key in self.cooling: + demand = self.cooling[cooling_demand_key][0] + consumption_type = cte.COOLING + final_energy_consumed = self._calculate_consumption(consumption_type, demand) + self._cooling_consumption[cooling_demand_key] = final_energy_consumed + return self._cooling_consumption + + @property + def domestic_hot_water_consumption(self): + """ + Get energy consumption for domestic according to the domestic hot water system installed + return: dict + """ + for domestic_hot_water_demand_key in self.domestic_hot_water_heat_demand: + demand = self.domestic_hot_water_heat_demand[domestic_hot_water_demand_key][0] + consumption_type = cte.DOMESTIC_HOT_WATER + final_energy_consumed = self._calculate_consumption(consumption_type, demand) + self._domestic_hot_water_consumption[domestic_hot_water_demand_key] = final_energy_consumed + return self._domestic_hot_water_consumption + + @property + def lighting_electrical_consumption(self): + """ + Get energy consumption for lighting according to the electricity system installed + return: dict + """ + for lighting_demand_key in self.lighting_electrical_demand: + demand = self.lighting_electrical_demand[lighting_demand_key][0] + consumption_type = cte.ELECTRICITY + final_energy_consumed = self._calculate_consumption(consumption_type, demand) + self._lighting_electrical_consumption[lighting_demand_key] = final_energy_consumed + return self._lighting_electrical_consumption + + @property + def appliances_electrical_consumption(self): + """ + Get energy consumption for appliances according to the electricity system installed + return: dict + """ + for appliances_demand_key in self.appliances_electrical_demand: + demand = self.appliances_electrical_demand[appliances_demand_key][0] + consumption_type = cte.ELECTRICITY + final_energy_consumed = self._calculate_consumption(consumption_type, demand) + self._appliances_electrical_consumption[appliances_demand_key] = final_energy_consumed + return self._appliances_electrical_consumption + + def _calculate_consumption(self, consumption_type, demand): + # todo: modify when COP depends on the hour + coefficient_of_performance = 0 + for energy_system in self.energy_systems: + for demand_type in energy_system.demand_types: + if demand_type == consumption_type: + if consumption_type == cte.HEATING or consumption_type == cte.DOMESTIC_HOT_WATER: + coefficient_of_performance = energy_system.generation_system.heat_efficiency + elif consumption_type == cte.COOLING: + coefficient_of_performance = energy_system.generation_system.cooling_efficiency + elif consumption_type == cte.ELECTRICITY: + coefficient_of_performance = energy_system.generation_system.electricity_efficiency + final_energy_consumed = [] + if coefficient_of_performance == 0: + final_energy_consumed.append(0) + else: + for demand_value in demand: + final_energy_consumed.append(demand_value / coefficient_of_performance) + return final_energy_consumed + + @property + def onsite_electrical_production(self): + """ + Get total electricity produced onsite + return: dict + """ + # Add other systems whenever new ones appear + for energy_system in self.energy_systems: + if energy_system.generation_system.type == cte.PHOTOVOLTAIC: + _efficiency = energy_system.generation_system.electricity_efficiency + _temporal_cases = self.roofs[0].global_irradiance.keys() + self._onsite_electrical_production = {} + for _case in _temporal_cases: + _results = [] + for surface in self.surfaces: + for i, value in enumerate(surface.global_irradiance[_case]): + if len(_results) == 0: + _collector_production = value * _efficiency \ + * surface.perimeter_area * surface.solar_collectors_area_reduction_factor + else: + _collector_production = _results[i] * value * _efficiency \ + * surface.perimeter_area * surface.solar_collectors_area_reduction_factor + _results.append(_collector_production) + self._onsite_electrical_production[_case] = _results + return self._onsite_electrical_production diff --git a/hub/city_model_structure/building_demand/surface.py b/hub/city_model_structure/building_demand/surface.py index 56d73d1c..10e60788 100644 --- a/hub/city_model_structure/building_demand/surface.py +++ b/hub/city_model_structure/building_demand/surface.py @@ -7,6 +7,8 @@ Code contributors: Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concord """ from __future__ import annotations + +import math import uuid import numpy as np from typing import List, Union @@ -42,7 +44,7 @@ class Surface: self._associated_thermal_boundaries = [] self._vegetation = None self._percentage_shared = None - self._pv_coverage = None + self._solar_collectors_area_reduction_factor = None @property def name(self): @@ -135,7 +137,7 @@ class Surface: @property def azimuth(self): """ - Get surface azimuth in radians + Get surface azimuth in radians (north = 0) :return: float """ if self._azimuth is None: @@ -146,7 +148,7 @@ class Surface: @property def inclination(self): """ - Get surface inclination in radians + Get surface inclination in radians (zenith = 0, horizon = pi/2) :return: float """ if self._inclination is None: @@ -162,10 +164,12 @@ class Surface: :return: str """ if self._type is None: - grad = np.rad2deg(self.inclination) - if grad >= 170: + inclination_cos = math.cos(self.inclination) + # 170 degrees + if inclination_cos <= -0.98: self._type = 'Ground' - elif 80 <= grad <= 100: + # between 80 and 100 degrees + elif abs(inclination_cos) <= 0.17: self._type = 'Wall' else: self._type = 'Roof' @@ -347,3 +351,36 @@ class Surface: :param value: float """ self._percentage_shared = value + + @property + def solar_collectors_area_reduction_factor(self): + """ + Get factor area collector per surface area if set or calculate using Romero Rodríguez, L. et al (2017) model if not + :return: float + """ + if self._solar_collectors_area_reduction_factor is None: + _solar_collectors_area_reduction_factor = 0 + if self.type == cte.ROOF: + _protected_building_restriction = 1 + # 10 degrees range + if abs(math.sin(self.inclination)) < 0.17: + # horizontal + _construction_restriction = 0.8 + _separation_of_panels = 0.46 + _shadow_between_panels = 0.7 + else: + # pitched + _construction_restriction = 0.9 + _separation_of_panels = 0.9 + _shadow_between_panels = 1 + _solar_collectors_area_reduction_factor = _protected_building_restriction * _construction_restriction \ + * _separation_of_panels * _shadow_between_panels + return self._solar_collectors_area_reduction_factor + + @solar_collectors_area_reduction_factor.setter + def solar_collectors_area_reduction_factor(self, value): + """ + Set factor area collector per surface area + :param value: float + """ + self._solar_collectors_area_reduction_factor = value diff --git a/hub/city_model_structure/energy_systems/generic_energy_system.py b/hub/city_model_structure/energy_systems/generic_energy_system.py index 5fd2f1e5..45767ed7 100644 --- a/hub/city_model_structure/energy_systems/generic_energy_system.py +++ b/hub/city_model_structure/energy_systems/generic_energy_system.py @@ -25,7 +25,7 @@ class GenericEnergySystem: @property def demand_types(self): """ - Get demand able to cover from [heating, cooling, domestic_hot_water, electricity] + Get demand able to cover from [Heating, Cooling, Domestic Hot Water, Electricity] :return: [string] """ return self._demand_types @@ -33,7 +33,7 @@ class GenericEnergySystem: @demand_types.setter def demand_types(self, value): """ - Set demand able to cover from [heating, cooling, domestic_hot_water, electricity] + Set demand able to cover from [Heating, Cooling, Domestic Hot Water, Electricity] :param value: [string] """ self._demand_types = value diff --git a/hub/city_model_structure/energy_systems/generic_generation_system.py b/hub/city_model_structure/energy_systems/generic_generation_system.py index 72c5fb9e..1f31bae2 100644 --- a/hub/city_model_structure/energy_systems/generic_generation_system.py +++ b/hub/city_model_structure/energy_systems/generic_generation_system.py @@ -14,6 +14,7 @@ class GenericGenerationSystem: GenericGenerationSystem class """ def __init__(self): + self._type = None self._fuel_type = None self._heat_power = None self._cooling_power = None @@ -27,10 +28,26 @@ class GenericGenerationSystem: self._storage_capacity = None self._auxiliary_equipment = None + @property + def type(self): + """ + Get system type + :return: string + """ + return self._type + + @type.setter + def type(self, value): + """ + Set system type + :param value: string + """ + self._type = value + @property def fuel_type(self): """ - Get fuel_type from [renewable, gas, diesel, electricity, wood, coal] + Get fuel_type from [Renewable, Gas, Diesel, Electricity, Wood, Coal] :return: string """ return self._fuel_type @@ -38,7 +55,7 @@ class GenericGenerationSystem: @fuel_type.setter def fuel_type(self, value): """ - Set fuel_type from [renewable, gas, diesel, electricity, wood, coal] + Set fuel_type from [Renewable, Gas, Diesel, Electricity, Wood, Coal] :param value: string """ self._fuel_type = value @@ -46,7 +63,7 @@ class GenericGenerationSystem: @property def source_types(self): """ - Get source_type from [air, water, geothermal, district_heating, grid, on_site_electricity] + Get source_type from [Air, Water, Geothermal, District Heating, Grid, Onsite Electricity] :return: [string] """ return self._source_types @@ -54,7 +71,7 @@ class GenericGenerationSystem: @source_types.setter def source_types(self, value): """ - Set source_type from [air, water, geothermal, district_heating, grid, on_site_electricity] + Set source_type from [Air, Water, Geothermal, District Heating, Grid, Onsite Electricity] :param value: [string] """ self._source_types = value diff --git a/hub/helpers/constants.py b/hub/helpers/constants.py index b147e26d..43f55240 100644 --- a/hub/helpers/constants.py +++ b/hub/helpers/constants.py @@ -164,6 +164,23 @@ EQUIPMENT = 'Equipment' ACTIVITY = 'Activity' PEOPLE_ACTIVITY_LEVEL = 'People Activity Level' DOMESTIC_HOT_WATER = 'Domestic Hot Water' +HEATING = 'Heating' +COOLING = 'Cooling' +ELECTRICITY = 'Electricity' +RENEWABLE = 'Renewable' +WOOD = 'Wood' +GAS = 'Gas' +DIESEL = 'Diesel' +COAL = 'Coal' +AIR = 'Air' +WATER = 'Water' +GEOTHERMAL = 'Geothermal' +DISTRICT_HEATING_NETWORK = 'District Heating' +GRID = 'Grid' +ONSITE_ELECTRICITY = 'Onsite Electricity' +PHOTOVOLTAIC = 'Photovoltaic' +BOILER = 'Boiler' +HEAT_PUMP = 'Heat Pump' # Geometry EPSILON = 0.0000001 diff --git a/hub/helpers/data/montreal_system_to_hub_energy_generation_system.py b/hub/helpers/data/montreal_system_to_hub_energy_generation_system.py new file mode 100644 index 00000000..ba68bd47 --- /dev/null +++ b/hub/helpers/data/montreal_system_to_hub_energy_generation_system.py @@ -0,0 +1,26 @@ +""" +Dictionaries module for Montreal system to hub energy generation system +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2023 Concordia CERC group +Project Coder Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +""" + +import hub.helpers.constants as cte + + +class MontrealSystemToHubEnergyGenerationSystem: + + def __init__(self): + self._dictionary = {'Fuel-fired water boiler with baseboards': cte.BOILER, + 'Electrical resistance water boiler': cte.BOILER, + 'Fuel-fired furnace and fuel boiler for acs': cte.BOILER, + 'Baseboards: hydronic with fuel boiler': cte.BOILER, + 'Electrical baseboards and electrical boiler for acs': cte.BOILER, + 'Air cooled DX with external condenser': cte.HEAT_PUMP, + 'Water cooled, water chiller': cte.HEAT_PUMP, + 'PV system': cte.PHOTOVOLTAIC + } + + @property + def dictionary(self) -> dict: + return self._dictionary diff --git a/hub/helpers/dictionaries.py b/hub/helpers/dictionaries.py index 1fc73fde..a260e920 100644 --- a/hub/helpers/dictionaries.py +++ b/hub/helpers/dictionaries.py @@ -14,6 +14,7 @@ from hub.helpers.data.hub_function_to_nrcan_construction_function import HubFunc from hub.helpers.data.hub_usage_to_comnet_usage import HubUsageToComnetUsage from hub.helpers.data.hub_usage_to_hft_usage import HubUsageToHftUsage from hub.helpers.data.hub_usage_to_nrcan_usage import HubUsageToNrcanUsage +from hub.helpers.data.montreal_system_to_hub_energy_generation_system import MontrealSystemToHubEnergyGenerationSystem class Dictionaries: @@ -91,3 +92,9 @@ class Dictionaries: """ return AlkisFunctionToHubFunction().dictionary + @property + def montreal_system_to_hub_energy_generation_system(self): + """ + Get montreal custom system names to hub energy system names, transformation dictionary + """ + return MontrealSystemToHubEnergyGenerationSystem().dictionary diff --git a/hub/imports/energy_systems/montreal_custom_energy_system_parameters.py b/hub/imports/energy_systems/montreal_custom_energy_system_parameters.py index 3ce85e0c..4d5e25c6 100644 --- a/hub/imports/energy_systems/montreal_custom_energy_system_parameters.py +++ b/hub/imports/energy_systems/montreal_custom_energy_system_parameters.py @@ -12,6 +12,7 @@ from hub.catalog_factories.energy_systems_catalog_factory import EnergySystemsCa from hub.city_model_structure.energy_systems.generic_energy_system import GenericEnergySystem from hub.city_model_structure.energy_systems.generic_generation_system import GenericGenerationSystem from hub.city_model_structure.energy_systems.generic_distribution_system import GenericDistributionSystem +from hub.helpers.dictionaries import Dictionaries logger = get_logger() @@ -32,12 +33,17 @@ class MontrealCustomEnergySystemParameters: city = self._city montreal_custom_catalog = EnergySystemsCatalogFactory('montreal_custom').catalog for building in city.buildings: - archetype_name = building.energy_systems_archetype_name + archetype_name = f'{building.energy_systems_archetype_name}_lod1.0' +# archetype_name = building.energy_systems_archetype_name try: + print(archetype_name) archetype = self._search_archetypes(montreal_custom_catalog, archetype_name) except KeyError: - logger.error(f'Building {building.name} has unknown usage archetype for usage: {archetype_name}') - sys.stderr.write(f'Building {building.name} has unknown usage archetype for usage: {archetype_name}') + print('ERROR') + logger.error(f'Building {building.name} has unknown energy system archetype for system name: {archetype_name}') + sys.stderr.write(f'Building {building.name} has unknown energy system archetype ' + f'for system name: {archetype_name}') + print('END ERROR') continue building_systems = [] for equipment in archetype.equipments: @@ -45,6 +51,9 @@ class MontrealCustomEnergySystemParameters: energy_system.demand_types = equipment.demand_types _generation_system = GenericGenerationSystem() archetype_generation_equipment = equipment.generation_system + _type = str(equipment.name).split('_')[0] + _generation_system.type = Dictionaries().montreal_system_to_hub_energy_generation_system[ + _type] # dhw peak does not add anything to the total heat peak _generation_system.heat_power = building.heating_peak_load _generation_system.cooling_power = building.cooling_peak_load diff --git a/hub/unittests/test_systems_factory.py b/hub/unittests/test_systems_factory.py new file mode 100644 index 00000000..80c1f168 --- /dev/null +++ b/hub/unittests/test_systems_factory.py @@ -0,0 +1,58 @@ +""" +TestSystemsFactory +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2023 Concordia CERC group +Project Coder Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +""" + +from pathlib import Path +from unittest import TestCase + +from hub.imports.geometry_factory import GeometryFactory +from hub.imports.construction_factory import ConstructionFactory +from hub.imports.usage_factory import UsageFactory +from hub.imports.energy_systems_factory import EnergySystemsFactory + + +class TestSystemsFactory(TestCase): + """ + TestSystemsFactory TestCase + """ + def setUp(self) -> None: + """ + Configure test environment + :return: + """ + self._city = None + self._example_path = (Path(__file__).parent / 'tests_data').resolve() + + def _get_citygml(self, file): + file_path = (self._example_path / file).resolve() + self._city = GeometryFactory('citygml', path=file_path).city + self.assertIsNotNone(self._city, 'city is none') + self.assertIsNotNone(self._city.level_of_detail.geometry, 'wrong construction level of detail') + return self._city + + def test_montreal_custom_system_factory(self): + """ + Enrich the city with the construction information and verify it + """ + file = 'one_building_in_kelowna.gml' + city = self._get_citygml(file) + for building in city.buildings: + building.energy_systems_archetype_name = 'system 1 gas' + + EnergySystemsFactory('montreal_custom', city).enrich() + for building in city.buildings: + print(building.energy_systems) + + def test_montreal_custom_system_results(self): + """ + Enrich the city with the construction information and verify it + """ + file = 'one_building_in_kelowna.gml' + city = self._get_citygml(file) + for building in city.buildings: + building.year_of_construction = 1980 + ConstructionFactory('nrcan', city).enrich() + UsageFactory('nrcan', city).enrich()