diff --git a/hub/catalog_factories/cost/montreal_custom_catalog.py b/hub/catalog_factories/cost/montreal_custom_catalog.py index 5df24d9f..894695cf 100644 --- a/hub/catalog_factories/cost/montreal_custom_catalog.py +++ b/hub/catalog_factories/cost/montreal_custom_catalog.py @@ -20,7 +20,7 @@ from hub.catalog_factories.data_models.cost.cost_helper import CostHelper class MontrealCustomCatalog(Catalog): def __init__(self, path): - path = 'C:/Users/JGAVALDA/PycharmProjects/hub/hub/data/costs/montreal_costs.xml' + path = (path / 'montreal_costs.xml') with open(path) as xml: self._archetypes = xmltodict.parse(xml.read(), force_list='archetype') diff --git a/hub/catalog_factories/costs_catalog_factory.py b/hub/catalog_factories/costs_catalog_factory.py index d628baee..51bafe17 100644 --- a/hub/catalog_factories/costs_catalog_factory.py +++ b/hub/catalog_factories/costs_catalog_factory.py @@ -19,7 +19,7 @@ class CostCatalogFactory: """ def __init__(self, file_type, base_path=None): if base_path is None: - base_path = 'C:/Users/JGAVALDA/PycharmProjects/hub/hub/data/costs' + base_path = Path(Path(__file__).parent.parent / 'data/costs') self._catalog_type = '_' + file_type.lower() self._path = base_path diff --git a/hub/catalog_factories/data_models/cost/archetype.py b/hub/catalog_factories/data_models/cost/archetype.py index cf24c6ee..97cabe1a 100644 --- a/hub/catalog_factories/data_models/cost/archetype.py +++ b/hub/catalog_factories/data_models/cost/archetype.py @@ -38,7 +38,7 @@ class Archetype: Get name :return: string """ - return f'{self._country}_{self._municipality}_{self._function}_{self._lod}' + return f'{self._country}_{self._municipality}_{self._function}_lod{self._lod}' @property def lod(self): diff --git a/hub/catalog_factories/data_models/cost/capital_cost.py b/hub/catalog_factories/data_models/cost/capital_cost.py index 828b5cfc..94981e42 100644 --- a/hub/catalog_factories/data_models/cost/capital_cost.py +++ b/hub/catalog_factories/data_models/cost/capital_cost.py @@ -38,3 +38,13 @@ class CapitalCost: :return: float """ return self._overhead_and_profit + + def chapter(self, name) -> Chapter: + """ + Get specific chapter by name + :return: Chapter + """ + for chapter in self.general_chapters: + if chapter.chapter_type == name: + return chapter + raise KeyError(f'Chapter name {name} not found') diff --git a/hub/catalog_factories/data_models/cost/chapter.py b/hub/catalog_factories/data_models/cost/chapter.py index bf393cbb..2cd7b4e6 100644 --- a/hub/catalog_factories/data_models/cost/chapter.py +++ b/hub/catalog_factories/data_models/cost/chapter.py @@ -30,3 +30,13 @@ class Chapter: :return: [str] """ return self._items + + def item(self, name) -> ItemDescription: + """ + Get specific item by name + :return: ItemDescription + """ + for item in self.items: + if item.type == name: + return item + raise KeyError(f'Item name {name} not found') diff --git a/hub/city_model_structure/building.py b/hub/city_model_structure/building.py index afcdb85e..97582a7c 100644 --- a/hub/city_model_structure/building.py +++ b/hub/city_model_structure/building.py @@ -9,8 +9,11 @@ Code contributors: Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concord import sys from typing import List, Union import numpy as np +import pandas as pd + from hub.hub_logger import logger import hub.helpers.constants as cte +import hub.helpers.peak_loads as pl from hub.city_model_structure.building_demand.surface import Surface from hub.city_model_structure.city_object import CityObject from hub.city_model_structure.building_demand.household import Household @@ -370,15 +373,14 @@ class Building(CityObject): Get heating peak load in W :return: dict{DataFrame(float)} """ - return self._heating_peak_load - - @heating_peak_load.setter - def heating_peak_load(self, value): - """ - Set heating peak load in W - :param value: dict{DataFrame(float)} - """ - self._heating_peak_load = value + results = {} + if cte.HOUR in self.heating: + monthly_values = pl.peak_loads_from_hourly(self.heating[cte.HOUR][next(iter(self.heating[cte.HOUR]))].values) + else: + monthly_values = pl.heating_peak_loads_from_methodology(self) + results[cte.MONTH] = pd.DataFrame(monthly_values, columns=['heating peak loads']) + results[cte.YEAR] = pd.DataFrame([max(monthly_values)], columns=['heating peak loads']) + return results @property def cooling_peak_load(self) -> dict: @@ -386,15 +388,14 @@ class Building(CityObject): Get cooling peak load in W :return: dict{DataFrame(float)} """ - return self._cooling_peak_load - - @cooling_peak_load.setter - def cooling_peak_load(self, value): - """ - Set peak load in W - :param value: dict{DataFrame(float)} - """ - self._cooling_peak_load = value + results = {} + if cte.HOUR in self.cooling: + monthly_values = pl.peak_loads_from_hourly(self.cooling[cte.HOUR][next(iter(self.cooling[cte.HOUR]))]) + else: + monthly_values = pl.cooling_peak_loads_from_methodology(self) + results[cte.MONTH] = pd.DataFrame(monthly_values, columns=['cooling peak loads']) + results[cte.YEAR] = pd.DataFrame([max(monthly_values)], columns=['cooling peak loads']) + return results @property def eave_height(self): diff --git a/hub/city_model_structure/building_demand/internal_zone.py b/hub/city_model_structure/building_demand/internal_zone.py index 802c7e20..c8669652 100644 --- a/hub/city_model_structure/building_demand/internal_zone.py +++ b/hub/city_model_structure/building_demand/internal_zone.py @@ -78,7 +78,7 @@ class InternalZone: def usages(self) -> [Usage]: """ Get internal zone usage zones - :return: [UsageZone] + :return: [Usage] """ return self._usages @@ -86,7 +86,7 @@ class InternalZone: def usages(self, value): """ Set internal zone usage zones - :param value: [UsageZone] + :param value: [Usage] """ self._usages = value diff --git a/hub/city_model_structure/city.py b/hub/city_model_structure/city.py index 987a47e1..e8a34595 100644 --- a/hub/city_model_structure/city.py +++ b/hub/city_model_structure/city.py @@ -122,6 +122,8 @@ class City: Get the name for the climatic information reference city :return: None or str """ + if self._climate_reference_city is None: + self._climate_reference_city = self._get_location().city return self._climate_reference_city @climate_reference_city.setter @@ -130,8 +132,7 @@ class City: Set the name for the climatic information reference city :param value: str """ - if value is not None: - self._climate_reference_city = str(value) + self._climate_reference_city = str(value) @property def climate_file(self) -> Union[None, Path]: diff --git a/hub/exports/building_energy/insel/insel_monthly_energy_balance.py b/hub/exports/building_energy/insel/insel_monthly_energy_balance.py index caf2e0a6..9c660af0 100644 --- a/hub/exports/building_energy/insel/insel_monthly_energy_balance.py +++ b/hub/exports/building_energy/insel/insel_monthly_energy_balance.py @@ -51,6 +51,7 @@ class InselMonthlyEnergyBalance(Insel): ) self._export() + def _export(self): for i_file, content in enumerate(self._contents): file_name = self._insel_files_paths[i_file] @@ -94,12 +95,19 @@ class InselMonthlyEnergyBalance(Insel): inputs.append(f"{str(100 + i)}.1 % Radiation surface {str(i)}") number_of_storeys = int(building.eave_height / building.average_storey_height) + attic_heated = building.attic_heated + basement_heated = building.basement_heated + if building.attic_heated is None: + attic_heated = 0 + if building.basement_heated is None: + basement_heated = 0 + # BUILDING PARAMETERS parameters = [f'{building.volume} % BP(1) Heated Volume (m3)', f'{building.average_storey_height} % BP(2) Average storey height (m)', f'{number_of_storeys} % BP(3) Number of storeys above ground', - f'{building.attic_heated} % BP(4) Attic heating type (0=no room, 1=unheated, 2=heated)', - f'{building.basement_heated} % BP(5) Cellar heating type (0=no room, 1=unheated, 2=heated, ' + f'{attic_heated} % BP(4) Attic heating type (0=no room, 1=unheated, 2=heated)', + f'{basement_heated} % BP(5) Cellar heating type (0=no room, 1=unheated, 2=heated, ' f'99=invalid)'] # todo: this method and the insel model have to be reviewed for more than one internal zone diff --git a/hub/exports/energy_building_exports_factory.py b/hub/exports/energy_building_exports_factory.py index 0b39ebd0..46e233ea 100644 --- a/hub/exports/energy_building_exports_factory.py +++ b/hub/exports/energy_building_exports_factory.py @@ -55,9 +55,7 @@ class EnergyBuildingsExportsFactory: """ idf_data_path = (Path(__file__).parent / './building_energy/idf_files/').resolve() # todo: create a get epw file function based on the city - #print('path', idf_data_path) weather_path = (Path(__file__).parent / '../data/weather/epw/CAN_PQ_Montreal.Intl.AP.716270_CWEC.epw').resolve() - #print(weather_path) return Idf(self._city, self._path, (idf_data_path / 'Minimal.idf'), (idf_data_path / 'Energy+.idd'), weather_path, target_buildings=self._target_buildings, adjacent_buildings=self._adjacent_buildings) @@ -74,7 +72,6 @@ class EnergyBuildingsExportsFactory: Export the city given to the class using the given export type handler :return: None """ - print(self) return getattr(self, self._export_type, lambda: None) def export_debug(self): diff --git a/hub/exports/exports_factory.py b/hub/exports/exports_factory.py index ae99dd0d..83fde861 100644 --- a/hub/exports/exports_factory.py +++ b/hub/exports/exports_factory.py @@ -65,14 +65,6 @@ class ExportsFactory: """ return Obj(self._city, self._path) - @property - def _grounded_obj(self): - """ - Export the city geometry to obj with grounded coordinates - :return: None - """ - return Obj(self._city, self._path) - @property def _sra(self): """ diff --git a/hub/exports/formats/simplified_radiosity_algorithm.py b/hub/exports/formats/simplified_radiosity_algorithm.py index e2fa2ab9..03896901 100644 --- a/hub/exports/formats/simplified_radiosity_algorithm.py +++ b/hub/exports/formats/simplified_radiosity_algorithm.py @@ -4,6 +4,8 @@ SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2022 Concordia CERC group Project Coder Guillermo.GutierrezMorote@concordia.ca """ +from pathlib import Path + import xmltodict from hub.imports.weather_factory import WeatherFactory @@ -32,9 +34,12 @@ class SimplifiedRadiosityAlgorithm: self._end_month = end_month self._end_day = end_day self._city = city + self._city.climate_file = str((Path(file_name).parent / f'{city.name}.cli').resolve()) + self._city.climate_reference_city = city.location self._target_buildings = target_buildings self._weather_format = weather_format self._weather_file = weather_file + self._export() def _correct_point(self, point): @@ -45,8 +50,8 @@ class SimplifiedRadiosityAlgorithm: return [x, y, z] def _export(self): - self._export_sra_xml() self._export_sra_cli() + self._export_sra_xml() def _export_sra_cli(self): file = self._city.climate_file diff --git a/hub/helpers/data/hub_function_to_montreal_custom_costs_function.py b/hub/helpers/data/hub_function_to_montreal_custom_costs_function.py new file mode 100644 index 00000000..5e0d58f5 --- /dev/null +++ b/hub/helpers/data/hub_function_to_montreal_custom_costs_function.py @@ -0,0 +1,78 @@ +""" +Dictionaries module for hub function to Montreal custom costs function +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2022 Concordia CERC group +Project Coder Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +""" + +import hub.helpers.constants as cte + + +class HubFunctionToMontrealCustomCostsFunction: + + def __init__(self): + self._dictionary = { + cte.RESIDENTIAL: 'residential', + cte.SINGLE_FAMILY_HOUSE: 'residential', + cte.MULTI_FAMILY_HOUSE: 'residential', + cte.ROW_HOUSE: 'residential', + cte.MID_RISE_APARTMENT: 'residential', + cte.HIGH_RISE_APARTMENT: 'residential', + cte.OFFICE_AND_ADMINISTRATION: 'non-residential', + cte.SMALL_OFFICE: 'non-residential', + cte.MEDIUM_OFFICE: 'non-residential', + cte.LARGE_OFFICE: 'non-residential', + cte.COURTHOUSE: 'non-residential', + cte.FIRE_STATION: 'non-residential', + cte.PENITENTIARY: 'non-residential', + cte.POLICE_STATION: 'non-residential', + cte.POST_OFFICE: 'non-residential', + cte.LIBRARY: 'non-residential', + cte.EDUCATION: 'non-residential', + cte.PRIMARY_SCHOOL: 'non-residential', + cte.PRIMARY_SCHOOL_WITH_SHOWER: 'non-residential', + cte.SECONDARY_SCHOOL: 'non-residential', + cte.UNIVERSITY: 'non-residential', + cte.LABORATORY_AND_RESEARCH_CENTER: 'non-residential', + cte.STAND_ALONE_RETAIL: 'non-residential', + cte.HOSPITAL: 'non-residential', + cte.OUT_PATIENT_HEALTH_CARE: 'non-residential', + cte.HEALTH_CARE: 'non-residential', + cte.RETIREMENT_HOME_OR_ORPHANAGE: 'non-residential', + cte.COMMERCIAL: 'non-residential', + cte.STRIP_MALL: 'non-residential', + cte.SUPERMARKET: 'non-residential', + cte.RETAIL_SHOP_WITHOUT_REFRIGERATED_FOOD: 'non-residential', + cte.RETAIL_SHOP_WITH_REFRIGERATED_FOOD: 'non-residential', + cte.RESTAURANT: 'full service restaurant', + cte.QUICK_SERVICE_RESTAURANT: 'non-residential', + cte.FULL_SERVICE_RESTAURANT: 'non-residential', + cte.HOTEL: 'non-residential', + cte.HOTEL_MEDIUM_CLASS: 'non-residential', + cte.SMALL_HOTEL: 'non-residential', + cte.LARGE_HOTEL: 'non-residential', + cte.DORMITORY: 'non-residential', + cte.EVENT_LOCATION: 'non-residential', + cte.CONVENTION_CENTER: 'non-residential', + cte.HALL: 'non-residential', + cte.GREEN_HOUSE: 'non-residential', + cte.INDUSTRY: 'non-residential', + cte.WORKSHOP: 'non-residential', + cte.WAREHOUSE: 'non-residential', + cte.WAREHOUSE_REFRIGERATED: 'non-residential', + cte.SPORTS_LOCATION: 'non-residential', + cte.SPORTS_ARENA: 'non-residential', + cte.GYMNASIUM: 'non-residential', + cte.MOTION_PICTURE_THEATRE: 'non-residential', + cte.MUSEUM: 'non-residential', + cte.PERFORMING_ARTS_THEATRE: 'non-residential', + cte.TRANSPORTATION: 'non-residential', + cte.AUTOMOTIVE_FACILITY: 'non-residential', + cte.PARKING_GARAGE: 'non-residential', + cte.RELIGIOUS: 'non-residential', + cte.NON_HEATED: 'non-residential' + } + + @property + def dictionary(self) -> dict: + return self._dictionary diff --git a/hub/helpers/data/hub_function_to_nrel_construction_function.py b/hub/helpers/data/hub_function_to_nrel_construction_function.py index 2c0a2906..0d9aedd8 100644 --- a/hub/helpers/data/hub_function_to_nrel_construction_function.py +++ b/hub/helpers/data/hub_function_to_nrel_construction_function.py @@ -67,7 +67,7 @@ class HubFunctionToNrelConstructionFunction: cte.MUSEUM: 'n/a', cte.PERFORMING_ARTS_THEATRE: 'n/a', cte.TRANSPORTATION: 'n/a', - cte.AUTOMOTIVE_FACILITY: 'n/aquebec_to_hub', + cte.AUTOMOTIVE_FACILITY: 'n/a', cte.PARKING_GARAGE: 'n/a', cte.RELIGIOUS: 'n/a', cte.NON_HEATED: 'n/a' diff --git a/hub/helpers/dictionaries.py b/hub/helpers/dictionaries.py index 1fc73fde..3b5a9970 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.hub_function_to_montreal_custom_costs_function import HubFunctionToMontrealCustomCostsFunction class Dictionaries: @@ -91,3 +92,11 @@ class Dictionaries: """ return AlkisFunctionToHubFunction().dictionary + @property + def hub_function_to_montreal_custom_costs_function(self) -> dict: + """ + Get hub function to Montreal custom costs function, transformation dictionary + :return: dict + """ + return HubFunctionToMontrealCustomCostsFunction().dictionary + diff --git a/hub/helpers/geometry_helper.py b/hub/helpers/geometry_helper.py index b4e20351..e9b9a6d9 100644 --- a/hub/helpers/geometry_helper.py +++ b/hub/helpers/geometry_helper.py @@ -100,9 +100,9 @@ class GeometryHelper: j = 0 next_coordinate = ground.perimeter_polygon.coordinates[j] distance = GeometryHelper.distance_between_points(coordinate, next_coordinate) - if distance == 0: - continue steps = int(distance * factor * 2) + if steps == 0: + continue delta_x = (next_coordinate[0] - coordinate[0]) / steps delta_y = (next_coordinate[1] - coordinate[1]) / steps diff --git a/hub/imports/results/peak_calculation/__init__.py b/hub/helpers/peak_calculation/__init__.py similarity index 100% rename from hub/imports/results/peak_calculation/__init__.py rename to hub/helpers/peak_calculation/__init__.py diff --git a/hub/imports/results/peak_calculation/loads_calculation.py b/hub/helpers/peak_calculation/loads_calculation.py similarity index 100% rename from hub/imports/results/peak_calculation/loads_calculation.py rename to hub/helpers/peak_calculation/loads_calculation.py diff --git a/hub/helpers/peak_loads.py b/hub/helpers/peak_loads.py new file mode 100644 index 00000000..11bfc208 --- /dev/null +++ b/hub/helpers/peak_loads.py @@ -0,0 +1,71 @@ +import math + +import hub.helpers.constants as cte +from hub.helpers.peak_calculation.loads_calculation import LoadsCalculation + +_MONTH_STARTING_HOUR = [0, 744, 1416, 2160, 2880, 3624, 4344, 5088, 5832, 6552, 7296, 8016, math.inf] + +def peak_loads_from_hourly(hourly_values): + month = 1 + peaks = [0 for _ in range(12)] + for i, value in enumerate(hourly_values): + if _MONTH_STARTING_HOUR[month] <= i: + month += 1 + if value > peaks[month-1]: + peaks[month-1] = value + return peaks + +def heating_peak_loads_from_methodology(building): + monthly_heating_loads = [] + ambient_temperature = building.external_temperature[cte.HOUR]['epw'] + for month in range(0, 12): + ground_temperature = building.ground_temperature[cte.MONTH]['2'][month] + heating_ambient_temperature = 100 + start_hour = _MONTH_STARTING_HOUR[month] + end_hour = 8760 + if month < 11: + end_hour = _MONTH_STARTING_HOUR[month + 1] + for hour in range(start_hour, end_hour): + temperature = ambient_temperature[hour] + if temperature < heating_ambient_temperature: + heating_ambient_temperature = temperature + loads = LoadsCalculation(building) + heating_load_transmitted = loads.get_heating_transmitted_load(heating_ambient_temperature, ground_temperature) + heating_load_ventilation_sensible = loads.get_heating_ventilation_load_sensible(heating_ambient_temperature) + heating_load_ventilation_latent = 0 + heating_load = heating_load_transmitted + heating_load_ventilation_sensible + heating_load_ventilation_latent + if heating_load < 0: + heating_load = 0 + monthly_heating_loads.append(heating_load) + return monthly_heating_loads + +def cooling_peak_loads_from_methodology(building): + monthly_cooling_loads = [] + ambient_temperature = building.external_temperature[cte.HOUR]['epw'] + for month in range(0, 12): + ground_temperature = building.ground_temperature[cte.MONTH]['2'][month] + cooling_ambient_temperature = -100 + cooling_calculation_hour = -1 + start_hour = _MONTH_STARTING_HOUR[month] + end_hour = 8760 + if month < 11: + end_hour = _MONTH_STARTING_HOUR[month + 1] + for hour in range(start_hour, end_hour): + temperature = ambient_temperature[hour] + if temperature > cooling_ambient_temperature: + cooling_ambient_temperature = temperature + cooling_calculation_hour = hour + loads = LoadsCalculation(building) + cooling_load_transmitted = loads.get_cooling_transmitted_load(cooling_ambient_temperature, ground_temperature) + cooling_load_renovation_sensible = loads.get_cooling_ventilation_load_sensible(cooling_ambient_temperature) + cooling_load_internal_gains_sensible = loads.get_internal_load_sensible() + cooling_load_radiation = loads.get_radiation_load('sra', cooling_calculation_hour) + cooling_load_sensible = cooling_load_transmitted + cooling_load_renovation_sensible - cooling_load_radiation \ + - cooling_load_internal_gains_sensible + + cooling_load_latent = 0 + cooling_load = cooling_load_sensible + cooling_load_latent + if cooling_load > 0: + cooling_load = 0 + monthly_cooling_loads.append(abs(cooling_load)) + return monthly_cooling_loads diff --git a/hub/imports/geometry/geojson.py b/hub/imports/geometry/geojson.py index 4fd9d340..6c238e28 100644 --- a/hub/imports/geometry/geojson.py +++ b/hub/imports/geometry/geojson.py @@ -61,66 +61,51 @@ class Geojson: self._min_y = y @staticmethod - def _create_buildings_lod0(name, year_of_construction, function, surfaces_coordinates): - surfaces = [] - buildings = [] - for zone, surface_coordinates in enumerate(surfaces_coordinates): - points = igh.points_from_string(igh.remove_last_point_from_string(surface_coordinates)) - # geojson provides the roofs, need to be transform into grounds - points = igh.invert_points(points) - polygon = Polygon(points) - polygon.area = igh.ground_area(points) - surface = Surface(polygon, polygon) - if len(buildings) == 1: - buildings[0].surfaces.append(surface) - else: - surfaces.append(surface) - buildings.append(Building(f'{name}', surfaces, year_of_construction, function)) - return buildings + def _create_building_lod0(name, year_of_construction, function, surface_coordinates): + points = igh.points_from_string(igh.remove_last_point_from_string(surface_coordinates)) + points = igh.invert_points(points) + polygon = Polygon(points) + polygon.area = igh.ground_area(points) + surface = Surface(polygon, polygon, name=f'{name}_ground') + return Building(f'{name}', [surface], year_of_construction, function) @staticmethod - def _create_buildings_lod1(name, year_of_construction, function, height, surface_coordinates): - lod0_buildings = Geojson._create_buildings_lod0(name, year_of_construction, function, surface_coordinates) + def _create_building_lod1(name, year_of_construction, function, height, surface_coordinates): + building = Geojson._create_building_lod0(name, year_of_construction, function, surface_coordinates) surfaces = [] - buildings = [] - - for zone, lod0_building in enumerate(lod0_buildings): - # print(zone, lod0_building.name) - volume = 0 - for surface in lod0_building.grounds: - volume = volume + surface.solid_polygon.area * height - surfaces.append(surface) - roof_coordinates = [] - # adding a roof means invert the polygon coordinates and change the Z value - for coordinate in surface.solid_polygon.coordinates: - roof_coordinate = np.array([coordinate[0], coordinate[1], height]) - # insert the roof rotated already - roof_coordinates.insert(0, roof_coordinate) - polygon = Polygon(roof_coordinates) - polygon.area = surface.solid_polygon.area - roof = Surface(polygon, polygon) - surfaces.append(roof) - # adding a wall means add the point coordinates and the next point coordinates with Z's height and 0 - coordinates_length = len(roof.solid_polygon.coordinates) - for i, coordinate in enumerate(roof.solid_polygon.coordinates): - j = i + 1 - if j == coordinates_length: - j = 0 - next_coordinate = roof.solid_polygon.coordinates[j] - wall_coordinates = [ - np.array([coordinate[0], coordinate[1], 0.0]), - np.array([next_coordinate[0], next_coordinate[1], 0.0]), - np.array([next_coordinate[0], next_coordinate[1], next_coordinate[2]]), - np.array([coordinate[0], coordinate[1], coordinate[2]]) - ] - polygon = Polygon(wall_coordinates) - wall = Surface(polygon, polygon) - surfaces.append(wall) + volume = 0 + for ground in building.grounds: + volume += ground.solid_polygon.area * height + surfaces.append(ground) + roof_coordinates = [] + # adding a roof means invert the polygon coordinates and change the Z value + for coordinate in ground.solid_polygon.coordinates: + roof_coordinate = np.array([coordinate[0], coordinate[1], height]) + # insert the roof rotated already + roof_coordinates.insert(0, roof_coordinate) + roof_polygon = Polygon(roof_coordinates) + roof_polygon.area = ground.solid_polygon.area + roof = Surface(roof_polygon, roof_polygon) + surfaces.append(roof) + # adding a wall means add the point coordinates and the next point coordinates with Z's height and 0 + coordinates_length = len(roof.solid_polygon.coordinates) + for i, coordinate in enumerate(roof.solid_polygon.coordinates): + j = i + 1 + if j == coordinates_length: + j = 0 + next_coordinate = roof.solid_polygon.coordinates[j] + wall_coordinates = [ + np.array([coordinate[0], coordinate[1], 0.0]), + np.array([next_coordinate[0], next_coordinate[1], 0.0]), + np.array([next_coordinate[0], next_coordinate[1], next_coordinate[2]]), + np.array([coordinate[0], coordinate[1], coordinate[2]]) + ] + polygon = Polygon(wall_coordinates) + wall = Surface(polygon, polygon) + surfaces.append(wall) building = Building(f'{name}', surfaces, year_of_construction, function) building.volume = volume - buildings.append(building) - - return buildings + return building def _get_polygons(self, polygons, coordinates): if type(coordinates[0][self.X]) != float: @@ -173,8 +158,6 @@ class Geojson: GeometryHelper.distance_between_points(neighbour_line[0], neighbour_line[1]) - GeometryHelper.distance_between_points(line[1], neighbour_line[0]) - GeometryHelper.distance_between_points(line[0], neighbour_line[1])) / 2 - print(line_shared) - print() percentage_ground = line_shared / GeometryHelper.distance_between_points(line[0], line[1]) percentage_height = neighbour_height / building.max_height if percentage_height > 1: @@ -188,14 +171,13 @@ class Geojson: Get city out of a Geojson file """ if self._city is None: - missing_functions = [] buildings = [] - building_id = 0 - lod = 1 + lod = 0 for feature in self._geojson['features']: extrusion_height = 0 if self._extrusion_height_field is not None: extrusion_height = float(feature['properties'][self._extrusion_height_field]) + lod = 0.5 year_of_construction = None if self._year_of_construction_field is not None: year_of_construction = int(feature['properties'][self._year_of_construction_field]) @@ -206,57 +188,111 @@ class Geojson: # use the transformation dictionary to retrieve the proper function if function in self._function_to_hub: function = self._function_to_hub[function] - else: - if function not in missing_functions: - missing_functions.append(function) - function = function geometry = feature['geometry'] if 'id' in feature: building_name = feature['id'] - else: - building_name = f'building_{building_id}' - building_id += 1 if self._name_field is not None: building_name = feature['properties'][self._name_field] - polygons = [] - for part, coordinates in enumerate(geometry['coordinates']): - polygons = self._get_polygons(polygons, coordinates) - for polygon in polygons: - if extrusion_height == 0: - buildings = buildings + Geojson._create_buildings_lod0(f'{building_name}', - year_of_construction, - function, - [polygon]) - lod = 0 - else: - if self._max_z < extrusion_height: - self._max_z = extrusion_height - if part == 0: - buildings = buildings + Geojson._create_buildings_lod1(f'{building_name}', - year_of_construction, - function, - extrusion_height, - [polygon]) - else: - new_part = Geojson._create_buildings_lod1(f'{building_name}', - year_of_construction, - function, - extrusion_height, - [polygon]) - surfaces = buildings[len(buildings) - 1].surfaces + new_part[0].surfaces - volume = buildings[len(buildings) - 1].volume + new_part[0].volume - buildings[len(buildings) - 1] = Building(f'{building_name}', surfaces, year_of_construction, function) - buildings[len(buildings) - 1].volume = volume + if str(geometry['type']).lower() == 'polygon': + buildings.append(self._parse_polygon(geometry['coordinates'], + building_name, + function, + year_of_construction, + extrusion_height)) + + elif str(geometry['type']).lower() == 'multipolygon': + buildings.append(self._parse_multi_polygon(geometry['coordinates'], + building_name, + function, + year_of_construction, + extrusion_height)) + else: + raise NotImplementedError(f'Geojson geometry type [{geometry["type"]}] unknown') self._city = City([self._min_x, self._min_y, 0.0], [self._max_x, self._max_y, self._max_z], 'epsg:26911') for building in buildings: # Do not include "small building-like structures" to buildings if building.floor_area >= 25: self._city.add_city_object(building) self._city.level_of_detail.geometry = lod - if lod == 1: + if lod > 0: lines_information = GeometryHelper.city_mapping(self._city, plot=False) self._store_shared_percentage_to_walls(self._city, lines_information) - if len(missing_functions) > 0: - print(f'There are unknown functions {missing_functions}') + return self._city + + def _polygon_coordinates_to_3d(self, polygon_coordinates): + transformed_coordinates = '' + for coordinate in polygon_coordinates: + transformed = self._transformer.transform(coordinate[self.Y], coordinate[self.X]) + self._save_bounds(transformed[self.X], transformed[self.Y]) + transformed_coordinates = f'{transformed_coordinates} {transformed[self.X]} {transformed[self.Y]} 0.0' + return transformed_coordinates.lstrip(' ') + + def _parse_polygon(self, coordinates, building_name, function, year_of_construction, extrusion_height): + print('poly') + for polygon_coordinates in coordinates: + coordinates_3d = self._polygon_coordinates_to_3d(polygon_coordinates) + if extrusion_height == 0: + building = Geojson._create_building_lod0(f'{building_name}', + year_of_construction, + function, + coordinates_3d) + else: + if self._max_z < extrusion_height: + self._max_z = extrusion_height + building = Geojson._create_building_lod1(f'{building_name}', + year_of_construction, + function, + extrusion_height, + coordinates_3d) + return building + + def _parse_multi_polygon(self, coordinates, building_name, function, year_of_construction, extrusion_height): + print('multi') + surfaces = [] + for polygon_coordinate in coordinates: + building = self._parse_polygon(polygon_coordinate, building_name, function, year_of_construction, 0) + for surface in building.surfaces: + if surface.type == cte.GROUND: + surfaces.append(surface) + else: + # overwrite last surface by adding the "hole" in the polygon + polygon = Polygon(surfaces[-1].solid_polygon.coordinates + surface.solid_polygon.coordinates) + surfaces[-1] = Surface(polygon, polygon) + if extrusion_height == 0: + return Building(building_name, surfaces, year_of_construction, function) + else: + volume = 0 + for ground in building.grounds: + volume += ground.solid_polygon.area * extrusion_height + surfaces.append(ground) + roof_coordinates = [] + # adding a roof means invert the polygon coordinates and change the Z value + for coordinate in ground.solid_polygon.coordinates: + roof_coordinate = np.array([coordinate[0], coordinate[1], extrusion_height]) + # insert the roof rotated already + roof_coordinates.insert(0, roof_coordinate) + roof_polygon = Polygon(roof_coordinates) + roof_polygon.area = ground.solid_polygon.area + roof = Surface(roof_polygon, roof_polygon) + surfaces.append(roof) + # adding a wall means add the point coordinates and the next point coordinates with Z's height and 0 + coordinates_length = len(roof.solid_polygon.coordinates) + for i, coordinate in enumerate(roof.solid_polygon.coordinates): + j = i + 1 + if j == coordinates_length: + j = 0 + next_coordinate = roof.solid_polygon.coordinates[j] + wall_coordinates = [ + np.array([coordinate[0], coordinate[1], 0.0]), + np.array([next_coordinate[0], next_coordinate[1], 0.0]), + np.array([next_coordinate[0], next_coordinate[1], next_coordinate[2]]), + np.array([coordinate[0], coordinate[1], coordinate[2]]) + ] + polygon = Polygon(wall_coordinates) + wall = Surface(polygon, polygon) + surfaces.append(wall) + building = Building(f'{building_name}', surfaces, year_of_construction, function) + building.volume = volume + return building diff --git a/hub/imports/results/insel_monthly_energry_balance.py b/hub/imports/results/insel_monthly_energry_balance.py index 15f2882b..a2b5b322 100644 --- a/hub/imports/results/insel_monthly_energry_balance.py +++ b/hub/imports/results/insel_monthly_energry_balance.py @@ -12,7 +12,7 @@ import hub.helpers.constants as cte class InselMonthlyEnergyBalance: """ - Import SRA results + Import insel monthly energy balance results """ def __init__(self, city, base_path): diff --git a/hub/imports/results/peak_load.py b/hub/imports/results/peak_load.py deleted file mode 100644 index 9ace3104..00000000 --- a/hub/imports/results/peak_load.py +++ /dev/null @@ -1,61 +0,0 @@ -import hub.helpers.constants as cte -from hub.imports.results.peak_calculation.loads_calculation import LoadsCalculation - - -class PeakLoad: - - _MONTH_STARTING_HOUR = [0, 744, 1416, 2160, 2880, 3624, 4344, 5088, 5832, 6552, 7296, 8016] - - def __init__(self, city): - self._city = city - self._weather_format = 'epw' - - def enrich(self): - for building in self._city.buildings: - monthly_heating_loads = [] - monthly_cooling_loads = [] - ambient_temperature = building.external_temperature[cte.HOUR][self._weather_format] - for month in range(0, 12): - ground_temperature = building.ground_temperature[cte.MONTH]['2'][month] - heating_ambient_temperature = 100 - cooling_ambient_temperature = -100 - heating_calculation_hour = -1 - cooling_calculation_hour = -1 - start_hour = self._MONTH_STARTING_HOUR[month] - end_hour = 8760 - if month < 11: - end_hour = self._MONTH_STARTING_HOUR[month + 1] - for hour in range(start_hour, end_hour): - temperature = ambient_temperature[hour] - if temperature < heating_ambient_temperature: - heating_ambient_temperature = temperature - heating_calculation_hour = hour - if temperature > cooling_ambient_temperature: - cooling_ambient_temperature = temperature - cooling_calculation_hour = hour - - loads = LoadsCalculation(building) - heating_load_transmitted = loads.get_heating_transmitted_load(heating_ambient_temperature, ground_temperature) - heating_load_ventilation_sensible = loads.get_heating_ventilation_load_sensible(heating_ambient_temperature) - heating_load_ventilation_latent = 0 - heating_load = heating_load_transmitted + heating_load_ventilation_sensible + heating_load_ventilation_latent - - cooling_load_transmitted = loads.get_cooling_transmitted_load(cooling_ambient_temperature, ground_temperature) - cooling_load_renovation_sensible = loads.get_cooling_ventilation_load_sensible(cooling_ambient_temperature) - cooling_load_internal_gains_sensible = loads.get_internal_load_sensible() - cooling_load_radiation = loads.get_radiation_load(self._irradiance_format, cooling_calculation_hour) - cooling_load_sensible = cooling_load_transmitted + cooling_load_renovation_sensible - cooling_load_radiation \ - - cooling_load_internal_gains_sensible - - cooling_load_latent = 0 - cooling_load = cooling_load_sensible + cooling_load_latent - if heating_load < 0: - heating_load = 0 - if cooling_load > 0: - cooling_load = 0 - monthly_heating_loads.append(heating_load) - monthly_cooling_loads.append(cooling_load) - - self._results[building.name] = {'monthly heating peak load': monthly_heating_loads, - 'monthly cooling peak load': monthly_cooling_loads} - self._print_results() diff --git a/hub/imports/results_factory.py b/hub/imports/results_factory.py index 4eb7c0e4..5fd542bc 100644 --- a/hub/imports/results_factory.py +++ b/hub/imports/results_factory.py @@ -9,7 +9,6 @@ from pathlib import Path from hub.helpers.utils import validate_import_export_type from hub.hub_logger import logger -from hub.imports.results.peak_load import PeakLoad from hub.imports.results.simplified_radiosity_algorithm import SimplifiedRadiosityAlgorithm from hub.imports.results.insel_monthly_energry_balance import InselMonthlyEnergyBalance from hub.imports.results.insel_heatpump_energy_demand import InselHeatPumpEnergyDemand @@ -54,18 +53,12 @@ class ResultFactory: """ InselHeatPumpEnergyDemand(self._city, self._base_path, self._hp_model).enrich() - def _insel_meb(self): + def _insel_monthly_energy_balance(self): """ Enrich the city with insel monthly energy balance results """ InselMonthlyEnergyBalance(self._city, self._base_path).enrich() - def _peak_load(self): - """ - Enrich the city with peak load results - """ - PeakLoad(self._city).enrich() - def enrich(self): """ Enrich the city given to the class using the usage factory given handler diff --git a/hub/imports/weather/epw_weather_parameters.py b/hub/imports/weather/epw_weather_parameters.py index faaa91f0..2e3b407c 100644 --- a/hub/imports/weather/epw_weather_parameters.py +++ b/hub/imports/weather/epw_weather_parameters.py @@ -71,7 +71,6 @@ class EpwWeatherParameters: except SystemExit: sys.stderr.write(f'Error: wrong formatting of weather file {self._path}\n') sys.exit() - for building in self._city.buildings: building.ground_temperature[cte.MONTH] = ground_temperature ground_temperature = {} diff --git a/hub/imports/weather_factory.py b/hub/imports/weather_factory.py index 296755de..cae4bef6 100644 --- a/hub/imports/weather_factory.py +++ b/hub/imports/weather_factory.py @@ -52,3 +52,7 @@ class WeatherFactory: :return: None """ getattr(self, self._handler, lambda: None)() + + def enrich_debug(self): + _path = Path(self._base_path / 'epw').resolve() + return EpwWeatherParameters(self._city, _path, self._file_name) diff --git a/hub/unittests/test_geometry_factory.py b/hub/unittests/test_geometry_factory.py index 0e43a59f..6bdeca21 100644 --- a/hub/unittests/test_geometry_factory.py +++ b/hub/unittests/test_geometry_factory.py @@ -137,18 +137,16 @@ class TestGeometryFactory(TestCase): """ Test geojson import """ - file = '2000_buildings.geojson' + file = 'hole_building.geojson' city = GeometryFactory('geojson', path=(self._example_path / file).resolve(), - height_field='building_height', + height_field='citygml_me', year_of_construction_field='ANNEE_CONS', name_field='ID_UEV', function_field='CODE_UTILI', function_to_hub=MontrealFunctionToHubFunction().dictionary).city - # include 25 square meter condition for a building reduces buildings number from 2289 to 2057 - for building in city.buildings: - print(building.name) - self.assertEqual(2057, len(city.buildings), 'wrong number of buildings') + hub.exports.exports_factory.ExportsFactory('obj', city, self._output_path).export_debug() + self.assertEqual(1964, len(city.buildings), 'wrong number of buildings') def test_map_neighbours(self): """ @@ -164,7 +162,7 @@ class TestGeometryFactory(TestCase): year_of_construction_field='ANNEE_CONS', function_field='LIBELLE_UT') - # info_lod0 = GeometryHelper.city_mapping(city, plot=False) + info_lod0 = GeometryHelper.city_mapping(city, plot=False) hub.exports.exports_factory.ExportsFactory('obj', city, self._output_path).export() self.assertEqual(info_lod0, info_lod1) for building in city.buildings: @@ -177,36 +175,6 @@ class TestGeometryFactory(TestCase): self.assertEqual('1_part_0_zone_0', city.city_object('3_part_0_zone_0').neighbours[0].name) self.assertEqual('2_part_0_zone_0', city.city_object('3_part_0_zone_0').neighbours[1].name) - def test_neighbours(self): - """ - Test neighbours map creation - """ - file_path = (self._example_path / 'concordia_clean.geojson').resolve() - city = GeometryFactory('geojson', - path=file_path, - height_field='citygml_me', - year_of_construction_field='ANNEE_CONS', - name_field='OBJECTID_12', - function_field='CODE_UTILI', - function_to_hub=Dictionaries().montreal_function_to_hub_function).city - # print(city.lower_corner, city.upper_corner) - for building in city.buildings: - #for ground in building.grounds: - # print(ground.perimeter_polygon.coordinates) - # print(ground.perimeter_polygon.coordinates[0][0] - city.lower_corner[0], ground.perimeter_polygon.coordinates[0][1] - city.lower_corner[1]) - # print(ground.perimeter_polygon.coordinates[1][0] - city.lower_corner[0], ground.perimeter_polygon.coordinates[1][1] - city.lower_corner[1]) - break - ConstructionFactory('nrcan', city).enrich() - UsageFactory('nrcan', city).enrich() - info_lod1 = GeometryHelper.city_mapping(city, plot=True) - for building in city.buildings: - print(building.name) - ns = '' - for n in building.neighbours: - ns = f'{ns} {n.name}' - for surface in n.surfaces: - print('shared', surface.percentage_shared) - print('\t', ns) - # EnergyBuildingsExportsFactory('idf', city, self._output_path).export() + diff --git a/hub/unittests/test_results_import.py b/hub/unittests/test_results_import.py new file mode 100644 index 00000000..c8a8e702 --- /dev/null +++ b/hub/unittests/test_results_import.py @@ -0,0 +1,115 @@ +""" +TestExports test and validate the city export formats +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2022 Concordia CERC group +Project Coder Guille Gutierrez guillermo.gutierrezmorote@concordia.ca +""" +import subprocess +from pathlib import Path +from unittest import TestCase + +import pandas as pd + +import hub.helpers.constants as cte +from hub.exports.energy_building_exports_factory import EnergyBuildingsExportsFactory +from hub.exports.exports_factory import ExportsFactory +from hub.helpers.dictionaries import Dictionaries +from hub.imports.construction_factory import ConstructionFactory +from hub.imports.geometry_factory import GeometryFactory +from hub.imports.results_factory import ResultFactory +from hub.imports.usage_factory import UsageFactory + + +class TestResultsImport(TestCase): + """ + TestImports class contains the unittest for import functionality + """ + def setUp(self) -> None: + """ + Test setup + :return: None + """ + self._example_path = (Path(__file__).parent / 'tests_data').resolve() + self._gml_path = (self._example_path / 'FZK_Haus_LoD_2.gml').resolve() + self._output_path = (Path(__file__).parent / 'tests_outputs').resolve() + self._city = GeometryFactory('citygml', + self._gml_path, + function_to_hub=Dictionaries().alkis_function_to_hub_function).city + ConstructionFactory('nrcan', self._city).enrich() + UsageFactory('nrcan', self._city).enrich() + + def test_sra_import(self): + weather_file = (self._example_path / 'CAN_PQ_Montreal.Intl.AP.716270_CWEC.epw').resolve() + ExportsFactory('sra', self._city, self._output_path, weather_file=weather_file, weather_format='epw').export() + sra_path = (self._output_path / f'{self._city.name}_sra.xml').resolve() + subprocess.run(['sra', str(sra_path)]) + ResultFactory('sra', self._city, self._output_path).enrich() + # Check that all the buildings have radiance in the surfaces + for building in self._city.buildings: + for surface in building.surfaces: + self.assertIsNotNone(surface.global_irradiance) + + def test_meb_import(self): + weather_file = (self._example_path / 'CAN_PQ_Montreal.Intl.AP.716270_CWEC.epw').resolve() + ExportsFactory('sra', self._city, self._output_path, weather_file=weather_file, weather_format='epw').export() + sra_path = (self._output_path / f'{self._city.name}_sra.xml').resolve() + subprocess.run(['sra', str(sra_path)]) + ResultFactory('sra', self._city, self._output_path).enrich() + EnergyBuildingsExportsFactory('insel_monthly_energy_balance', self._city, self._output_path).export() + for building in self._city.buildings: + insel_path = (self._output_path / f'{building.name}.insel') + subprocess.run(['insel', str(insel_path)]) + ResultFactory('insel_monthly_energy_balance', self._city, self._output_path).enrich() + # Check that all the buildings have heating and cooling values + for building in self._city.buildings: + self.assertIsNotNone(building.heating[cte.MONTH][cte.INSEL_MEB]) + self.assertIsNotNone(building.cooling[cte.MONTH][cte.INSEL_MEB]) + self.assertIsNotNone(building.heating[cte.YEAR][cte.INSEL_MEB]) + self.assertIsNotNone(building.cooling[cte.YEAR][cte.INSEL_MEB]) + + def test_peak_loads(self): + # todo: this is not technically a import + weather_file = (self._example_path / 'CAN_PQ_Montreal.Intl.AP.716270_CWEC.epw').resolve() + ExportsFactory('sra', self._city, self._output_path, weather_file=weather_file, weather_format='epw').export() + sra_path = (self._output_path / f'{self._city.name}_sra.xml').resolve() + subprocess.run(['sra', str(sra_path)]) + ResultFactory('sra', self._city, self._output_path).enrich() + for building in self._city.buildings: + self.assertIsNotNone(building.heating_peak_load) + self.assertIsNotNone(building.cooling_peak_load) + + values = [0 for _ in range(8760)] + values[0] = 1000 + expected_yearly = pd.DataFrame([1000], columns=['expected']) + expected_monthly_list = [0 for _ in range(12)] + expected_monthly_list[0] = 1000 + expected_monthly = pd.DataFrame(expected_monthly_list, columns=['expected']) + for building in self._city.buildings: + building.heating[cte.HOUR] = pd.DataFrame(values, columns=['dummy']) + building.cooling[cte.HOUR] = pd.DataFrame(values, columns=['dummy']) + self.assertIsNotNone(building.heating_peak_load) + self.assertIsNotNone(building.cooling_peak_load) + pd.testing.assert_series_equal( + building.heating_peak_load[cte.YEAR]['heating peak loads'], + expected_yearly['expected'], + check_names=False + ) + pd.testing.assert_series_equal( + building.cooling_peak_load[cte.YEAR]['cooling peak loads'], + expected_yearly['expected'], + check_names=False + ) + pd.testing.assert_series_equal( + building.heating_peak_load[cte.MONTH]['heating peak loads'], + expected_monthly['expected'], + check_names=False + ) + pd.testing.assert_series_equal( + building.cooling_peak_load[cte.MONTH]['cooling peak loads'], + expected_monthly['expected'], + check_names=False + ) + + + +