From a33bf0b36643adeb58504bbaf1e58e4fb32dac1d Mon Sep 17 00:00:00 2001 From: Guille Date: Tue, 16 May 2023 18:06:22 -0400 Subject: [PATCH] Partial completion of new persistence. Results are still missing and need to be added to the final commit. including the db table creation that seems to be missing --- .../attributes/polygon.py | 2 + .../attributes/polyhedron.py | 1 - hub/city_model_structure/building.py | 2 + .../insel/insel_monthly_energy_balance.py | 2 +- hub/exports/db_factory.py | 4 +- hub/hub_logger/__init__.py | 7 +- .../construction/nrcan_physics_parameters.py | 4 + .../construction/nrel_physics_parameters.py | 16 +++- hub/persistence/models/city_object.py | 79 +++++++++------ hub/persistence/repositories/city.py | 3 +- hub/persistence/repositories/city_object.py | 14 +-- hub/unittests/test_construction_factory.py | 9 -- hub/unittests/test_db_factory.py | 90 +++++++++--------- hub/unittests/test_insel_exports.py | 1 + hub/unittests/tests_data/pickle_path.bz2 | Bin 1806 -> 7062 bytes 15 files changed, 125 insertions(+), 109 deletions(-) diff --git a/hub/city_model_structure/attributes/polygon.py b/hub/city_model_structure/attributes/polygon.py index c98289be..cada1c29 100644 --- a/hub/city_model_structure/attributes/polygon.py +++ b/hub/city_model_structure/attributes/polygon.py @@ -246,7 +246,9 @@ class Polygon: polygon = shapley_polygon(coordinates) try: + vertices_2d, faces = trimesh.creation.triangulate_polygon(polygon, engine='triangle') + mesh = Trimesh(vertices=vertices, faces=faces) # check orientation diff --git a/hub/city_model_structure/attributes/polyhedron.py b/hub/city_model_structure/attributes/polyhedron.py index 553f1e26..c88e91d0 100644 --- a/hub/city_model_structure/attributes/polyhedron.py +++ b/hub/city_model_structure/attributes/polyhedron.py @@ -92,7 +92,6 @@ class Polyhedron: points = polygon.coordinates if len(points) != 3: sub_polygons = polygon.triangles - # todo: I modified this! To be checked @Guille if len(sub_polygons) >= 1: for sub_polygon in sub_polygons: face = [] diff --git a/hub/city_model_structure/building.py b/hub/city_model_structure/building.py index 04c4703a..4da4ce67 100644 --- a/hub/city_model_structure/building.py +++ b/hub/city_model_structure/building.py @@ -494,6 +494,8 @@ class Building(CityObject): """ _usage = '' for internal_zone in self.internal_zones: + if internal_zone.usages is None: + continue for usage in internal_zone.usages: _usage = f'{_usage}{usage.name}_{usage.percentage} ' return _usage.rstrip() 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 6cd228c5..0ecac92e 100644 --- a/hub/exports/building_energy/insel/insel_monthly_energy_balance.py +++ b/hub/exports/building_energy/insel/insel_monthly_energy_balance.py @@ -77,7 +77,7 @@ class InselMonthlyEnergyBalance(Insel): if levels_of_detail.weather < 1: raise Exception(f'Level of detail of weather = {levels_of_detail.weather}. Required minimum level 1') if levels_of_detail.surface_radiation is None: - raise Exception(f'Level of detail of usage not assigned') + raise Exception(f'Level of detail of surface radiation not assigned') if levels_of_detail.surface_radiation < 1: raise Exception(f'Level of detail of surface radiation = {levels_of_detail.surface_radiation}. ' f'Required minimum level 1') diff --git a/hub/exports/db_factory.py b/hub/exports/db_factory.py index 5f4fde9e..959d1906 100644 --- a/hub/exports/db_factory.py +++ b/hub/exports/db_factory.py @@ -37,7 +37,7 @@ class DBFactory: def user_info(self, name, password, application_id): """ Retrieve the user info for the given name and password and application_id - :param name: the user name + :param name: the username :param password: the user password :param application_id: the application id :return: User or None @@ -47,7 +47,7 @@ class DBFactory: def user_login(self, name, password, application_uuid): """ Retrieve the user info - :param name: the user name + :param name: the username :param password: the user password :param application_uuid: the application uuid :return: User or None diff --git a/hub/hub_logger/__init__.py b/hub/hub_logger/__init__.py index d1d11d35..d61ec00c 100644 --- a/hub/hub_logger/__init__.py +++ b/hub/hub_logger/__init__.py @@ -4,10 +4,11 @@ import os import sys -def get_logger(file_logger=False): +def get_logger(file_logger=False, debug_level=logger.ERROR): """ Returns a logging object :param file_logger: a boolean to indicate the kind of logging + :param debug_level: the value for the logger level (default error) object to return, true (default) means a file logger is required :return: """ @@ -21,11 +22,11 @@ def get_logger(file_logger=False): os.mkdir(log_dir) with open(log_file, 'x'): pass - logger.basicConfig(filename=log_file, format=log_format, level=logger.DEBUG) + logger.basicConfig(filename=log_file, format=log_format, level=debug_level) return logger except IOError as err: print(f'I/O exception: {err}') else: logger.getLogger().addHandler(logger.StreamHandler(stream=sys.stdout)) - logger.getLogger().setLevel(logger.DEBUG) + logger.getLogger().setLevel(debug_level) return logger.getLogger() diff --git a/hub/imports/construction/nrcan_physics_parameters.py b/hub/imports/construction/nrcan_physics_parameters.py index d1d5db99..0d563338 100644 --- a/hub/imports/construction/nrcan_physics_parameters.py +++ b/hub/imports/construction/nrcan_physics_parameters.py @@ -35,6 +35,10 @@ class NrcanPhysicsParameters: city = self._city nrcan_catalog = ConstructionCatalogFactory('nrcan').catalog for building in city.buildings: + if building.function not in Dictionaries().hub_function_to_nrcan_construction_function.keys(): + logger.error(f'Building {building.name} has an unknown building function {building.function}\n') + sys.stderr.write(f'Building {building.name} has an unknown building function {building.function}\n') + continue function = Dictionaries().hub_function_to_nrcan_construction_function[building.function] try: archetype = self._search_archetype(nrcan_catalog, function, building.year_of_construction, self._climate_zone) diff --git a/hub/imports/construction/nrel_physics_parameters.py b/hub/imports/construction/nrel_physics_parameters.py index c3dcbe07..9d4bf2f3 100644 --- a/hub/imports/construction/nrel_physics_parameters.py +++ b/hub/imports/construction/nrel_physics_parameters.py @@ -35,21 +35,27 @@ class NrelPhysicsParameters: city = self._city nrel_catalog = ConstructionCatalogFactory('nrel').catalog for building in city.buildings: + if building.function not in Dictionaries().hub_function_to_nrel_construction_function.keys(): + logger.error(f'Building {building.name} has unknown function [{building.function}]') + sys.stderr.write(f'Building {building.name} has unknown function [{building.function}]\n') + continue + if building.function not in Dictionaries().hub_function_to_nrel_construction_function.keys(): + logger.error(f'Building {building.name} has unknown function {building.function}\n') + sys.stderr.write(f'Building {building.name} has unknown function {building.function}\n') + continue function = Dictionaries().hub_function_to_nrel_construction_function[building.function] try: - archetype = self._search_archetype(nrel_catalog, function, building.year_of_construction, - self._climate_zone) + archetype = self._search_archetype(nrel_catalog, function, building.year_of_construction, self._climate_zone) except KeyError: logger.error(f'Building {building.name} has unknown construction archetype for building function: ' - f'{function} [{building.function}], building year of construction: {building.year_of_construction} ' - f'and climate zone {self._climate_zone}\n') + f'{function} [{building.function}], building year of construction: {building.year_of_construction}' + f' and climate zone {self._climate_zone}\n') sys.stderr.write(f'Building {building.name} has unknown construction archetype for building function: ' f'{function} [{building.function}], ' f'building year of construction: {building.year_of_construction} ' f'and climate zone {self._climate_zone}\n') continue - # if building has no thermal zones defined from geometry, and the building will be divided in storeys, # one thermal zone per storey is assigned if len(building.internal_zones) == 1: diff --git a/hub/persistence/models/city_object.py b/hub/persistence/models/city_object.py index 64eb5905..30b05323 100644 --- a/hub/persistence/models/city_object.py +++ b/hub/persistence/models/city_object.py @@ -13,36 +13,57 @@ from sqlalchemy import DateTime from hub.city_model_structure.building import Building from hub.persistence.configuration import Models + class CityObject(Models): - """ + """ A model representation of an application """ - __tablename__ = 'city_object' - 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) - type = Column(String, nullable=False) - year_of_construction = Column(Integer, nullable=True) - function = Column(String, nullable=True) - usage = Column(String, nullable=True) - volume = Column(Float, nullable=False) - area = Column(Float, nullable=False) - total_heating_area = Column(Float, nullable=False) - wall_area = Column(Float, nullable=False) - windows_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) + __tablename__ = 'city_object' + 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) + type = Column(String, nullable=False) + year_of_construction = Column(Integer, nullable=True) + function = Column(String, nullable=True) + usage = Column(String, nullable=True) + volume = Column(Float, nullable=False) + area = Column(Float, nullable=False) + total_heating_area = Column(Float, nullable=False) + wall_area = Column(Float, nullable=False) + windows_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) - # def __init__(self, city_id, name, alias, object_type, year_of_construction, function, usage, volume, area): - def __init__(self, city_id, building: Building): - self.city_id = city_id - self.name = building.name - self.alias = building.alias - 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 + # def __init__(self, city_id, name, alias, object_type, year_of_construction, function, usage, volume, area): + def __init__(self, city_id, building: Building): + self.city_id = city_id + self.name = building.name + self.alias = building.alias + 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 + storeys = building.storeys_above_ground + if storeys is None: + print(building.average_storey_height) + storeys = building.max_height / building.average_storey_height + self.total_heating_area = building.floor_area * storeys + wall_area = 0 + for wall in building.walls: + wall_area += wall.solid_polygon.area + self.wall_area = wall_area + window_ratio = 0 + for internal_zone in building.internal_zones: + for thermal_zone in internal_zone.thermal_zones: + for thermal_boundary in thermal_zone.thermal_boundaries: + window_ratio = thermal_boundary.window_ratio + break + self.windows_area = wall_area * window_ratio + system_name = building.energy_systems_archetype_name + if system_name is None: + system_name = '' + self.system_name = system_name diff --git a/hub/persistence/repositories/city.py b/hub/persistence/repositories/city.py index 7515bfef..ac535877 100644 --- a/hub/persistence/repositories/city.py +++ b/hub/persistence/repositories/city.py @@ -76,7 +76,8 @@ class City(Repository): """ try: now = datetime.datetime.utcnow() - self.session.query(Model).filter(Model.id == city_id).update({'name': city.name,'updated': now}) + print(f'{now}') + self.session.query(Model).filter(Model.id == city_id).update({'name': city.name, 'updated': now}) self.session.commit() except SQLAlchemyError as err: logger.error(f'Error while updating city: {err}') diff --git a/hub/persistence/repositories/city_object.py b/hub/persistence/repositories/city_object.py index 7ca4d8e6..8a33df7c 100644 --- a/hub/persistence/repositories/city_object.py +++ b/hub/persistence/repositories/city_object.py @@ -41,20 +41,8 @@ class CityObject(Repository): city_object = self.get_by_name_and_city(building.name, city_id) if city_object is None: try: - object_usage = '' - for internal_zone in building.internal_zones: - for usage in internal_zone.usages: - object_usage = f'{object_usage}{usage.name}_{usage.percentage} ' - object_usage = object_usage.rstrip() city_object = Model(city_id=city_id, - name=building.name, - alias=building.alias, - object_type=building.type, - year_of_construction=building.year_of_construction, - function=building.function, - usage=object_usage, - volume=building.volume, - area=building.floor_area) + building=building) self.session.add(city_object) self.session.flush() self.session.commit() diff --git a/hub/unittests/test_construction_factory.py b/hub/unittests/test_construction_factory.py index 1c5c1b52..8a29398b 100644 --- a/hub/unittests/test_construction_factory.py +++ b/hub/unittests/test_construction_factory.py @@ -284,12 +284,3 @@ class TestConstructionFactory(TestCase): self.assertIsNotNone(thermal_boundary.layers, 'layers is none') self._check_thermal_openings(thermal_boundary) self._check_surfaces(thermal_boundary) - - def test_archetype_not_found(self): - file = 'pluto_building.gml' - city = self._get_citygml(file) - for building in city.buildings: - building.year_of_construction = 1990 - building.function = 'office' - ConstructionFactory('nrel', city).enrich() - diff --git a/hub/unittests/test_db_factory.py b/hub/unittests/test_db_factory.py index 6e92c294..391f21ff 100644 --- a/hub/unittests/test_db_factory.py +++ b/hub/unittests/test_db_factory.py @@ -11,6 +11,8 @@ from pathlib import Path import sqlalchemy.exc from hub.imports.geometry_factory import GeometryFactory +from hub.imports.construction_factory import ConstructionFactory +from hub.imports.usage_factory import UsageFactory from hub.imports.db_factory import DBFactory as ImportDBFactory from hub.imports.user_factory import UserFactory from hub.exports.db_factory import DBFactory as ExportDBFactory @@ -18,29 +20,34 @@ from hub.persistence.repository import Repository from sqlalchemy import create_engine from hub.persistence.models import City, Application, CityObject from hub.persistence.models import User, UserRoles +from hub.helpers.dictionaries import Dictionaries from sqlalchemy.exc import ProgrammingError import uuid -class Configure: +class Configure: _skip_test = False _skip_reason = 'PostgreSQL not properly installed in host machine' def __init__(self): """ - Test - setup - :return: None - """ +Test +setup +:return: None +""" self._skip_test = False # Create test database - dotenv_path = str(Path("{}/.local/etc/hub/.env".format(os.path.expanduser('~'))).resolve()) - repository = Repository(db_name='hub_unittest', app_env='TEST', dotenv_path=dotenv_path) + dotenv_path = Path("{}/.local/etc/hub/.env".format(os.path.expanduser('~'))).resolve() + if not dotenv_path.exists(): + self._skip_test = True + self._skip_reason = f'.env file missing at {dotenv_path}' + return + dotenv_path = str(dotenv_path) + repository = Repository(db_name='hub_unittests', app_env='TEST', dotenv_path=dotenv_path) engine = create_engine(repository.configuration.connection_string) try: # delete test database if it exists connection = engine.connect() - connection.execute('commit') connection.close() except ProgrammingError: print(f'Database does not exist. Nothing to delete') @@ -55,7 +62,12 @@ class Configure: CityObject.__table__.create(bind=repository.engine, checkfirst=True) city_file = "tests_data/FZK_Haus_LoD_2.gml" - self._city = GeometryFactory('citygml', city_file).city + self._city = GeometryFactory('citygml', + city_file, + function_to_hub=Dictionaries().alkis_function_to_hub_function).city + ConstructionFactory('nrcan', self._city).enrich() + UsageFactory('nrcan', self._city).enrich() + self._import_db_factory = ImportDBFactory( db_name=repository.configuration.db_name, app_env='TEST', @@ -103,7 +115,7 @@ class Configure: @property def message(self): - return self._skip_message + return self._skip_reason @property def city(self): @@ -113,12 +125,14 @@ class Configure: def pickle_path(self): return self._pickle_path + configure = Configure() + class TestDBFactory(TestCase): """ - TestDBFactory - """ +TestDBFactory +""" @unittest.skipIf(configure.skip_test, configure.skip_reason) def test_save_application(self): @@ -129,43 +143,29 @@ class TestDBFactory(TestCase): @unittest.skipIf(configure.skip_test, configure.skip_reason) def test_save_city(self): configure.city.name = "Montreal" - saved_city = configure.import_db_factory.persist_city( + city = configure.import_db_factory.persist_city( configure.city, configure.pickle_path, configure.application.id, configure.user.id) - self.assertEqual(saved_city.name, 'Montreal') - self.assertEqual(saved_city.pickle_path, self.pickle_path) - self.assertEqual(saved_city.level_of_detail, self.city.level_of_detail.geometry) - self._db_factory.delete_city(saved_city.id) - - @unittest.skipIf(configure.skip_test, configure.skip_reason) - def test_get_city_by_name(self): - city = self._db_factory.persist_city(self.city, self.pickle_path, self.application.id, self._user.id) - retrieved_city = self._export_db_factory.get_city_by_name(city.name) - self.assertEqual(retrieved_city[0].application_id, 1) - self.assertEqual(retrieved_city[0].user_id, self._user.id) - self._db_factory.delete_city(city.id) - - @unittest.skipIf(configure.skip_test, configure.skip_reason) - def test_get_city_by_user(self): - city = self._import_db_factory.persist_city(self.city, self.pickle_path, self.application.id, self._user.id) - retrieved_city = self._export_db_factory.get_city_by_user(self._user.id) - self.assertEqual(retrieved_city[0].pickle_path, self.pickle_path) - self._db_factory.delete_city(city.id) - - @unittest.skipIf(configure.skip_test, configure.skip_reason) - def test_get_city_by_id(self): - city = self._db_factory.persist_city(self.city, self.pickle_path, self.application.id, self._user.id) - retrieved_city = self._export_db_factory.get_city(city.id) - self.assertEqual(retrieved_city.level_of_detail, self.city.level_of_detail.geometry) - self._db_factory.delete_city(city.id) + self.assertEqual(city.name, 'Montreal') + self.assertEqual(city.pickle_path, configure.pickle_path) + self.assertEqual(city.level_of_detail, configure.city.level_of_detail.geometry) + configure.import_db_factory.delete_city(city.id) @unittest.skipIf(configure.skip_test, configure.skip_reason) def test_get_update_city(self): - city = self._db_factory.persist_city(self.city, self.pickle_path, self.application.id, self._user.id) - self.city.name = "Ottawa" - self._db_factory.update_city(city.id, self.city) - updated_city = self._export_db_factory.get_city(city.id) - self.assertEqual(updated_city.name, self.city.name) - self._db_factory.delete_city(city.id) + city = configure.import_db_factory.persist_city(configure.city, + configure.pickle_path, + configure.application.id, + configure._user.id) + city.name = "Ottawa" + configure.import_db_factory.update_city(city.id, configure.city) + cities = configure.export_db_factory.cities_by_user_and_application( + configure.user.id, + configure.application.id) + for updated_city in cities: + if updated_city.id == city.id: + self.assertEqual(updated_city.name, city.name) + break + configure.import_db_factory.delete_city(city.id) diff --git a/hub/unittests/test_insel_exports.py b/hub/unittests/test_insel_exports.py index 2fbc54c1..30d73e1f 100644 --- a/hub/unittests/test_insel_exports.py +++ b/hub/unittests/test_insel_exports.py @@ -63,6 +63,7 @@ class TestExports(TestCase): :parameter city: city :return: none """ + city.level_of_detail.surface_radiation = 2 for radiation in self._read_sra_file: city_object_name = radiation.columns.values.tolist()[1].split(':')[1] building = city.city_object(city_object_name) diff --git a/hub/unittests/tests_data/pickle_path.bz2 b/hub/unittests/tests_data/pickle_path.bz2 index 8a80bdae96822a0090802b07aa67e395c82927f1..df2daadfcad9e0c37bc0d6ddd17e457fccb857a0 100644 GIT binary patch literal 7062 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