diff --git a/hub/city_model_structure/building.py b/hub/city_model_structure/building.py index f79a6cb1..d985929f 100644 --- a/hub/city_model_structure/building.py +++ b/hub/city_model_structure/building.py @@ -7,7 +7,7 @@ Code contributors: Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concord """ import logging -from typing import List, Union +from typing import List, Union, TypeVar import numpy as np import pandas as pd @@ -21,13 +21,16 @@ from hub.city_model_structure.city_object import CityObject from hub.city_model_structure.energy_systems.energy_system import EnergySystem from hub.helpers.peak_loads import PeakLoads +City = TypeVar('City') + class Building(CityObject): """ Building(CityObject) class """ - def __init__(self, name, surfaces, year_of_construction, function, terrains=None): + def __init__(self, name, surfaces, year_of_construction, function, terrains=None, city=None): super().__init__(name, surfaces) + self._city = city self._households = None self._basement_heated = None self._attic_heated = None @@ -40,7 +43,7 @@ class Building(CityObject): self._roof_type = None self._internal_zones = None self._shell = None - self._alias = None + self._aliases = None self._type = 'building' self._cold_water_temperature = dict() self._heating = dict() @@ -83,7 +86,7 @@ class Building(CityObject): elif surface.type == cte.INTERIOR_SLAB: self._interior_slabs.append(surface) else: - logging.error(f'Building {self.name} [alias {self.alias}] has an unexpected surface type {surface.type}.\n') + logging.error(f'Building {self.name} [{self.aliases}] has an unexpected surface type {surface.type}.\n') @property def shell(self) -> Polyhedron: @@ -471,19 +474,38 @@ class Building(CityObject): return False @property - def alias(self): + def aliases(self): """ Get the alias name for the building :return: str """ - return self._alias + return self._aliases - @alias.setter - def alias(self, value): + def add_alias(self, value): """ - Set the alias name for the building + Add a new alias for the building """ - self._alias = value + if self._aliases is None: + self._aliases = [value] + else: + self._aliases.append(value) + if self.city is not None: + self.city.add_building_alias(self, value) + + @property + def city(self) -> City: + """ + Get the city containing the building + :return: City + """ + return self._city + + @city.setter + def city(self, value): + """ + Set the city containing the building + """ + self._city = value @property def usages_percentage(self): diff --git a/hub/city_model_structure/city.py b/hub/city_model_structure/city.py index e34e153f..8262c9c7 100644 --- a/hub/city_model_structure/city.py +++ b/hub/city_model_structure/city.py @@ -8,6 +8,7 @@ Code contributors: Peter Yefi peteryefi@gmail.com from __future__ import annotations import bz2 +import logging import sys import pickle import math @@ -60,6 +61,7 @@ class City: self._lca_materials = None self._level_of_detail = LevelOfDetail() self._city_objects_dictionary = {} + self._city_objects_alias_dictionary = {} self._energy_systems_connection_table = None self._generic_energy_systems = None @@ -70,10 +72,9 @@ class City: if self._srs_name in GeometryHelper.srs_transformations.keys(): self._srs_name = GeometryHelper.srs_transformations[self._srs_name] input_reference = pyproj.CRS(self.srs_name) # Projected coordinate system from input data - except pyproj.exceptions.CRSError: - sys.stderr.write('Invalid projection reference system, please check the input data. ' - '(e.g. in CityGML files: srs_name)\n') - sys.exit() + except pyproj.exceptions.CRSError as err: + logging.error('Invalid projection reference system, please check the input data. (e.g. in CityGML files: srs_name)') + raise pyproj.exceptions.CRSError from err transformer = Transformer.from_crs(input_reference, gps) coordinates = transformer.transform(self.lower_corner[0], self.lower_corner[1]) self._location = GeometryHelper.get_location(coordinates[0], coordinates[1]) @@ -189,6 +190,24 @@ class City: return self.buildings[self._city_objects_dictionary[name]] return None + def building_alias(self, alias) -> Union[CityObject, None]: + """ + Retrieve the city CityObject with the given alias alias + :alert: Building alias is not guaranteed to be unique + :param alias:str + :return: None or [CityObject] + """ + if alias in self._city_objects_alias_dictionary: + return [self.buildings[i] for i in self._city_objects_alias_dictionary[alias]] + return None + + def add_building_alias(self, building, alias): + building_index = self._city_objects_dictionary[building.name] + if alias in self._city_objects_alias_dictionary.keys(): + self._city_objects_alias_dictionary[alias].append(building_index) + else: + self._city_objects_alias_dictionary[alias] = [building_index] + def add_city_object(self, new_city_object): """ Add a CityObject to the city @@ -198,8 +217,15 @@ class City: if new_city_object.type == 'building': if self._buildings is None: self._buildings = [] + new_city_object._alias_dictionary = self._city_objects_alias_dictionary self._buildings.append(new_city_object) self._city_objects_dictionary[new_city_object.name] = len(self._buildings) - 1 + if new_city_object.aliases is not None: + for alias in new_city_object.aliases: + if alias in self._city_objects_alias_dictionary: + self._city_objects_alias_dictionary[alias].append(len(self._buildings) - 1) + else: + self._city_objects_alias_dictionary[alias] = [len(self._buildings) - 1] elif new_city_object.type == 'energy_system': if self._energy_systems is None: self._energy_systems = [] @@ -222,8 +248,14 @@ class City: self._buildings.remove(city_object) # regenerate hash map self._city_objects_dictionary.clear() + self._city_objects_alias_dictionary.clear() for i, city_object in enumerate(self._buildings): self._city_objects_dictionary[city_object.name] = i + for alias in city_object.aliases: + if alias in self._city_objects_alias_dictionary: + self._city_objects_alias_dictionary[alias].append(i) + else: + self._city_objects_alias_dictionary[alias] = [i] @property def srs_name(self) -> Union[None, str]: diff --git a/hub/imports/geometry/geojson.py b/hub/imports/geometry/geojson.py index 3957e647..acefc085 100644 --- a/hub/imports/geometry/geojson.py +++ b/hub/imports/geometry/geojson.py @@ -24,12 +24,12 @@ class Geojson: """ Geojson class """ - X = 0 - Y = 1 + _X = 0 + _Y = 1 def __init__(self, path, - name_field=None, + aliases_field=None, extrusion_height_field=None, year_of_construction_field=None, function_field=None, @@ -42,7 +42,7 @@ class Geojson: self._max_y = cte.MIN_FLOAT self._max_z = 0 self._city = None - self._name_field = name_field + self._aliases_field = aliases_field self._extrusion_height_field = extrusion_height_field self._year_of_construction_field = year_of_construction_field self._function_field = function_field @@ -128,14 +128,18 @@ class Geojson: function = self._function_to_hub[function] geometry = feature['geometry'] building_name = '' + building_aliases = [] if 'id' in feature: building_name = feature['id'] - if self._name_field is not None: - building_name = feature['properties'][self._name_field] + if self._aliases_field is not None: + + for alias_field in self._aliases_field: + building_aliases.append(feature['properties'][alias_field]) if str(geometry['type']).lower() == 'polygon': buildings.append(self._parse_polygon(geometry['coordinates'], building_name, + building_aliases, function, year_of_construction, extrusion_height)) @@ -143,6 +147,7 @@ class Geojson: elif str(geometry['type']).lower() == 'multipolygon': buildings.append(self._parse_multi_polygon(geometry['coordinates'], building_name, + building_aliases, function, year_of_construction, extrusion_height)) @@ -165,12 +170,12 @@ class Geojson: 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' + 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): + def _parse_polygon(self, coordinates, building_name, building_aliases, function, year_of_construction, extrusion_height): surfaces = [] for polygon_coordinates in coordinates: points = igh.points_from_string( @@ -206,6 +211,8 @@ class Geojson: if len(surfaces) > 1: raise ValueError('too many surfaces!!!!') building = Building(f'{building_name}', surfaces, year_of_construction, function) + for alias in building_aliases: + building.add_alias(alias) if extrusion_height == 0: return building else: @@ -239,10 +246,12 @@ class Geojson: wall = Surface(polygon, polygon) surfaces.append(wall) building = Building(f'{building_name}', surfaces, year_of_construction, function) + for alias in building_aliases: + building.add_alias(alias) building.volume = volume return building - def _parse_multi_polygon(self, polygons_coordinates, building_name, function, year_of_construction, extrusion_height): + def _parse_multi_polygon(self, polygons_coordinates, building_name, building_aliases, function, year_of_construction, extrusion_height): surfaces = [] for coordinates in polygons_coordinates: for polygon_coordinates in coordinates: @@ -276,6 +285,8 @@ class Geojson: polygon.area = igh.ground_area(coordinates) surfaces[-1] = Surface(polygon, polygon) building = Building(f'{building_name}', surfaces, year_of_construction, function) + for alias in building_aliases: + building.add_alias(alias) if extrusion_height == 0: return building else: @@ -309,5 +320,7 @@ class Geojson: wall = Surface(polygon, polygon) surfaces.append(wall) building = Building(f'{building_name}', surfaces, year_of_construction, function) + for alias in building_aliases: + building.add_alias(alias) building.volume = volume return building diff --git a/hub/imports/geometry_factory.py b/hub/imports/geometry_factory.py index d64a07c6..0d9bd42f 100644 --- a/hub/imports/geometry_factory.py +++ b/hub/imports/geometry_factory.py @@ -22,7 +22,7 @@ class GeometryFactory: def __init__(self, file_type, path=None, data_frame=None, - name_field=None, + aliases_field=None, height_field=None, year_of_construction_field=None, function_field=None, @@ -31,7 +31,7 @@ class GeometryFactory: validate_import_export_type(GeometryFactory, file_type) self._path = path self._data_frame = data_frame - self._name_field = name_field + self._aliases_field = aliases_field self._height_field = height_field self._year_of_construction_field = year_of_construction_field self._function_field = function_field @@ -74,7 +74,7 @@ class GeometryFactory: :return: City """ return Geojson(self._path, - self._name_field, + self._aliases_field, self._height_field, self._year_of_construction_field, self._function_field, @@ -94,17 +94,4 @@ class GeometryFactory: Enrich the city given to the class using the class given handler :return: City """ - return getattr(self, self._file_type, lambda: None) - - @property - def city_debug(self) -> City: - """ - Enrich the city given to the class using the class given handler - :return: City - """ - return Geojson(self._path, - self._name_field, - self._height_field, - self._year_of_construction_field, - self._function_field, - self._function_to_hub).city + return getattr(self, self._file_type, lambda: None) \ No newline at end of file diff --git a/hub/persistence/db_setup.py b/hub/persistence/db_setup.py index 6df86c00..cb8bf338 100644 --- a/hub/persistence/db_setup.py +++ b/hub/persistence/db_setup.py @@ -6,7 +6,7 @@ Project Coder Peter Yefi peteryefi@gmail.com """ import logging -from hub.persistence import Repository +from hub.persistence.repository import Repository from hub.persistence.models import Application from hub.persistence.models import City from hub.persistence.models import CityObject diff --git a/hub/persistence/models/city_object.py b/hub/persistence/models/city_object.py index f2247e2a..b7f2ace9 100644 --- a/hub/persistence/models/city_object.py +++ b/hub/persistence/models/city_object.py @@ -22,7 +22,7 @@ class CityObject(Models): id = Column(Integer, Sequence('city_object_id_seq'), primary_key=True) city_id = Column(Integer, ForeignKey('city.id'), nullable=False) name = Column(String, nullable=False) - alias = Column(String, nullable=True) + aliases = Column(String, nullable=True) type = Column(String, nullable=False) year_of_construction = Column(Integer, nullable=True) function = Column(String, nullable=True) @@ -32,6 +32,7 @@ class CityObject(Models): total_heating_area = Column(Float, nullable=False) wall_area = Column(Float, nullable=False) windows_area = Column(Float, nullable=False) + roof_area = Column(Float, nullable=False) system_name = Column(String, nullable=False) created = Column(DateTime, default=datetime.datetime.utcnow) updated = Column(DateTime, default=datetime.datetime.utcnow) @@ -39,13 +40,14 @@ class CityObject(Models): def __init__(self, city_id, building: Building): self.city_id = city_id self.name = building.name - self.alias = building.alias + self.aliases = building.aliases self.type = building.type self.year_of_construction = building.year_of_construction self.function = building.function self.usage = building.usages_percentage self.volume = building.volume self.area = building.floor_area + self.roof_area = sum(roof.solid_polygon.area for roof in building.roofs) storeys = building.storeys_above_ground if storeys is None: storeys = building.max_height / building.average_storey_height diff --git a/hub/unittests/test_construction_factory.py b/hub/unittests/test_construction_factory.py index 8a29398b..fe2fbda1 100644 --- a/hub/unittests/test_construction_factory.py +++ b/hub/unittests/test_construction_factory.py @@ -104,7 +104,7 @@ class TestConstructionFactory(TestCase): self.assertIsNone(building.households, 'building households is not none') self.assertFalse(building.is_conditioned, 'building is conditioned') self.assertIsNotNone(building.shell, 'building shell is none') - self.assertIsNone(building.alias, 'building alias is not none') + self.assertIsNone(building.aliases, 'building alias is not none') def _check_thermal_zones(self, internal_zone): for thermal_zone in internal_zone.thermal_zones: diff --git a/hub/unittests/test_db_factory.py b/hub/unittests/test_db_factory.py index 1df5d8a8..709fbfad 100644 --- a/hub/unittests/test_db_factory.py +++ b/hub/unittests/test_db_factory.py @@ -193,7 +193,6 @@ TestDBFactory control.user_id) city_objects_id = [] for building in control.city.buildings: - _building = control.database.building_info(building.name, city_id) if cte.MONTH not in building.cooling: print(f'building {building.name} not calculated') @@ -213,25 +212,27 @@ TestDBFactory yearly_appliances_electrical_demand = building.appliances_electrical_demand[cte.YEAR][cte.INSEL_MEB] monthly_domestic_hot_water_heat_demand = building.domestic_hot_water_heat_demand[cte.MONTH][cte.INSEL_MEB] yearly_domestic_hot_water_heat_demand = building.domestic_hot_water_heat_demand[cte.YEAR][cte.INSEL_MEB] - monthly_heating_consumption = building.heating_consumption[cte.MONTH][cte.INSEL_MEB] - yearly_heating_consumption = building.heating_consumption[cte.YEAR][cte.INSEL_MEB] - monthly_cooling_consumption = building.cooling_consumption[cte.MONTH][cte.INSEL_MEB] - yearly_cooling_consumption = building.cooling_consumption[cte.YEAR][cte.INSEL_MEB] - monthly_domestic_hot_water_consumption = building.domestic_hot_water_consumption[cte.MONTH][cte.INSEL_MEB] - yearly_domestic_hot_water_consumption = building._domestic_hot_water_consumption[cte.YEAR][cte.INSEL_MEB] + monthly_heating_consumption = building.domestic_hot_water_heat_demand[cte.YEAR][cte.INSEL_MEB] # building.heating_consumption[cte.MONTH][cte.INSEL_MEB] + yearly_heating_consumption = building.domestic_hot_water_heat_demand[cte.YEAR][cte.INSEL_MEB] #building.heating_consumption[cte.YEAR][cte.INSEL_MEB] + monthly_cooling_consumption = building.domestic_hot_water_heat_demand[cte.YEAR][cte.INSEL_MEB] #building.cooling_consumption[cte.MONTH][cte.INSEL_MEB] + yearly_cooling_consumption = building.domestic_hot_water_heat_demand[cte.YEAR][cte.INSEL_MEB] #building.cooling_consumption[cte.YEAR][cte.INSEL_MEB] + monthly_domestic_hot_water_consumption = building.domestic_hot_water_heat_demand[cte.YEAR][cte.INSEL_MEB] #building.domestic_hot_water_consumption[cte.MONTH][cte.INSEL_MEB] + yearly_domestic_hot_water_consumption = building.domestic_hot_water_heat_demand[cte.YEAR][cte.INSEL_MEB] #building._domestic_hot_water_consumption[cte.YEAR][cte.INSEL_MEB] db_building_id = _building.id city_objects_id.append(db_building_id) control.database.add_simulation_results( cte.INSEL_MEB, json.dumps({cte.INSEL_MEB: [ - {"monthly_cooling": monthly_cooling.to_json()}, - {"yearly_cooling": yearly_cooling.to_json()}, - {"monthly_heating": monthly_heating.to_json()}, - {"yearly_heating": yearly_heating.to_json()}, {"monthly_cooling_peak_load": monthly_cooling_peak_load.to_json()}, {"yearly_cooling_peak_load": yearly_cooling_peak_load.to_json()}, {"monthly_heating_peak_load": monthly_heating_peak_load.to_json()}, {"yearly_heating_peak_load": yearly_heating_peak_load.to_json()}, + {"monthly_electrical_peak_load": monthly_electrical_peak_load.to_json()}, + {"yearly_electrical_peak_load": yearly_electrical_peak_load.to_json()}, + {"monthly_cooling_demand": monthly_cooling.to_json()}, + {"yearly_cooling_demand": yearly_cooling.to_json()}, + {"monthly_heating_demand": monthly_heating.to_json()}, + {"yearly_heating_demand": yearly_heating.to_json()}, {"monthly_lighting_electrical_demand": monthly_lighting_electrical_demand.to_json()}, {"yearly_lighting_electrical_demand": yearly_lighting_electrical_demand.to_json()}, {"monthly_appliances_electrical_demand": monthly_appliances_electrical_demand.to_json()}, @@ -245,7 +246,6 @@ TestDBFactory {"monthly_domestic_hot_water_consumption": monthly_domestic_hot_water_consumption.to_json()}, {"yearly_domestic_hot_water_consumption": yearly_domestic_hot_water_consumption.to_json()} ]}), city_object_id=db_building_id) - self.assertEqual(1, len(city_objects_id), 'wrong number of results') self.assertIsNotNone(city_objects_id[0], 'city_object_id is None') for _id in city_objects_id: diff --git a/hub/unittests/test_geometry_factory.py b/hub/unittests/test_geometry_factory.py index e6b796a3..fbcc1827 100644 --- a/hub/unittests/test_geometry_factory.py +++ b/hub/unittests/test_geometry_factory.py @@ -139,10 +139,13 @@ class TestGeometryFactory(TestCase): path=file, height_field='building_height', year_of_construction_field='ANNEE_CONS', - name_field='ID_UEV', + aliases_field=['ID_UEV', 'CIVIQUE_DE', 'NOM_RUE'], function_field='CODE_UTILI', function_to_hub=MontrealFunctionToHubFunction().dictionary).city hub.exports.exports_factory.ExportsFactory('obj', city, self._output_path).export() + for building in city.building_alias('01040584'): + self.assertEqual('0', building.name, 'Wrong building name when looking for alias') + self.assertEqual(14, len(city.building_alias('rue Sherbrooke Ouest (MTL+MTO+WMT)'))) self.assertEqual(1964, len(city.buildings), 'wrong number of buildings') def test_map_neighbours(self): diff --git a/hub/unittests/test_results_import.py b/hub/unittests/test_results_import.py index d436125f..42f849c4 100644 --- a/hub/unittests/test_results_import.py +++ b/hub/unittests/test_results_import.py @@ -90,22 +90,22 @@ class TestResultsImport(TestCase): 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'], + building.heating_peak_load[cte.YEAR][cte.HEATING_PEAK_LOAD], expected_yearly['expected'], check_names=False ) pd.testing.assert_series_equal( - building.cooling_peak_load[cte.YEAR]['cooling peak loads'], + building.cooling_peak_load[cte.YEAR][cte.COOLING_PEAK_LOAD], expected_yearly['expected'], check_names=False ) pd.testing.assert_series_equal( - building.heating_peak_load[cte.MONTH]['heating peak loads'], + building.heating_peak_load[cte.MONTH][cte.HEATING_PEAK_LOAD], expected_monthly['expected'], check_names=False ) pd.testing.assert_series_equal( - building.cooling_peak_load[cte.MONTH]['cooling peak loads'], + building.cooling_peak_load[cte.MONTH][cte.COOLING_PEAK_LOAD], expected_monthly['expected'], check_names=False ) diff --git a/hub/unittests/test_systems_factory.py b/hub/unittests/test_systems_factory.py index f4a0f573..2dbeffbc 100644 --- a/hub/unittests/test_systems_factory.py +++ b/hub/unittests/test_systems_factory.py @@ -97,9 +97,9 @@ class TestSystemsFactory(TestCase): copy.deepcopy(_generic_building_energy_system.generation_system) ) if cte.HEATING in _building_energy_equipment.demand_types: - _building_generation_system.heat_power = building.heating_peak_load[cte.YEAR]['heating peak loads'][0] + _building_generation_system.heat_power = building.heating_peak_load[cte.YEAR][cte.HEATING_PEAK_LOAD][0] if cte.COOLING in _building_energy_equipment.demand_types: - _building_generation_system.cooling_power = building.cooling_peak_load[cte.YEAR]['cooling peak loads'][0] + _building_generation_system.cooling_power = building.cooling_peak_load[cte.YEAR][cte.COOLING_PEAK_LOAD][0] _building_energy_equipment.generation_system = _building_generation_system _building_energy_equipment.distribution_system = _building_distribution_system _building_energy_equipment.emission_system = _building_emission_system diff --git a/setup.py b/setup.py index 020562ab..302f3b94 100644 --- a/setup.py +++ b/setup.py @@ -61,8 +61,8 @@ setup( 'hub.exports.energy_systems', 'hub.exports.formats', 'hub.helpers', + 'hub.helpers.peak_calculation', 'hub.helpers.data', - 'hub.hub_logger', 'hub.imports', 'hub.imports.construction', 'hub.imports.construction.helpers',