From 1449298a250957aac9f0abee3a0614a777e7f495 Mon Sep 17 00:00:00 2001 From: Vagrant Date: Sun, 24 Nov 2024 06:48:35 +0000 Subject: [PATCH] Update multi-usage methods to work with usage geojson input and parsers --- hub/city_model_structure/building.py | 13 ++++- hub/helpers/parsers/__init__.py | 0 hub/helpers/parsers/list_usage_to_hub.py | 31 ++++++++++++ hub/helpers/parsers/string_usage_to_hub.py | 18 +++++++ hub/helpers/usage_parsers.py | 31 ++++++++++++ .../construction/nrcan_physics_parameters.py | 17 ++----- hub/imports/geometry/geojson.py | 37 ++++++-------- hub/imports/geometry_factory.py | 6 +++ hub/imports/usage/comnet_usage_parameters.py | 47 +++++++---------- hub/imports/usage/nrcan_usage_parameters.py | 50 +++++++------------ setup.py | 3 +- 11 files changed, 153 insertions(+), 100 deletions(-) create mode 100644 hub/helpers/parsers/__init__.py create mode 100644 hub/helpers/parsers/list_usage_to_hub.py create mode 100644 hub/helpers/parsers/string_usage_to_hub.py create mode 100644 hub/helpers/usage_parsers.py diff --git a/hub/city_model_structure/building.py b/hub/city_model_structure/building.py index 5e838791..8a05ec89 100644 --- a/hub/city_model_structure/building.py +++ b/hub/city_model_structure/building.py @@ -36,6 +36,7 @@ class Building(CityObject): self._terrains = terrains self._year_of_construction = year_of_construction self._function = function + self._usages = None self._average_storey_height = None self._storeys_above_ground = None self._floor_area = None @@ -256,7 +257,17 @@ class Building(CityObject): :param value: str """ if value is not None: - self._function = str(value) + self._function = value + + @property + def usages(self) -> Union[None, list]: + """ + Get building function + :return: None or str + """ + if self._usages is None and self._function is not None: + self._usages = [{'usage': self._function, 'ratio': 1 }] + return self._usages @property def average_storey_height(self) -> Union[None, float]: diff --git a/hub/helpers/parsers/__init__.py b/hub/helpers/parsers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hub/helpers/parsers/list_usage_to_hub.py b/hub/helpers/parsers/list_usage_to_hub.py new file mode 100644 index 00000000..d336454b --- /dev/null +++ b/hub/helpers/parsers/list_usage_to_hub.py @@ -0,0 +1,31 @@ + +class ListUsageToHub: + """ + Eilat function to hub function class + """ + + def __init__(self, function_dictionary=None): + self._function_dictionary = function_dictionary + + def _apply_function_dictionary(self, usages): + + function_dictionary = self._function_dictionary + + if function_dictionary is not None: + for usage in usages: + if usage['usage'] in function_dictionary: + usage['usage'] = function_dictionary[usage['usage']] + + return usages + + def parse(self, usages) -> list[dict]: + """ + Get the dictionary + :return: {} + """ + + usages = [{"usage": str(i["usage"]), "ratio": float(i["ratio"])} for i in usages] + + usages = self._apply_function_dictionary(usages) + + return usages diff --git a/hub/helpers/parsers/string_usage_to_hub.py b/hub/helpers/parsers/string_usage_to_hub.py new file mode 100644 index 00000000..ad58fde1 --- /dev/null +++ b/hub/helpers/parsers/string_usage_to_hub.py @@ -0,0 +1,18 @@ + +class StringUsageToHub: + """ + Eilat function to hub function class + """ + + def parse(self, usages) -> list[dict]: + """ + Get the dictionary + :return: {} + """ + + parsed_usages = [] + for usage in usages.split('_'): + usage_dict = {"usage": str(usage.split('-')[0]), "ratio": float(usage.split('-')[1])} + parsed_usages.append(usage_dict) + + return usages diff --git a/hub/helpers/usage_parsers.py b/hub/helpers/usage_parsers.py new file mode 100644 index 00000000..ab07466c --- /dev/null +++ b/hub/helpers/usage_parsers.py @@ -0,0 +1,31 @@ +""" +Dictionaries module saves all transformations of functions and usages to access the catalogs +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 hub.helpers.parsers.list_usage_to_hub import ListUsageToHub +from hub.helpers.parsers.string_usage_to_hub import StringUsageToHub + +class UseageParsers: + """ + Dictionaries class + """ + + @staticmethod + def string_usage_to_hub() -> object: + """ + Hub usage to HfT usage, transformation dictionary + :return: dict + """ + return StringUsageToHub().parse + + @staticmethod + def list_usage_to_hub(function_dictionary=None) -> object: + """ + Hub usage to HfT usage, transformation dictionary + :return: dict + """ + return ListUsageToHub(function_dictionary).parse + diff --git a/hub/imports/construction/nrcan_physics_parameters.py b/hub/imports/construction/nrcan_physics_parameters.py index 35f5382a..c3bcc82e 100644 --- a/hub/imports/construction/nrcan_physics_parameters.py +++ b/hub/imports/construction/nrcan_physics_parameters.py @@ -33,21 +33,10 @@ class NrcanPhysicsParameters: city = self._city nrcan_catalog = ConstructionCatalogFactory('nrcan').catalog for building in city.buildings: - main_function = None - functions = building.function.split('_') - if len(functions) > 1: - maximum_percentage = 0 - for function in functions: - percentage_and_function = function.split('-') - if float(percentage_and_function[0]) > maximum_percentage: - maximum_percentage = float(percentage_and_function[0]) - main_function = percentage_and_function[-1] - else: - main_function = functions[-1] - if main_function not in Dictionaries().hub_function_to_nrcan_construction_function: - logging.error('Building %s has an unknown building function %s', building.name, main_function) + if building.function not in Dictionaries().hub_function_to_nrcan_construction_function: + logging.error('Building %s has an unknown building function %s', building.name, building.function) continue - function = Dictionaries().hub_function_to_nrcan_construction_function[main_function] + 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/geometry/geojson.py b/hub/imports/geometry/geojson.py index 998a2298..26cfead7 100644 --- a/hub/imports/geometry/geojson.py +++ b/hub/imports/geometry/geojson.py @@ -35,6 +35,8 @@ class Geojson: year_of_construction_field=None, function_field=None, function_to_hub=None, + usages_field=None, + usages_to_hub=None, hub_crs=None ): self._hub_crs = hub_crs @@ -52,6 +54,8 @@ class Geojson: self._year_of_construction_field = year_of_construction_field self._function_field = function_field self._function_to_hub = function_to_hub + self._usages_field = usages_field + self._usages_to_hub = usages_to_hub with open(path, 'r', encoding='utf8') as json_file: self._geojson = json.loads(json_file.read()) @@ -117,41 +121,30 @@ class Geojson: 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 = 1 self._max_z = max(self._max_z, extrusion_height) year_of_construction = None + if self._year_of_construction_field is not None: year_of_construction = int(feature['properties'][self._year_of_construction_field]) + function = None if self._function_field is not None: function = str(feature['properties'][self._function_field]) - if function == 'Mixed use' or function == 'mixed use': - function_parts = [] - if 'usages' in feature['properties']: - usages = feature['properties']['usages'] - for usage in usages: - if self._function_to_hub is not None and usage['usage'] in self._function_to_hub: - function_parts.append(f"{usage['percentage']}-{self._function_to_hub[usage['usage']]}") - else: - function_parts.append(f"{usage['percentage']}-{usage['usage']}") - else: - for key, value in feature['properties'].items(): - if key.startswith("mixed_type_") and not key.endswith("_percentage"): - type_key = key - percentage_key = f"{key}_percentage" - if percentage_key in feature['properties']: - if self._function_to_hub is not None and feature['properties'][type_key] in self._function_to_hub: - usage_function = self._function_to_hub[feature['properties'][type_key]] - function_parts.append(f"{feature['properties'][percentage_key]}-{usage_function}") - else: - function_parts.append(f"{feature['properties'][percentage_key]}-{feature['properties'][type_key]}") - function = "_".join(function_parts) if self._function_to_hub is not None: - # use the transformation dictionary to retrieve the proper function if function in self._function_to_hub: function = self._function_to_hub[function] + + usages = None + if self._usages_field is not None: + if self._usages_field in feature['properties']: + usages = feature['properties'][self._usages_field] + if self._usages_to_hub is not None: + usages = self._usages_to_hub(usages) + geometry = feature['geometry'] building_aliases = [] if 'id' in feature: diff --git a/hub/imports/geometry_factory.py b/hub/imports/geometry_factory.py index 21dba708..de8ec318 100644 --- a/hub/imports/geometry_factory.py +++ b/hub/imports/geometry_factory.py @@ -23,6 +23,8 @@ class GeometryFactory: year_of_construction_field=None, function_field=None, function_to_hub=None, + usages_field=None, + usages_to_hub=None, hub_crs=None): self._file_type = '_' + file_type.lower() validate_import_export_type(GeometryFactory, file_type) @@ -32,6 +34,8 @@ class GeometryFactory: self._year_of_construction_field = year_of_construction_field self._function_field = function_field self._function_to_hub = function_to_hub + self._usages_field = usages_field + self._usages_to_hub = usages_to_hub self._hub_crs = hub_crs @property @@ -66,6 +70,8 @@ class GeometryFactory: self._year_of_construction_field, self._function_field, self._function_to_hub, + self._usages_field, + self._usages_to_hub, self._hub_crs).city @property diff --git a/hub/imports/usage/comnet_usage_parameters.py b/hub/imports/usage/comnet_usage_parameters.py index a3a6a613..adf65aed 100644 --- a/hub/imports/usage/comnet_usage_parameters.py +++ b/hub/imports/usage/comnet_usage_parameters.py @@ -38,38 +38,36 @@ class ComnetUsageParameters: city = self._city comnet_catalog = UsageCatalogFactory('comnet').catalog for building in city.buildings: - usages = [] comnet_archetype_usages = [] - building_functions = building.function.split('_') - for function in building_functions: - usages.append(function.split('-')) + usages = building.usages for usage in usages: - comnet_usage_name = Dictionaries().hub_usage_to_comnet_usage[usage[-1]] + comnet_usage_name = Dictionaries().hub_usage_to_comnet_usage[usage['usage']] try: comnet_archetype_usage = self._search_archetypes(comnet_catalog, comnet_usage_name) comnet_archetype_usages.append(comnet_archetype_usage) except KeyError: logging.error('Building %s has unknown usage archetype for usage %s', building.name, comnet_usage_name) continue + for (i, internal_zone) in enumerate(building.internal_zones): internal_zone_usages = [] if len(building.internal_zones) > 1: volume_per_area = 0 if internal_zone.area is None: logging.error('Building %s has internal zone area not defined, ACH cannot be calculated for usage %s', - building.name, usages[i][-1]) + building.name, usages[i]['usage']) continue if internal_zone.volume is None: logging.error('Building %s has internal zone volume not defined, ACH cannot be calculated for usage %s', - building.name, usages[i][-1]) + building.name, usages[i]['usage']) continue if internal_zone.area <= 0: logging.error('Building %s has internal zone area equal to 0, ACH cannot be calculated for usage %s', - building.name, usages[i][-1]) + building.name, usages[i]['usage']) continue volume_per_area += internal_zone.volume / internal_zone.area usage = Usage() - usage.name = usages[i][-1] + usage.name = usages[i]['usage'] self._assign_values(usage, comnet_archetype_usages[i], volume_per_area, building.cold_water_temperature) usage.percentage = 1 self._calculate_reduced_values_from_extended_library(usage, comnet_archetype_usages[i]) @@ -82,13 +80,11 @@ class ComnetUsageParameters: 'ground', building.name, usages, building.year_of_construction) storeys_above_ground = self.average_storey_height_calculator(self._city, building) volume_per_area = building.volume / building.floor_area / storeys_above_ground - for (j, mixed_usage) in enumerate(usages): + for j, usage_type in enumerate(usages): usage = Usage() - usage.name = mixed_usage[-1] - if len(usages) > 1: - usage.percentage = float(mixed_usage[0]) / 100 - else: - usage.percentage = 1 + usage.name = usage_type['usage'] + usage.percentage = float(usage_type['ratio']) + self._assign_values(usage, comnet_archetype_usages[j], volume_per_area, building.cold_water_temperature) self._calculate_reduced_values_from_extended_library(usage, comnet_archetype_usages[j]) internal_zone_usages.append(usage) @@ -270,20 +266,11 @@ class ComnetUsageParameters: def average_storey_height_calculator(city, building): climate_zone = ConstructionHelper.city_to_nrcan_climate_zone(city.climate_reference_city) nrcan_catalog = ConstructionCatalogFactory('nrcan').catalog - main_function = None - functions = building.function.split('_') - if len(functions) > 1: - maximum_percentage = 0 - for function in functions: - percentage_and_function = function.split('-') - if float(percentage_and_function[0]) > maximum_percentage: - maximum_percentage = float(percentage_and_function[0]) - main_function = percentage_and_function[-1] - else: - main_function = functions[-1] - if main_function not in Dictionaries().hub_function_to_nrcan_construction_function: - logging.error('Building %s has an unknown building function %s', building.name, main_function) - function = Dictionaries().hub_function_to_nrcan_construction_function[main_function] + + if building.function not in Dictionaries().hub_function_to_nrcan_construction_function: + logging.error('Building %s has an unknown building function %s', building.name, building.function) + + function = Dictionaries().hub_function_to_nrcan_construction_function[building.function] construction_archetype = None average_storey_height = None nrcan_archetypes = nrcan_catalog.entries('archetypes') @@ -298,4 +285,4 @@ class ComnetUsageParameters: '[%s], building year of construction: %s and climate zone %s', building.name, function, building.function, building.year_of_construction, climate_zone) - return average_storey_height \ No newline at end of file + return average_storey_height diff --git a/hub/imports/usage/nrcan_usage_parameters.py b/hub/imports/usage/nrcan_usage_parameters.py index 854d7051..f9cf5e63 100644 --- a/hub/imports/usage/nrcan_usage_parameters.py +++ b/hub/imports/usage/nrcan_usage_parameters.py @@ -37,21 +37,18 @@ class NrcanUsageParameters: nrcan_catalog = UsageCatalogFactory('nrcan').catalog comnet_catalog = UsageCatalogFactory('comnet').catalog for building in city.buildings: - usages = [] nrcan_archetype_usages = [] comnet_archetype_usages = [] - building_functions = building.function.split('_') - for function in building_functions: - usages.append(function.split('-')) + usages = building.usages for usage in usages: - usage_name = Dictionaries().hub_usage_to_nrcan_usage[usage[-1]] + usage_name = Dictionaries().hub_usage_to_nrcan_usage[usage['usage']] try: archetype_usage = self._search_archetypes(nrcan_catalog, usage_name) nrcan_archetype_usages.append(archetype_usage) except KeyError: logging.error('Building %s has unknown usage archetype for usage %s', building.name, usage_name) continue - comnet_usage_name = Dictionaries().hub_usage_to_comnet_usage[usage[-1]] + comnet_usage_name = Dictionaries().hub_usage_to_comnet_usage[usage['usage']] try: comnet_archetype_usage = self._search_archetypes(comnet_catalog, comnet_usage_name) comnet_archetype_usages.append(comnet_archetype_usage) @@ -65,19 +62,19 @@ class NrcanUsageParameters: volume_per_area = 0 if internal_zone.area is None: logging.error('Building %s has internal zone area not defined, ACH cannot be calculated for usage %s', - building.name, usages[i][-1]) + building.name, usages[i]['usage']) continue if internal_zone.volume is None: logging.error('Building %s has internal zone volume not defined, ACH cannot be calculated for usage %s', - building.name, usages[i][-1]) + building.name, usages[i]['usage']) continue if internal_zone.area <= 0: logging.error('Building %s has internal zone area equal to 0, ACH cannot be calculated for usage %s', - building.name, usages[i][-1]) + building.name, usages[i]['usage']) continue volume_per_area += internal_zone.volume / internal_zone.area usage = Usage() - usage.name = usages[i][-1] + usage.name = usages[i]['usage'] self._assign_values(usage, nrcan_archetype_usages[i], volume_per_area, building.cold_water_temperature) self._assign_comnet_extra_values(usage, comnet_archetype_usages[i], nrcan_archetype_usages[i].occupancy.occupancy_density) usage.percentage = 1 @@ -86,19 +83,17 @@ class NrcanUsageParameters: else: storeys_above_ground = building.storeys_above_ground if storeys_above_ground is None: - logging.error('Building %s no number of storeys assigned, ACH cannot be calculated for usage %s. ' + logging.error('Building %s no number of storeys assigned, ACH cannot be calculated for function %s. ' 'NRCAN construction data for the year %s is used to calculated number of storeys above ' - 'ground', building.name, usages, building.year_of_construction) + 'ground', building.name, building.function, building.year_of_construction) storeys_above_ground = self.average_storey_height_calculator(self._city, building) continue volume_per_area = building.volume / building.floor_area / storeys_above_ground - for (j, mixed_usage) in enumerate(usages): + for j, usage_type in enumerate(usages): usage = Usage() - usage.name = mixed_usage[-1] - if len(usages) > 1: - usage.percentage = float(mixed_usage[0]) / 100 - else: - usage.percentage = 1 + usage.name = usage_type['usage'] + usage.percentage = float(usage_type['ratio']) + self._assign_values(usage, nrcan_archetype_usages[j], volume_per_area, building.cold_water_temperature) self._assign_comnet_extra_values(usage, comnet_archetype_usages[j], nrcan_archetype_usages[j].occupancy.occupancy_density) self._calculate_reduced_values_from_extended_library(usage, nrcan_archetype_usages[j]) @@ -227,20 +222,11 @@ class NrcanUsageParameters: def average_storey_height_calculator(city, building): climate_zone = ConstructionHelper.city_to_nrcan_climate_zone(city.climate_reference_city) nrcan_catalog = ConstructionCatalogFactory('nrcan').catalog - main_function = None - functions = building.function.split('_') - if len(functions) > 1: - maximum_percentage = 0 - for function in functions: - percentage_and_function = function.split('-') - if float(percentage_and_function[0]) > maximum_percentage: - maximum_percentage = float(percentage_and_function[0]) - main_function = percentage_and_function[-1] - else: - main_function = functions[-1] - if main_function not in Dictionaries().hub_function_to_nrcan_construction_function: - logging.error('Building %s has an unknown building function %s', building.name, main_function) - function = Dictionaries().hub_function_to_nrcan_construction_function[main_function] + + if building.function not in Dictionaries().hub_function_to_nrcan_construction_function: + logging.error('Building %s has an unknown building function %s', building.name, building.function) + + function = Dictionaries().hub_function_to_nrcan_construction_function[building.function] construction_archetype = None average_storey_height = None nrcan_archetypes = nrcan_catalog.entries('archetypes') diff --git a/setup.py b/setup.py index 7c2df9ca..45968c30 100644 --- a/setup.py +++ b/setup.py @@ -65,6 +65,7 @@ setup( 'hub.helpers', 'hub.helpers.peak_calculation', 'hub.helpers.data', + 'hub.helpers.parsers', 'hub.imports', 'hub.imports.construction', 'hub.imports.construction.helpers', @@ -109,4 +110,4 @@ setup( ('hub/exports/building_energy/idf_files', glob.glob('hub/exports/building_energy/idf_files/*.idd')) ], -) \ No newline at end of file +)