From fae8ba80229de5af42e84be7c24fba59f77b887e Mon Sep 17 00:00:00 2001 From: Pilar Date: Tue, 8 Mar 2022 19:19:52 -0500 Subject: [PATCH 01/19] fixed bug saving values --- catalogs/catalog.py | 33 - .../ecore_greenery/greenerycatalog.ecore | 268 ------ .../ecore_greenery/greenerycatalog.py | 318 ------- .../greenerycatalog_no_quantities.ecore | 268 ------ catalogs/greenery/greenery_catalog.py | 41 - catalogs/greenery_catalog_factory.py | 44 - city_model_structure/attributes/polygon.py | 49 +- city_model_structure/attributes/polyhedron.py | 2 +- city_model_structure/building.py | 112 ++- .../building_demand/appliances.py | 103 +++ .../building_demand/internal_gains.py | 18 + .../building_demand/lighting.py | 103 +++ .../building_demand/material.py | 164 +++- .../building_demand/occupancy.py | 121 +++ .../{occupants.py => occupant.py} | 53 +- .../building_demand/storey.py | 23 +- .../building_demand/surface.py | 3 + .../building_demand/thermal_boundary.py | 140 ++-- .../building_demand/thermal_control.py | 86 ++ .../building_demand/thermal_opening.py | 15 +- .../building_demand/thermal_zone.py | 110 +-- .../building_demand/usage_zone.py | 290 ++----- city_model_structure/city.py | 56 +- city_model_structure/city_object.py | 10 +- .../energy_systems/hvac_system.py | 32 + .../iot/concordia_energy_sensor.py | 45 + .../iot/concordia_gas_flow_sensor.py | 45 + .../iot/concordia_temperature_sensor.py | 45 + city_model_structure/iot/sensor.py | 81 +- city_model_structure/iot/sensor_measure.py | 40 - city_model_structure/iot/sensor_type.py | 20 - city_model_structure/iot/station.py | 41 - city_model_structure/lca_calculations.py | 23 + city_model_structure/lca_material.py | 242 ------ config/configuration.ini | 2 + .../construction/ca_constructions_reduced.xml | 32 - data/construction/us_constructions.xml | 38 +- data/greenery/ecore_greenery_catalog.xml | 59 -- data/life_cycle_assessment/lca_data.xml | 778 +++++++++--------- exports/formats/energy_ade.py | 2 +- helpers/configuration_helper.py | 16 + helpers/constants.py | 15 +- helpers/monthly_to_hourly_demand.py | 6 +- imports/construction/ca_physics_parameters.py | 19 +- ...lding_achetype.py => building_achetype.py} | 10 +- ..._layer_archetype.py => layer_archetype.py} | 38 +- .../nrel_thermal_boundary_archetype.py | 99 --- .../thermal_boundary_archetype.py | 136 +++ ...hetype.py => thermal_opening_archetype.py} | 70 +- .../helpers/storeys_generation.py | 15 +- .../construction/nrel_physics_interface.py | 25 +- imports/construction/us_physics_parameters.py | 24 +- .../sanam_customized_usage_parameters.py | 2 +- imports/geometry/citygml.py | 2 +- imports/geometry/obj.py | 2 +- imports/geometry/rhino.py | 27 +- imports/life_cycle_assessment/lca_machine.py | 14 +- imports/life_cycle_assessment/lca_material.py | 35 +- imports/life_cycle_assessment/lca_vehicle.py | 6 +- .../sensors/concordia_energy_consumption.py | 5 +- imports/sensors/concordia_gas_flow.py | 4 +- imports/sensors/concordia_temperature.py | 3 +- imports/usage/ca_usage_parameters.py | 4 +- imports/usage/comnet_usage_parameters.py | 87 +- .../data_classes/hft_usage_zone_archetype.py | 144 ---- .../data_classes/usage_zone_archetype.py | 98 +++ imports/usage/hft_usage_interface.py | 4 +- imports/usage/hft_usage_parameters.py | 4 +- requirements.txt | 2 +- unittests/test_construction_factory.py | 237 ++++-- unittests/test_geometry_factory.py | 245 ++---- .../test_life_cycle_assessment_factory.py | 6 +- unittests/test_schedules_factory.py | 1 + unittests/test_sensors_factory.py | 4 +- unittests/test_usage_factory.py | 122 ++- 75 files changed, 2436 insertions(+), 3050 deletions(-) delete mode 100644 catalogs/catalog.py delete mode 100644 catalogs/greenery/ecore_greenery/greenerycatalog.ecore delete mode 100644 catalogs/greenery/ecore_greenery/greenerycatalog.py delete mode 100644 catalogs/greenery/ecore_greenery/greenerycatalog_no_quantities.ecore delete mode 100644 catalogs/greenery/greenery_catalog.py delete mode 100644 catalogs/greenery_catalog_factory.py create mode 100644 city_model_structure/building_demand/appliances.py create mode 100644 city_model_structure/building_demand/lighting.py create mode 100644 city_model_structure/building_demand/occupancy.py rename city_model_structure/building_demand/{occupants.py => occupant.py} (79%) create mode 100644 city_model_structure/building_demand/thermal_control.py create mode 100644 city_model_structure/energy_systems/hvac_system.py create mode 100644 city_model_structure/iot/concordia_energy_sensor.py create mode 100644 city_model_structure/iot/concordia_gas_flow_sensor.py create mode 100644 city_model_structure/iot/concordia_temperature_sensor.py delete mode 100644 city_model_structure/iot/sensor_measure.py delete mode 100644 city_model_structure/iot/sensor_type.py delete mode 100644 city_model_structure/iot/station.py create mode 100644 city_model_structure/lca_calculations.py delete mode 100644 city_model_structure/lca_material.py delete mode 100644 data/greenery/ecore_greenery_catalog.xml rename imports/construction/data_classes/{nrel_building_achetype.py => building_achetype.py} (89%) rename imports/construction/data_classes/{nrel_layer_archetype.py => layer_archetype.py} (67%) delete mode 100644 imports/construction/data_classes/nrel_thermal_boundary_archetype.py create mode 100644 imports/construction/data_classes/thermal_boundary_archetype.py rename imports/construction/data_classes/{nrel_thermal_opening_archetype.py => thermal_opening_archetype.py} (56%) delete mode 100644 imports/usage/data_classes/hft_usage_zone_archetype.py create mode 100644 imports/usage/data_classes/usage_zone_archetype.py diff --git a/catalogs/catalog.py b/catalogs/catalog.py deleted file mode 100644 index ed272383..00000000 --- a/catalogs/catalog.py +++ /dev/null @@ -1,33 +0,0 @@ -""" -Catalog base class -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2022 Project Author Guille Gutierrez guillermo.gutierrezmorote@concordia.ca -""" - -class Catalog: - """ - Catalog name - """ - - @property - def names(self): - """ - Base property to return the catalog entries names - :return: not implemented error - """ - raise NotImplementedError - - @property - def entries(self): - """ - Base property to return the catalog entries - :return: not implemented error - """ - raise NotImplementedError - - def get_entry(self, name): - """ - Base property to return the catalog entry matching the given name - :return: not implemented error - """ - raise NotImplementedError diff --git a/catalogs/greenery/ecore_greenery/greenerycatalog.ecore b/catalogs/greenery/ecore_greenery/greenerycatalog.ecore deleted file mode 100644 index 5c3bb85d..00000000 --- a/catalogs/greenery/ecore_greenery/greenerycatalog.ecore +++ /dev/null @@ -1,268 +0,0 @@ - - - - -
- - - - -
- - - - -
- - -
-
- - - - -
- - -
-
- - - - -
- - -
-
- - - - -
- - -
-
- - - - -
- - -
-
- - - - -
- - -
-
- - - - -
- - -
-
- - - - -
- - -
-
- - - - -
- - -
-
- - - - - - - -
- - -
-
- - - - -
- - -
-
- - - - -
- - -
-
- - - - -
- - -
-
- - - - -
- - -
-
- - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - -
- - - - -
-
- - - - - - - - - -
- - - - - - - - - - - - - -
-
- - - - - - - - - - - - - diff --git a/catalogs/greenery/ecore_greenery/greenerycatalog.py b/catalogs/greenery/ecore_greenery/greenerycatalog.py deleted file mode 100644 index 30ac116c..00000000 --- a/catalogs/greenery/ecore_greenery/greenerycatalog.py +++ /dev/null @@ -1,318 +0,0 @@ -"""Definition of meta model 'greenerycatalog'.""" -from functools import partial -import pyecore.ecore as Ecore -from pyecore.ecore import * - - -name = 'greenerycatalog' -nsURI = 'http://ca.concordia/greenerycatalog' -nsPrefix = 'greenery' - -eClass = EPackage(name=name, nsURI=nsURI, nsPrefix=nsPrefix) - -eClassifiers = {} -getEClassifier = partial(Ecore.getEClassifier, searchspace=eClassifiers) -Management = EEnum('Management', literals=['Intensive', 'Extensive', 'SemiIntensive', 'NA']) - -Roughness = EEnum('Roughness', literals=['VeryRough', 'Rough', - 'MediumRough', 'MediumSmooth', 'Smooth', 'VerySmooth']) - - -class Soil(EObject, metaclass=MetaEClass): - - name = EAttribute(eType=EString, unique=True, derived=False, changeable=True) - roughness = EAttribute(eType=Roughness, unique=True, derived=False, - changeable=True, default_value=Roughness.MediumRough) - conductivityOfDrySoil = EAttribute( - eType=EString, unique=True, derived=False, changeable=True, default_value='1.0 W/(m*K)') - densityOfDrySoil = EAttribute(eType=EString, unique=True, derived=False, - changeable=True, default_value='1100 kg/m³') - specificHeatOfDrySoil = EAttribute( - eType=EString, unique=True, derived=False, changeable=True, default_value='1200 J/(kg*K)') - thermalAbsorptance = EAttribute(eType=EString, unique=True, - derived=False, changeable=True, default_value='0.9') - solarAbsorptance = EAttribute(eType=EString, unique=True, - derived=False, changeable=True, default_value='0.7') - visibleAbsorptance = EAttribute(eType=EString, unique=True, - derived=False, changeable=True, default_value='0.75') - saturationVolumetricMoistureContent = EAttribute( - eType=EString, unique=True, derived=False, changeable=True, default_value='0.0') - residualVolumetricMoistureContent = EAttribute( - eType=EString, unique=True, derived=False, changeable=True, default_value='0.05') - initialVolumetricMoistureContent = EAttribute( - eType=EString, unique=True, derived=False, changeable=True, default_value='0.1') - - def __init__(self, *, name=None, roughness=None, conductivityOfDrySoil=None, densityOfDrySoil=None, specificHeatOfDrySoil=None, thermalAbsorptance=None, solarAbsorptance=None, visibleAbsorptance=None, saturationVolumetricMoistureContent=None, residualVolumetricMoistureContent=None, initialVolumetricMoistureContent=None): - # if kwargs: - # raise AttributeError('unexpected arguments: {}'.format(kwargs)) - - super().__init__() - - if name is not None: - self.name = name - - if roughness is not None: - self.roughness = roughness - - if conductivityOfDrySoil is not None: - self.conductivityOfDrySoil = conductivityOfDrySoil - - if densityOfDrySoil is not None: - self.densityOfDrySoil = densityOfDrySoil - - if specificHeatOfDrySoil is not None: - self.specificHeatOfDrySoil = specificHeatOfDrySoil - - if thermalAbsorptance is not None: - self.thermalAbsorptance = thermalAbsorptance - - if solarAbsorptance is not None: - self.solarAbsorptance = solarAbsorptance - - if visibleAbsorptance is not None: - self.visibleAbsorptance = visibleAbsorptance - - if saturationVolumetricMoistureContent is not None: - self.saturationVolumetricMoistureContent = saturationVolumetricMoistureContent - - if residualVolumetricMoistureContent is not None: - self.residualVolumetricMoistureContent = residualVolumetricMoistureContent - - if initialVolumetricMoistureContent is not None: - self.initialVolumetricMoistureContent = initialVolumetricMoistureContent - - -class Plant(EObject, metaclass=MetaEClass): - - name = EAttribute(eType=EString, unique=True, derived=False, changeable=True) - height = EAttribute(eType=EString, unique=True, derived=False, - changeable=True, default_value='0.1 m') - leafAreaIndex = EAttribute(eType=EString, unique=True, derived=False, - changeable=True, default_value='2.5') - leafReflectivity = EAttribute(eType=EString, unique=True, - derived=False, changeable=True, default_value='0.1') - leafEmissivity = EAttribute(eType=EString, unique=True, derived=False, - changeable=True, default_value='0.9') - minimalStomatalResistance = EAttribute( - eType=EString, unique=True, derived=False, changeable=True, default_value='100.0 s/m') - co2Sequestration = EAttribute(eType=EString, unique=True, derived=False, - changeable=True, default_value='kgCO₂eq') - growsOn = EReference(ordered=True, unique=True, containment=False, derived=False, upper=-1) - - def __init__(self, *, name=None, height=None, leafAreaIndex=None, leafReflectivity=None, leafEmissivity=None, minimalStomatalResistance=None, growsOn=None, co2Sequestration=None): - # if kwargs: - # raise AttributeError('unexpected arguments: {}'.format(kwargs)) - - super().__init__() - - if name is not None: - self.name = name - - if height is not None: - self.height = height - - if leafAreaIndex is not None: - self.leafAreaIndex = leafAreaIndex - - if leafReflectivity is not None: - self.leafReflectivity = leafReflectivity - - if leafEmissivity is not None: - self.leafEmissivity = leafEmissivity - - if minimalStomatalResistance is not None: - self.minimalStomatalResistance = minimalStomatalResistance - - if co2Sequestration is not None: - self.co2Sequestration = co2Sequestration - - if growsOn: - self.growsOn.extend(growsOn) - - -class SupportEnvelope(EObject, metaclass=MetaEClass): - - roughness = EAttribute(eType=Roughness, unique=True, derived=False, - changeable=True, default_value=Roughness.MediumRough) - solarAbsorptance = EAttribute(eType=EDouble, unique=True, - derived=False, changeable=True, default_value=0.0) - conductivity = EAttribute(eType=EDouble, unique=True, derived=False, - changeable=True, default_value=0.0) - visibleAbsorptance = EAttribute(eType=EDouble, unique=True, - derived=False, changeable=True, default_value=0.0) - specificHeat = EAttribute(eType=EDouble, unique=True, derived=False, - changeable=True, default_value=0.0) - density = EAttribute(eType=EDouble, unique=True, derived=False, - changeable=True, default_value=0.0) - thermalAbsorptance = EAttribute(eType=EDouble, unique=True, - derived=False, changeable=True, default_value=0.0) - - def __init__(self, *, roughness=None, solarAbsorptance=None, conductivity=None, visibleAbsorptance=None, specificHeat=None, density=None, thermalAbsorptance=None): - # if kwargs: - # raise AttributeError('unexpected arguments: {}'.format(kwargs)) - - super().__init__() - - if roughness is not None: - self.roughness = roughness - - if solarAbsorptance is not None: - self.solarAbsorptance = solarAbsorptance - - if conductivity is not None: - self.conductivity = conductivity - - if visibleAbsorptance is not None: - self.visibleAbsorptance = visibleAbsorptance - - if specificHeat is not None: - self.specificHeat = specificHeat - - if density is not None: - self.density = density - - if thermalAbsorptance is not None: - self.thermalAbsorptance = thermalAbsorptance - - -class GreeneryCatalog(EObject, metaclass=MetaEClass): - - name = EAttribute(eType=EString, unique=True, derived=False, changeable=True) - description = EAttribute(eType=EString, unique=True, derived=False, changeable=True) - source = EAttribute(eType=EString, unique=True, derived=False, changeable=True) - plantCategories = EReference(ordered=True, unique=True, - containment=True, derived=False, upper=-1) - vegetationCategories = EReference(ordered=True, unique=True, - containment=True, derived=False, upper=-1) - soils = EReference(ordered=True, unique=True, containment=True, derived=False, upper=-1) - - def __init__(self, *, name=None, description=None, source=None, plantCategories=None, vegetationCategories=None, soils=None): - # if kwargs: - # raise AttributeError('unexpected arguments: {}'.format(kwargs)) - - super().__init__() - - if name is not None: - self.name = name - - if description is not None: - self.description = description - - if source is not None: - self.source = source - - if plantCategories: - self.plantCategories.extend(plantCategories) - - if vegetationCategories: - self.vegetationCategories.extend(vegetationCategories) - - if soils: - self.soils.extend(soils) - - -class PlantCategory(EObject, metaclass=MetaEClass): - """Excluding (that is non-overlapping) categories like Trees, Hedeges, Grasses that help users finding a specific biol. plant species.""" - name = EAttribute(eType=EString, unique=True, derived=False, changeable=True) - plants = EReference(ordered=True, unique=True, containment=True, derived=False, upper=-1) - - def __init__(self, *, name=None, plants=None): - # if kwargs: - # raise AttributeError('unexpected arguments: {}'.format(kwargs)) - - super().__init__() - - if name is not None: - self.name = name - - if plants: - self.plants.extend(plants) - - -class IrrigationSchedule(EObject, metaclass=MetaEClass): - - name = EAttribute(eType=EString, unique=True, derived=False, changeable=True) - - def __init__(self, *, name=None): - # if kwargs: - # raise AttributeError('unexpected arguments: {}'.format(kwargs)) - - super().__init__() - - if name is not None: - self.name = name - - -class Vegetation(EObject, metaclass=MetaEClass): - """Plant life or total plant cover (as of an area)""" - name = EAttribute(eType=EString, unique=True, derived=False, changeable=True) - thicknessOfSoil = EAttribute(eType=EString, unique=True, derived=False, - changeable=True, default_value='20 cm') - management = EAttribute(eType=Management, unique=True, derived=False, - changeable=True, default_value=Management.NA) - airGap = EAttribute(eType=EString, unique=True, derived=False, - changeable=True, default_value='0.0 cm') - soil = EReference(ordered=True, unique=True, containment=False, derived=False) - plants = EReference(ordered=True, unique=True, containment=True, derived=False, upper=-1) - - def __init__(self, *, name=None, thicknessOfSoil=None, soil=None, plants=None, management=None, airGap=None): - # if kwargs: - # raise AttributeError('unexpected arguments: {}'.format(kwargs)) - - super().__init__() - - if name is not None: - self.name = name - - if thicknessOfSoil is not None: - self.thicknessOfSoil = thicknessOfSoil - - if management is not None: - self.management = management - - if airGap is not None: - self.airGap = airGap - - if soil is not None: - self.soil = soil - - if plants: - self.plants.extend(plants) - - -class VegetationCategory(EObject, metaclass=MetaEClass): - """Excluding (that is non-overlapping) categories to help users finding a specific vegetation template.""" - name = EAttribute(eType=EString, unique=True, derived=False, changeable=True) - vegetationTemplates = EReference(ordered=True, unique=True, - containment=True, derived=False, upper=-1) - - def __init__(self, *, vegetationTemplates=None, name=None): - # if kwargs: - # raise AttributeError('unexpected arguments: {}'.format(kwargs)) - - super().__init__() - - if name is not None: - self.name = name - - if vegetationTemplates: - self.vegetationTemplates.extend(vegetationTemplates) - - -class PlantPercentage(EObject, metaclass=MetaEClass): - - percentage = EAttribute(eType=EString, unique=True, derived=False, - changeable=True, default_value='100') - plant = EReference(ordered=True, unique=True, containment=False, derived=False) - - def __init__(self, *, percentage=None, plant=None): - # if kwargs: - # raise AttributeError('unexpected arguments: {}'.format(kwargs)) - - super().__init__() - - if percentage is not None: - self.percentage = percentage - - if plant is not None: - self.plant = plant diff --git a/catalogs/greenery/ecore_greenery/greenerycatalog_no_quantities.ecore b/catalogs/greenery/ecore_greenery/greenerycatalog_no_quantities.ecore deleted file mode 100644 index db58a9c0..00000000 --- a/catalogs/greenery/ecore_greenery/greenerycatalog_no_quantities.ecore +++ /dev/null @@ -1,268 +0,0 @@ - - - - -
- - - - -
- - - - -
- - -
-
- - - - -
- - -
-
- - - - -
- - -
-
- - - - -
- - -
-
- - - - -
- - -
-
- - - - -
- - -
-
- - - - -
- - -
-
- - - - -
- - -
-
- - - - -
- - -
-
- - - - - - - -
- - -
-
- - - - -
- - -
-
- - - - -
- - -
-
- - - - -
- - -
-
- - - - -
- - -
-
- - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - -
- - - - -
-
- - - - - - - - - -
- - - - - - - - - - - - - -
-
- - - - - - - - - - - - - diff --git a/catalogs/greenery/greenery_catalog.py b/catalogs/greenery/greenery_catalog.py deleted file mode 100644 index 3ede5ab9..00000000 --- a/catalogs/greenery/greenery_catalog.py +++ /dev/null @@ -1,41 +0,0 @@ -""" -Greenery catalog -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2022 Project Author Guille Gutierrez guillermo.gutierrezmorote@concordia.ca -""" - -from pyecore.resources import ResourceSet, URI -from catalogs.greenery.ecore_greenery.greenerycatalog import GreeneryCatalog as gc -from catalogs.catalog import Catalog -from pathlib import Path - - -class GreeneryCatalog(Catalog): - - def __init__(self, path): - base_path = Path(Path(__file__).parent / 'ecore_greenery' / 'greenerycatalog_no_quantities.ecore') - resource_set = ResourceSet() - data_model = resource_set.get_resource(URI(str(base_path))) - data_model_root = data_model.contents[0] - resource_set.metamodel_registry[data_model_root.nsURI] = data_model_root - resource = resource_set.get_resource(URI(str(path))) - catalog_data: gc = resource.contents[0] - self._data = {'vegetation': []} - vegetation = [] - for vegetation_category in catalog_data.vegetationCategories: - vegetation.append({vegetation_category.name: []}) - self._data['vegetation'] = vegetation - - @property - def names(self): - """ - :parm: - """ - _names = [] - for category in self._data: - for value in self._data[category]: - for key in value.keys(): - _names.append(key) - return _names - - diff --git a/catalogs/greenery_catalog_factory.py b/catalogs/greenery_catalog_factory.py deleted file mode 100644 index 8e7dfe29..00000000 --- a/catalogs/greenery_catalog_factory.py +++ /dev/null @@ -1,44 +0,0 @@ -""" -Greenery catalog publish the greenery information -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2022 Project Author Guille Gutierrez guillermo.gutierrezmorote@concordia.ca -""" - -from pathlib import Path -from catalogs.greenery.greenery_catalog import GreeneryCatalog - - -class GreeneryCatalogFactory: - """ - GeometryFactory class - """ - def __init__(self, file_type, base_path=None): - if base_path is None: - base_path = Path(Path(__file__).parent.parent / 'data/greenery') - self._file_type = '_' + file_type.lower() - self._path = base_path - - @property - def _nrel(self) -> GreeneryCatalog: - """ - Return a greenery catalog using ecore as datasource - :return: GreeneryCatalog - """ - print('greenery') - return GreeneryCatalog((self._path / 'ecore_greenery_catalog.xml').resolve()) - - @property - def catalog(self) -> GreeneryCatalog: - """ - Enrich the city given to the class using the class given handler - :return: City - """ - return getattr(self, self._file_type, lambda: None) - - @property - def catalog_debug(self) -> GreeneryCatalog: - """ - Enrich the city given to the class using the class given handler - :return: City - """ - return GreeneryCatalog((self._path / 'ecore_greenery_catalog.xml').resolve()) \ No newline at end of file diff --git a/city_model_structure/attributes/polygon.py b/city_model_structure/attributes/polygon.py index cdfd56da..126e9ae4 100644 --- a/city_model_structure/attributes/polygon.py +++ b/city_model_structure/attributes/polygon.py @@ -16,6 +16,7 @@ from city_model_structure.attributes.plane import Plane from city_model_structure.attributes.point import Point import helpers.constants as cte + class Polygon: """ Polygon class @@ -78,6 +79,48 @@ class Polygon: z = vector_0[2] * vector_1[2] return x+y+z + def contains_point(self, point): + """ + Determines if the given point is contained by the current polygon + :return: boolean + """ + # fixme: This method doesn't seems to work. + n = len(self.vertices) + angle_sum = 0 + for i in range(0, n): + vector_0 = self.vertices[i] + vector_1 = self.vertices[(i+1) % n] + # set to origin + vector_0[0] = vector_0[0] - point.coordinates[0] + vector_0[1] = vector_0[1] - point.coordinates[1] + vector_0[2] = vector_0[2] - point.coordinates[2] + vector_1[0] = vector_1[0] - point.coordinates[0] + vector_1[1] = vector_1[1] - point.coordinates[1] + vector_1[2] = vector_1[2] - point.coordinates[2] + module = Polygon._module(vector_0) * Polygon._module(vector_1) + + scalar_product = Polygon._scalar_product(vector_0, vector_1) + angle = np.pi/2 + if module != 0: + angle = abs(np.arcsin(scalar_product / module)) + angle_sum += angle + print(angle_sum) + return abs(angle_sum - math.pi*2) < cte.EPSILON + + def contains_polygon(self, polygon): + """ + Determines if the given polygon is contained by the current polygon + :return: boolean + """ + print('contains') + for point in polygon.points: + print(point.coordinates, self.contains_point(point)) + + if not self.contains_point(point): + return False + print('Belong!') + return True + @property def points_list(self) -> np.ndarray: """ @@ -249,7 +292,7 @@ class Polygon: points_list = self.points_list normal = self.normal if np.linalg.norm(normal) == 0: - sys.stderr.write(f'Not able to triangulate polygon [normal length is 0]') + sys.stderr.write('Not able to triangulate polygon\n') return [self] # are points concave or convex? total_points_list, concave_points, convex_points = self._starting_lists(points_list, normal) @@ -297,10 +340,10 @@ class Polygon: continue break if len(total_points_list) <= 3 and len(convex_points) > 0: - sys.stderr.write('Not able to triangulate polygon [convex surface]\n') + sys.stderr.write('Not able to triangulate polygon\n') return [self] if j >= 100: - sys.stderr.write('Not able to triangulate polygon [infinite loop]\n') + sys.stderr.write('Not able to triangulate polygon\n') return [self] last_ear = self._triangle(points_list, total_points_list, concave_points[1]) ears.append(last_ear) diff --git a/city_model_structure/attributes/polyhedron.py b/city_model_structure/attributes/polyhedron.py index 44a1f8bc..3f292f79 100644 --- a/city_model_structure/attributes/polyhedron.py +++ b/city_model_structure/attributes/polyhedron.py @@ -114,7 +114,7 @@ class Polyhedron: if self._trimesh is None: for face in self.faces: if len(face) != 3: - sys.stderr.write(f'Not able to generate trimesh the face has {len(face)} vertices\n') + sys.stderr.write('Not able to generate trimesh\n') return None self._trimesh = Trimesh(vertices=self.vertices, faces=self.faces) return self._trimesh diff --git a/city_model_structure/building.py b/city_model_structure/building.py index 951eab05..f7d2dc16 100644 --- a/city_model_structure/building.py +++ b/city_model_structure/building.py @@ -5,17 +5,17 @@ Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@conc contributors: Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ -from typing import List, Union +from typing import List, Union, TypeVar import numpy as np from city_model_structure.building_demand.surface import Surface -from city_model_structure.building_demand.thermal_zone import ThermalZone -from city_model_structure.building_demand.thermal_boundary import ThermalBoundary from city_model_structure.building_demand.usage_zone import UsageZone from city_model_structure.building_demand.storey import Storey from city_model_structure.city_object import CityObject from city_model_structure.building_demand.household import Household from city_model_structure.attributes.polyhedron import Polyhedron +ThermalZone = TypeVar('ThermalZone') + class Building(CityObject): """ @@ -35,9 +35,8 @@ class Building(CityObject): self._roof_type = None self._storeys = None self._geometrical_zones = None - self._thermal_zones = [] - self._thermal_boundaries = None - self._usage_zones = [] + self._thermal_zones = None + self._usage_zones = None self._type = 'building' self._heating = dict() self._cooling = dict() @@ -78,28 +77,6 @@ class Building(CityObject): """ return self._grounds - @property - def is_heated(self): - """ - Get building heated flag - :return: Boolean - """ - for usage_zone in self.usage_zones: - if usage_zone.is_heated: - return usage_zone.is_heated - return False - - @property - def is_cooled(self): - """ - Get building cooled flag - :return: Boolean - """ - for usage_zone in self.usage_zones: - if usage_zone.is_cooled: - return usage_zone.is_cooled - return False - @property def roofs(self) -> List[Surface]: """ @@ -117,18 +94,23 @@ class Building(CityObject): return self._walls @property - def usage_zones(self) -> List[UsageZone]: + def usage_zones(self) -> Union[None, List[UsageZone]]: """ Get city object usage zones :return: [UsageZone] """ - if len(self._usage_zones) == 0: - for thermal_zone in self.thermal_zones: - self._usage_zones.extend(thermal_zone.usage_zones) return self._usage_zones + @usage_zones.setter + def usage_zones(self, value): + """ + Set city object usage zones + :param value: [UsageZone] + """ + self._usage_zones = value + @property - def terrains(self) -> List[Surface]: + def terrains(self) -> Union[None, List[Surface]]: """ Get city object terrain surfaces :return: [Surface] @@ -170,26 +152,21 @@ class Building(CityObject): self._basement_heated = int(value) @property - def name(self): - """ - Get building name - :return: str - """ - return self._name - - @property - def thermal_zones(self) -> List[ThermalZone]: + def thermal_zones(self) -> Union[None, List[ThermalZone]]: """ Get building thermal zones :return: [ThermalZone] """ - if len(self._thermal_zones) == 0: - if self.storeys is None: - return [] - for storey in self.storeys: - self._thermal_zones.append(storey.thermal_zone) return self._thermal_zones + @thermal_zones.setter + def thermal_zones(self, value): + """ + Set city object thermal zones + :param value: [ThermalZone] + """ + self._thermal_zones = value + @property def heated_volume(self): """ @@ -213,7 +190,7 @@ class Building(CityObject): :param value: int """ if value is not None: - self._year_of_construction = value + self._year_of_construction = int(value) @property def function(self) -> Union[None, str]: @@ -341,6 +318,14 @@ class Building(CityObject): break return self._roof_type + @roof_type.setter + def roof_type(self, value): + """ + Set roof type for the building flat or pitch + :return: str + """ + self._roof_type = value + @property def floor_area(self): """ @@ -354,24 +339,6 @@ class Building(CityObject): self._floor_area += surface.perimeter_polygon.area return self._floor_area - @property - def thermal_boundaries(self) -> List[ThermalBoundary]: - """ - Get all thermal boundaries associated to the building's thermal zones - :return: [ThermalBoundary] - """ - if self._thermal_boundaries is None: - self._thermal_boundaries = [] - for thermal_zone in self.thermal_zones: - _thermal_boundary_duplicated = False - for thermal_boundary in thermal_zone.thermal_boundaries: - if len(thermal_boundary.thermal_zones) > 1: - if thermal_zone != thermal_boundary.thermal_zones[1]: - self._thermal_boundaries.append(thermal_boundary) - else: - self._thermal_boundaries.append(thermal_boundary) - return self._thermal_boundaries - @property def households(self) -> List[Household]: """ @@ -379,3 +346,16 @@ class Building(CityObject): :return: List[Household] """ return self._households + + @property + def is_conditioned(self): + """ + Get building heated flag + :return: Boolean + """ + if self.thermal_zones is None: + return False + for thermal_zone in self.thermal_zones: + if thermal_zone.hvac_system is not None: + return True + return False diff --git a/city_model_structure/building_demand/appliances.py b/city_model_structure/building_demand/appliances.py new file mode 100644 index 00000000..ed09f795 --- /dev/null +++ b/city_model_structure/building_demand/appliances.py @@ -0,0 +1,103 @@ +""" +Appliances module +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2022 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +""" +from typing import Union +from city_model_structure.attributes.schedule import Schedule + + +class Appliances: + """ + Appliances class + """ + def __init__(self): + self._lighting_density = None + self._convective_fraction = None + self._radiant_fraction = None + self._latent_fraction = None + self._schedule = None + + @property + def lighting_density(self) -> Union[None, float]: + """ + Get lighting density in Watts per m2 + :return: None or float + """ + return self._lighting_density + + @lighting_density.setter + def lighting_density(self, value): + """ + Set lighting density in Watts per m2 + :param value: float + """ + if value is not None: + self._lighting_density = float(value) + + @property + def convective_fraction(self) -> Union[None, float]: + """ + Get convective fraction + :return: None or float + """ + return self._convective_fraction + + @convective_fraction.setter + def convective_fraction(self, value): + """ + Set convective fraction + :param value: float + """ + if value is not None: + self._convective_fraction = float(value) + + @property + def radiant_fraction(self) -> Union[None, float]: + """ + Get radiant fraction + :return: None or float + """ + return self._radiant_fraction + + @radiant_fraction.setter + def radiant_fraction(self, value): + """ + Set radiant fraction + :param value: float + """ + if value is not None: + self._radiant_fraction = float(value) + + @property + def latent_fraction(self) -> Union[None, float]: + """ + Get latent fraction + :return: None or float + """ + return self._latent_fraction + + @latent_fraction.setter + def latent_fraction(self, value): + """ + Set latent fraction + :param value: float + """ + if value is not None: + self._latent_fraction = float(value) + + @property + def schedule(self) -> Union[None, Schedule]: + """ + Get schedule + :return: None or Schedule + """ + return self._schedule + + @schedule.setter + def schedule(self, value): + """ + Set schedule + :param value: Schedule + """ + self._schedule = value diff --git a/city_model_structure/building_demand/internal_gains.py b/city_model_structure/building_demand/internal_gains.py index 75e60cb7..4cfeabb9 100644 --- a/city_model_structure/building_demand/internal_gains.py +++ b/city_model_structure/building_demand/internal_gains.py @@ -5,6 +5,7 @@ Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@conc """ from typing import Union +from city_model_structure.attributes.schedule import Schedule class InternalGains: @@ -18,6 +19,7 @@ class InternalGains: self._convective_fraction = None self._radiative_fraction = None self._latent_fraction = None + self._schedule = None @property def type(self) -> Union[None, str]: @@ -103,3 +105,19 @@ class InternalGains: """ if value is not None: self._latent_fraction = float(value) + + @property + def schedule(self) -> Union[None, Schedule]: + """ + Get internal gain schedule + :return: Schedule + """ + return self._schedule + + @schedule.setter + def schedule(self, value): + """ + Set internal gain schedule + :param value: Schedule + """ + self._schedule = value diff --git a/city_model_structure/building_demand/lighting.py b/city_model_structure/building_demand/lighting.py new file mode 100644 index 00000000..0407a432 --- /dev/null +++ b/city_model_structure/building_demand/lighting.py @@ -0,0 +1,103 @@ +""" +Lighting module +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2022 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +""" +from typing import Union +from city_model_structure.attributes.schedule import Schedule + + +class Lighting: + """ + Lighting class + """ + def __init__(self): + self._lighting_density = None + self._convective_fraction = None + self._radiant_fraction = None + self._latent_fraction = None + self._schedule = None + + @property + def lighting_density(self) -> Union[None, float]: + """ + Get lighting density in Watts per m2 + :return: None or float + """ + return self._lighting_density + + @lighting_density.setter + def lighting_density(self, value): + """ + Set lighting density in Watts per m2 + :param value: float + """ + if value is not None: + self._lighting_density = float(value) + + @property + def convective_fraction(self) -> Union[None, float]: + """ + Get convective fraction + :return: None or float + """ + return self._convective_fraction + + @convective_fraction.setter + def convective_fraction(self, value): + """ + Set convective fraction + :param value: float + """ + if value is not None: + self._convective_fraction = float(value) + + @property + def radiant_fraction(self) -> Union[None, float]: + """ + Get radiant fraction + :return: None or float + """ + return self._radiant_fraction + + @radiant_fraction.setter + def radiant_fraction(self, value): + """ + Set radiant fraction + :param value: float + """ + if value is not None: + self._radiant_fraction = float(value) + + @property + def latent_fraction(self) -> Union[None, float]: + """ + Get latent fraction + :return: None or float + """ + return self._latent_fraction + + @latent_fraction.setter + def latent_fraction(self, value): + """ + Set latent fraction + :param value: float + """ + if value is not None: + self._latent_fraction = float(value) + + @property + def schedule(self) -> Union[None, Schedule]: + """ + Get schedule + :return: None or Schedule + """ + return self._schedule + + @schedule.setter + def schedule(self, value): + """ + Set schedule + :param value: Schedule + """ + self._schedule = value diff --git a/city_model_structure/building_demand/material.py b/city_model_structure/building_demand/material.py index cb015fa7..5762550d 100644 --- a/city_model_structure/building_demand/material.py +++ b/city_model_structure/building_demand/material.py @@ -6,6 +6,7 @@ Contributor Atiya atiya.atiya@mail.concordia.ca Contributor Mohammad Reza mohammad.seyedabadi@mail.concordia.ca """ +import ast from typing import Union @@ -14,6 +15,7 @@ class Material: Material class """ def __init__(self): + self._type = type self._id = None self._name = None self._conductivity = None @@ -25,7 +27,30 @@ class Material: self._visible_absorptance = None self._no_mass = False self._thermal_resistance = None - self._lca_id = None + self._embodied_carbon = None + self._embodied_carbon_unit = None + self._recycling_ratio = None + self._onsite_recycling_ratio = None + self._company_recycling_ratio = None + self._landfilling_ratio = None + self._cost = None + self._cost_unit = None + + @property + def type(self): + """ + Get material type + :return: str + """ + return self._type + + @type.setter + def type(self, value): + """ + Set material type + :param value: string + """ + self._type = str(value) @property def id(self): @@ -213,10 +238,137 @@ class Material: self._thermal_resistance = float(value) @property - def lca_id(self): - return self._lca_id + def embodied_carbon(self) -> Union[None, float]: + """ + Get material embodied carbon + :return: None or float + """ + return self._embodied_carbon - @lca_id.setter - def lca_id(self, value): - self._lca_id = value + @embodied_carbon.setter + def embodied_carbon(self, value): + """ + Set material embodied carbon + :param value: float + """ + if value is not None: + self._embodied_carbon = float(value) + @property + def embodied_carbon_unit(self) -> Union[None, str]: + """ + Get material embodied carbon unit + :return: None or string + """ + return self._embodied_carbon + + @embodied_carbon_unit.setter + def embodied_carbon_unit(self, value): + """ + Set material embodied carbon unit + :param value: string + """ + if value is not None: + self._embodied_carbon_unit = str(value) + + @property + def recycling_ratio(self) -> Union[None, float]: + """ + Get material recycling ratio + :return: None or float + """ + return self._recycling_ratio + + @recycling_ratio.setter + def recycling_ratio(self, value): + """ + Set material recycling ratio + :param value: float + """ + if value is not None: + self._recycling_ratio = float(value) + + @property + def onsite_recycling_ratio(self) -> Union[None, float]: + """ + Get material onsite recycling ratio + :return: None or float + """ + return self._onsite_recycling_ratio + + @onsite_recycling_ratio.setter + def onsite_recycling_ratio(self, value): + """ + Set material onsite recycling ratio + :param value: float + """ + if value is not None: + self._onsite_recycling_ratio = float(value) + + @property + def company_recycling_ratio(self) -> Union[None, float]: + """ + Get material company recycling ratio + :return: None or float + """ + return self._company_recycling_ratio + + @company_recycling_ratio.setter + def company_recycling_ratio(self, value): + """ + Set material company recycling ratio + :param value: float + """ + if value is not None: + self._company_recycling_ratio = float(value) + + @property + def landfilling_ratio(self) -> Union[None, float]: + """ + Get material landfilling ratio + :return: None or float + """ + return self._landfilling_ratio + + @landfilling_ratio.setter + def landfilling_ratio(self, value): + """ + Set material landfilling ratio + :param value: float + """ + if value is not None: + self._landfilling_ratio = float(value) + + @property + def cost(self) -> Union[None, float]: + """ + Get material cost + :return: None or float + """ + return self._cost + + @cost.setter + def cost(self, value): + """ + Set material cost + :param value: float + """ + if value is not None: + self._cost = float(value) + + @property + def cost_unit(self) -> Union[None, str]: + """ + Get material cost unit + :return: None or string + """ + return self._cost_unit + + @cost_unit.setter + def cost_unit(self, value): + """ + Set material cost unit + :param value: string + """ + if value is not None: + self._cost_unit = float(value) diff --git a/city_model_structure/building_demand/occupancy.py b/city_model_structure/building_demand/occupancy.py new file mode 100644 index 00000000..443030ca --- /dev/null +++ b/city_model_structure/building_demand/occupancy.py @@ -0,0 +1,121 @@ +""" +Occupancy module +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2022 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +""" +from typing import Union, List +from city_model_structure.attributes.schedule import Schedule +from city_model_structure.building_demand.occupant import Occupant + + +class Occupancy: + """ + Occupancy class + """ + def __init__(self): + self._occupancy_density = None + self._sensible_convective_internal_gain = None + self._sensible_radiant_internal_gain = None + self._latent_internal_gain = None + self._occupancy_schedule = None + self._occupants = None + + @property + def occupancy_density(self) -> Union[None, float]: + """ + Get density in m2 per person + :return: None or float + """ + return self._occupancy_density + + @occupancy_density.setter + def occupancy_density(self, value): + """ + Set density in m2 per persons + :param value: float + """ + if value is not None: + self._occupancy_density = float(value) + + @property + def sensible_convective_internal_gain(self) -> Union[None, float]: + """ + Get sensible convective internal gain in Watts per m2 + :return: None or float + """ + return self._sensible_convective_internal_gain + + @sensible_convective_internal_gain.setter + def sensible_convective_internal_gain(self, value): + """ + Set sensible convective internal gain in Watts per m2 + :param value: float + """ + if value is not None: + self._sensible_convective_internal_gain = float(value) + + @property + def sensible_radiant_internal_gain(self) -> Union[None, float]: + """ + Get sensible radiant internal gain in Watts per m2 + :return: None or float + """ + return self._sensible_radiant_internal_gain + + @sensible_radiant_internal_gain.setter + def sensible_radiant_internal_gain(self, value): + """ + Set sensible radiant internal gain in Watts per m2 + :param value: float + """ + if value is not None: + self._sensible_radiant_internal_gain = float(value) + + @property + def latent_internal_gain(self) -> Union[None, float]: + """ + Get latent internal gain in Watts per m2 + :return: None or float + """ + return self._latent_internal_gain + + @latent_internal_gain.setter + def latent_internal_gain(self, value): + """ + Set latent internal gain in Watts per m2 + :param value: float + """ + if value is not None: + self._latent_internal_gain = float(value) + + @property + def occupancy_schedule(self) -> Union[None, Schedule]: + """ + Get occupancy schedule + :return: None or Schedule + """ + return self._occupancy_schedule + + @occupancy_schedule.setter + def occupancy_schedule(self, value): + """ + Set occupancy schedule + :param value: Schedule + """ + self._occupancy_schedule = value + + @property + def occupants(self) -> Union[None, List[Occupant]]: + """ + Get list of occupants + :return: None or List of Occupant + """ + return self._occupants + + @occupants.setter + def occupants(self, value): + """ + Set list of occupants + :param value: [Occupant] + """ + self._occupants = value diff --git a/city_model_structure/building_demand/occupants.py b/city_model_structure/building_demand/occupant.py similarity index 79% rename from city_model_structure/building_demand/occupants.py rename to city_model_structure/building_demand/occupant.py index 3d7a39b6..8a7dd533 100644 --- a/city_model_structure/building_demand/occupants.py +++ b/city_model_structure/building_demand/occupant.py @@ -1,19 +1,16 @@ """ -Occupants module +Occupant module SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2020 Project Author Sanam Dabirian sanam.dabirian@mail.concordia.ca Contributors Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ -from typing import TypeVar import calendar as cal -UsageZone = TypeVar('UsageZone') - -class Occupants: +class Occupant: """ - Occupants class + Occupant class """ def __init__(self): @@ -24,15 +21,11 @@ class Occupants: self._heat_dissipation = None self._occupancy_rate = None self._occupant_type = None - self._usage_zone = None - self._occupant_schedule = None - self._number_of_occupants = None self._arrival_time = None self._departure_time = None self._break_time = None self._day_of_week = None self._pd_of_meetings_duration = None - self._complete_year_schedule = None @property def heat_dissipation(self): @@ -82,46 +75,6 @@ class Occupants: """ self._occupant_type = float(value) - @property - def usage_zone(self) -> UsageZone: - """ - Get the zone an occupant is in - :return: UsageZone - """ - return self._usage_zone - - @property - def occupant_schedule(self): - """ - Get the schedules when an occupant is in a zone (24 values, 1 per hour of the day) - :return: [float] - """ - return self._occupant_schedule - - @occupant_schedule.setter - def occupant_schedule(self, value): - """ - Set the schedules when an occupant is in a zone (24 values, 1 per hour of the day) - :param value: [float] - """ - self._occupant_schedule = [float(i) for i in value] - - @property - def number_of_occupants(self): - """ - Get the number of occupants - :return: int - """ - return self._number_of_occupants - - @number_of_occupants.setter - def number_of_occupants(self, value): - """ - Set the number of occupants - :param value: int - """ - self._number_of_occupants = int(value) - @property def arrival_time(self): """ diff --git a/city_model_structure/building_demand/storey.py b/city_model_structure/building_demand/storey.py index f7a211d8..143eec29 100644 --- a/city_model_structure/building_demand/storey.py +++ b/city_model_structure/building_demand/storey.py @@ -12,12 +12,10 @@ from city_model_structure.building_demand.thermal_zone import ThermalZone class Storey: - # todo: rethink this class for buildings with windows """ Storey class """ - def __init__(self, name, storey_surfaces, neighbours, volume): - # todo: the information of the parent surface is lost -> need to recover it + def __init__(self, name, storey_surfaces, neighbours, volume, floor_area): self._name = name self._storey_surfaces = storey_surfaces self._thermal_boundaries = None @@ -25,6 +23,7 @@ class Storey: self._thermal_zone = None self._neighbours = neighbours self._volume = volume + self._floor_area = floor_area @property def name(self): @@ -59,7 +58,13 @@ class Storey: if self._thermal_boundaries is None: self._thermal_boundaries = [] for surface in self.surfaces: - self._thermal_boundaries.append(ThermalBoundary(surface)) + if surface.holes_polygons is None: + windows_areas = None + else: + windows_areas = [] + for hole in surface.holes_polygons: + windows_areas.append(hole.area) + self._thermal_boundaries.append(ThermalBoundary(surface, surface.solid_polygon.area, windows_areas)) return self._thermal_boundaries @property @@ -81,7 +86,7 @@ class Storey: :return: ThermalZone """ if self._thermal_zone is None: - self._thermal_zone = ThermalZone(self.thermal_boundaries, self.volume) + self._thermal_zone = ThermalZone(self.thermal_boundaries, self.volume, self.floor_area) return self._thermal_zone @property @@ -91,3 +96,11 @@ class Storey: :return: float """ return self._volume + + @property + def floor_area(self): + """ + Get storey's floor area in square meters + :return: float + """ + return self._floor_area diff --git a/city_model_structure/building_demand/surface.py b/city_model_structure/building_demand/surface.py index 1e2178c8..6c73d268 100644 --- a/city_model_structure/building_demand/surface.py +++ b/city_model_structure/building_demand/surface.py @@ -71,6 +71,7 @@ class Surface: if value is not None: self._id = str(value) + # todo: implement share surfaces @property def share_surfaces(self): """ @@ -191,6 +192,8 @@ class Surface: Get surface type Ground, Wall or Roof :return: str """ + + # todo: there are more types: internal wall, internal floor... this method must be redefined if self._type is None: grad = np.rad2deg(self.inclination) if grad >= 170: diff --git a/city_model_structure/building_demand/thermal_boundary.py b/city_model_structure/building_demand/thermal_boundary.py index 7b444a14..946e7642 100644 --- a/city_model_structure/building_demand/thermal_boundary.py +++ b/city_model_structure/building_demand/thermal_boundary.py @@ -5,25 +5,24 @@ Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@conc Contributors Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ import uuid -from typing import List, TypeVar, Union +from typing import List, Union +from helpers.configuration_helper import ConfigurationHelper as ch from city_model_structure.building_demand.layer import Layer from city_model_structure.building_demand.thermal_opening import ThermalOpening from city_model_structure.building_demand.thermal_zone import ThermalZone from city_model_structure.building_demand.surface import Surface -Polygon = TypeVar('Polygon') - class ThermalBoundary: """ ThermalBoundary class """ - def __init__(self, surface): - self._surface = surface + def __init__(self, parent_surface, opaque_area, windows_areas): + self._parent_surface = parent_surface + self._opaque_area = opaque_area + self._windows_areas = windows_areas self._id = None self._thermal_zones = None - # ToDo: up to at least LOD2 will be just one thermal opening per Thermal boundary only if window_ratio > 0, - # review for LOD3 and LOD4 self._thermal_openings = None self._layers = None self._outside_solar_absorptance = None @@ -32,16 +31,16 @@ class ThermalBoundary: self._u_value = None self._shortwave_reflectance = None self._construction_name = None - self._hi = 3.5 - self._he = 20 - self._window_ratio = None + self._hi = ch().convective_heat_transfer_coefficient_interior + self._he = ch().convective_heat_transfer_coefficient_exterior self._refurbishment_measure = None - self._surface_geometry = None self._thickness = None self._virtual_internal_surface = None self._inside_emissivity = None self._alpha_coefficient = None self._radiative_coefficient = None + self._window_ratio = None + self._calculated = False @property def id(self): @@ -54,13 +53,12 @@ class ThermalBoundary: return self._id @property - def surface(self) -> Surface: + def parent_surface(self) -> Surface: """ Get the surface that belongs to the thermal boundary :return: Surface """ - # todo: in LoD4 this property will be a list of surfaces, not only one - return self._surface + return self._parent_surface @property def thermal_zones(self) -> List[ThermalZone]: @@ -73,47 +71,35 @@ class ThermalBoundary: @thermal_zones.setter def thermal_zones(self, value): """ - Set the thermal zones delimited by the thermal boundary + Get the thermal zones delimited by the thermal boundary :param value: [ThermalZone] """ self._thermal_zones = value + # todo: do I need these two?? @property def azimuth(self): """ Get the thermal boundary azimuth in radians :return: float """ - return self._surface.azimuth + return self.parent_surface.azimuth @property def inclination(self): """ - Set the thermal boundary inclination in radians + Get the thermal boundary inclination in radians :return: float """ - return self._surface.inclination + return self.parent_surface.inclination @property - def area(self): + def opaque_area(self): """ - Set the thermal boundary area in square meters + Get the thermal boundary area in square meters :return: float """ - # to check the lod without depending on that parameter - if float(self.surface.solid_polygon.area) - float(self.surface.perimeter_polygon.area) < 1e-3: - area = float(self.surface.perimeter_polygon.area) * (1 - float(self.window_ratio)) - else: - area = self.surface.solid_polygon.area - return area - - @property - def _total_area_including_windows(self): - """ - Get the thermal boundary plus windows area in square meters - :return: float - """ - return self.surface.perimeter_polygon.area + return float(self._opaque_area) @property def thickness(self): @@ -125,7 +111,8 @@ class ThermalBoundary: self._thickness = 0.0 if self.layers is not None: for layer in self.layers: - self._thickness += layer.thickness + if not layer.material.no_mass: + self._thickness += layer.thickness return self._thickness @property @@ -181,30 +168,34 @@ class ThermalBoundary: self._outside_visible_absorptance = float(value) @property - def thermal_openings(self) -> List[ThermalOpening]: + def thermal_openings(self) -> Union[None, List[ThermalOpening]]: """ Get thermal boundary thermal openings :return: [ThermalOpening] """ if self._thermal_openings is None: - if float(self.window_ratio) > 0: - thermal_opening = ThermalOpening() - thermal_opening.area = float(self._total_area_including_windows) * float(self.window_ratio) - thermal_opening.hi = self.hi - thermal_opening.he = self.he - self._thermal_openings = [thermal_opening] + if self.window_ratio is not None: + if self.window_ratio == 0: + self._thermal_openings = [] + else: + thermal_opening = ThermalOpening() + if self.window_ratio == 1: + _area = self.opaque_area + else: + _area = self.opaque_area * self.window_ratio / (1-self.window_ratio) + thermal_opening.area = _area + self._thermal_openings = [thermal_opening] else: - self._thermal_openings = [] + if len(self.windows_areas) > 0: + self._thermal_openings = [] + for window_area in self.windows_areas: + thermal_opening = ThermalOpening() + thermal_opening.area = window_area + self._thermal_openings.append(thermal_opening) + else: + self._thermal_openings = [] return self._thermal_openings - @thermal_openings.setter - def thermal_openings(self, value): - """ - Set thermal boundary thermal openings - :param value: [ThermalOpening] - """ - self._thermal_openings = value - @property def construction_name(self) -> Union[None, str]: """ @@ -244,24 +235,47 @@ class ThermalBoundary: Get thermal boundary surface type :return: str """ - return self._surface.type + return self.parent_surface.type @property def window_ratio(self) -> Union[None, float]: """ Get thermal boundary window ratio - :return: None or float + It returns the window ratio calculated as the total windows' areas in a wall divided by + the total (opaque + transparent) area of that wall if windows are defined in the geometry imported. + If not, it returns the window ratio imported from an external source (e.g. construction library, manually assigned). + If none of those sources are available, it returns None. + :return: float """ + if self.windows_areas is not None: + if not self._calculated: + _calculated = True + if len(self.windows_areas) == 0: + self._window_ratio = 0 + else: + total_window_area = 0 + for window_area in self.windows_areas: + total_window_area += window_area + self._window_ratio = total_window_area / (self.opaque_area + total_window_area) return self._window_ratio @window_ratio.setter def window_ratio(self, value): """ Set thermal boundary window ratio - :param value: float + :param value: str """ - if value is not None: - self._window_ratio = float(value) + if self._calculated: + raise ValueError('Window ratio cannot be assigned when the windows are defined in the geometry.') + self._window_ratio = float(value) + + @property + def windows_areas(self) -> [float]: + """ + Get windows areas + :return: [float] + """ + return self._windows_areas @property def u_value(self) -> Union[None, float]: @@ -327,7 +341,7 @@ class ThermalBoundary: :param value: float """ if value is not None: - self._hi = float(value) + self._hi = value @property def he(self) -> Union[None, float]: @@ -344,14 +358,7 @@ class ThermalBoundary: :param value: float """ if value is not None: - self._he = float(value) - - @property - def surface_geometry(self): - """ - Raises not implemented error - """ - raise NotImplementedError + self._he = value @property def virtual_internal_surface(self) -> Surface: @@ -360,10 +367,9 @@ class ThermalBoundary: :return: Surface """ if self._virtual_internal_surface is None: - self._virtual_internal_surface = self.surface.inverse + self._virtual_internal_surface = self.parent_surface.inverse return self._virtual_internal_surface - # todo: need extract information from construction library or assume them at the beginning of workflows @property def inside_emissivity(self) -> Union[None, float]: """ diff --git a/city_model_structure/building_demand/thermal_control.py b/city_model_structure/building_demand/thermal_control.py new file mode 100644 index 00000000..20888023 --- /dev/null +++ b/city_model_structure/building_demand/thermal_control.py @@ -0,0 +1,86 @@ +""" +ThermalControl module +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2022 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +""" +from typing import Union +from city_model_structure.attributes.schedule import Schedule + + +class ThermalControl: + """ + ThermalControl class + """ + def __init__(self): + self._heating_set_point = None + self._cooling_set_point = None + self._hvac_availability = None + self._heating_set_back = None + + @property + def heating_set_point(self) -> Union[None, Schedule]: + """ + Get heating set point defined for a thermal zone in Celsius + :return: None or Schedule + """ + return self._heating_set_point + + @heating_set_point.setter + def heating_set_point(self, value): + """ + Set heating set point defined for a thermal zone in Celsius + :param value: Schedule + """ + self._heating_set_point = value + + @property + def heating_set_back(self) -> Union[None, float]: + """ + Get heating set back defined for a thermal zone in Celsius + Heating set back is the only parameter which is not a schedule as it is either one value or it is implicit in the + set point schedule + :return: None or float + """ + return self._heating_set_back + + @heating_set_back.setter + def heating_set_back(self, value): + """ + Set heating set back defined for a thermal zone in Celsius + :param value: float + """ + if value is not None: + self._heating_set_back = float(value) + + @property + def cooling_set_point(self) -> Union[None, Schedule]: + """ + Get cooling set point defined for a thermal zone in Celsius + :return: None or Schedule + """ + return self._cooling_set_point + + @cooling_set_point.setter + def cooling_set_point(self, value): + """ + Set cooling set point defined for a thermal zone in Celsius + :param value: Schedule + """ + self._cooling_set_point = value + + @property + def hvac_availability(self) -> Union[None, Schedule]: + """ + Get the availability of the conditioning system defined for a thermal zone + :return: None or Schedule + """ + return self._hvac_availability + + @hvac_availability.setter + def hvac_availability(self, value): + """ + Set the availability of the conditioning system defined for a thermal zone + :param value: Schedule + """ + self._hvac_availability = value + diff --git a/city_model_structure/building_demand/thermal_opening.py b/city_model_structure/building_demand/thermal_opening.py index 0683f095..101dcffc 100644 --- a/city_model_structure/building_demand/thermal_opening.py +++ b/city_model_structure/building_demand/thermal_opening.py @@ -6,6 +6,7 @@ Contributors Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ import uuid from typing import TypeVar, Union +from helpers.configuration_helper import ConfigurationHelper as ch Polygon = TypeVar('Polygon') @@ -25,9 +26,8 @@ class ThermalOpening: self._front_side_solar_transmittance_at_normal_incidence = None self._back_side_solar_transmittance_at_normal_incidence = None self._overall_u_value = None - self._hi = None - self._he = None - self._surface_geometry = None + self._hi = ch().convective_heat_transfer_coefficient_interior + self._he = ch().convective_heat_transfer_coefficient_exterior self._inside_emissivity = None self._alpha_coefficient = None self._radiative_coefficient = None @@ -240,15 +240,6 @@ class ThermalOpening: if value is not None: self._he = float(value) - @property - def surface_geometry(self) -> Polygon: - """ - Get the polygon that defines the thermal opening - :return: Polygon - """ - return self._surface_geometry - - # todo: need extract information from construction library or assume them at the beginning of workflows @property def inside_emissivity(self) -> Union[None, float]: """ diff --git a/city_model_structure/building_demand/thermal_zone.py b/city_model_structure/building_demand/thermal_zone.py index a3ad26e1..dd20bc28 100644 --- a/city_model_structure/building_demand/thermal_zone.py +++ b/city_model_structure/building_demand/thermal_zone.py @@ -5,32 +5,33 @@ Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@conc Contributors Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ import uuid -from typing import List, TypeVar, Union +from typing import List, Union, Tuple, TypeVar from city_model_structure.building_demand.usage_zone import UsageZone -import ast +from city_model_structure.building_demand.thermal_control import ThermalControl +from city_model_structure.energy_systems.hvac_system import HvacSystem +from city_model_structure.attributes.schedule import Schedule ThermalBoundary = TypeVar('ThermalBoundary') -Polyhedron = TypeVar('Polyhedron') class ThermalZone: """ ThermalZone class """ - def __init__(self, thermal_boundaries, volume): - self._floor_area = None + def __init__(self, thermal_boundaries, volume, floor_area): + self._id = None + self._floor_area = floor_area self._thermal_boundaries = thermal_boundaries - self._is_mechanically_ventilated = None self._additional_thermal_bridge_u_value = None self._effective_thermal_capacity = None self._indirectly_heated_area_ratio = None self._infiltration_rate_system_on = None self._infiltration_rate_system_off = None - self._usage_zones = [] + self._usage_zones = None self._volume = volume - self._volume_geometry = None - self._id = None self._ordinate_number = None + self._thermal_control = None + self._hvac_system = None @property def id(self): @@ -43,34 +44,11 @@ class ThermalZone: return self._id @property - def is_mechanically_ventilated(self) -> Union[None, bool]: - """ - Get thermal zone mechanical ventilation flag - :return: None or Boolean - """ - return self._is_mechanically_ventilated - - @is_mechanically_ventilated.setter - def is_mechanically_ventilated(self, value): - """ - Set thermal zone mechanical ventilation flag - :param value: Boolean - """ - if value is not None: - self._is_mechanically_ventilated = ast.literal_eval(value) - - @property - def floor_area(self): + def floor_area(self) -> float: """ Get thermal zone floor area in m2 :return: float """ - if self._floor_area is None: - self._floor_area = 0 - for thermal_boundary in self.thermal_boundaries: - s = thermal_boundary.surface - if s.type == 'Ground': - self._floor_area += s.perimeter_polygon.area return self._floor_area @property @@ -133,10 +111,10 @@ class ThermalZone: self._indirectly_heated_area_ratio = float(value) @property - def infiltration_rate_system_on(self) -> Union[None, float]: + def infiltration_rate_system_on(self) -> Union[None, Schedule]: """ Get thermal zone infiltration rate system on in air changes per hour (ACH) - :return: None or float + :return: None or Schedule """ return self._infiltration_rate_system_on @@ -144,16 +122,15 @@ class ThermalZone: def infiltration_rate_system_on(self, value): """ Set thermal zone infiltration rate system on in air changes per hour (ACH) - :param value: float + :param value: Schedule """ - if value is not None: - self._infiltration_rate_system_on = float(value) + self._infiltration_rate_system_on = value @property - def infiltration_rate_system_off(self) -> Union[None, float]: + def infiltration_rate_system_off(self) -> Union[None, Schedule]: """ Get thermal zone infiltration rate system off in air changes per hour (ACH) - :return: None or float + :return: None or Schedule """ return self._infiltration_rate_system_off @@ -161,15 +138,14 @@ class ThermalZone: def infiltration_rate_system_off(self, value): """ Set thermal zone infiltration rate system on in air changes per hour (ACH) - :param value: float + :param value: Schedule """ - if value is not None: - self._infiltration_rate_system_off = float(value) + self._infiltration_rate_system_off = value @property - def usage_zones(self) -> List[UsageZone]: + def usage_zones(self) -> Tuple[float, UsageZone]: """ - Get thermal zone usage zones + Get list of usage zones and the percentage of thermal zone's volume affected by that usage :return: [UsageZone] """ return self._usage_zones @@ -177,8 +153,8 @@ class ThermalZone: @usage_zones.setter def usage_zones(self, values): """ - Set thermal zone usage zones - :param values: [UsageZone] + Set list of usage zones and the percentage of thermal zone's volume affected by that usage + :param values: Tuple[float, UsageZone] """ self._usage_zones = values @@ -190,14 +166,6 @@ class ThermalZone: """ return self._volume - @property - def volume_geometry(self) -> Polyhedron: - """ - Get the polyhedron defined by the thermal zone - :return: Polyhedron - """ - return self._volume_geometry - @property def ordinate_number(self) -> Union[None, int]: """ @@ -214,3 +182,35 @@ class ThermalZone: """ if value is not None: self._ordinate_number = int(value) + + @property + def thermal_control(self) -> Union[None, ThermalControl]: + """ + Get thermal control of this thermal zone + :return: None or ThermalControl + """ + return self._thermal_control + + @thermal_control.setter + def thermal_control(self, value): + """ + Set thermal control for this thermal zone + :param value: ThermalControl + """ + self._thermal_control = value + + @property + def hvac_system(self) -> Union[None, HvacSystem]: + """ + Get HVAC system installed for this thermal zone + :return: None or HvacSystem + """ + return self._hvac_system + + @hvac_system.setter + def hvac_system(self, value): + """ + Set HVAC system installed for this thermal zone + :param value: HvacSystem + """ + self._hvac_system = value diff --git a/city_model_structure/building_demand/usage_zone.py b/city_model_structure/building_demand/usage_zone.py index a2c16435..590ce6d5 100644 --- a/city_model_structure/building_demand/usage_zone.py +++ b/city_model_structure/building_demand/usage_zone.py @@ -5,13 +5,12 @@ Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.mons Contributors Guille Gutierrez guillermo.gutierrezmorote@concordia.ca """ import uuid -from typing import List, TypeVar, Union -import ast - -InternalGains = TypeVar('InternalGains') -Occupants = TypeVar('Occupants') -Polyhedron = TypeVar('Polyhedron') -Schedule = TypeVar('Schedule') +from typing import List, Union +import helpers.constants as cte +from city_model_structure.building_demand.internal_gains import InternalGains +from city_model_structure.building_demand.occupancy import Occupancy +from city_model_structure.building_demand.lighting import Lighting +from city_model_structure.building_demand.appliances import Appliances class UsageZone: @@ -21,23 +20,15 @@ class UsageZone: def __init__(self): self._id = None self._usage = None - self._internal_gains = None - self._heating_setpoint = None - self._heating_setback = None - self._cooling_setpoint = None - self._occupancy_density = None + self._not_detailed_source_mean_annual_internal_gains = None self._hours_day = None self._days_year = None - self._dhw_average_volume_pers_day = None - self._dhw_preparation_temperature = None - self._electrical_app_average_consumption_sqm_year = None +# self._electrical_app_average_consumption_sqm_year = None self._mechanical_air_change = None - self._occupants = None - self._schedules = None - self._volume = None - self._volume_geometry = None - self._is_heated = False - self._is_cooled = False + self._occupancy = None + self._lighting = None + self._appliances = None + self._internal_gains = None @property def id(self): @@ -50,71 +41,20 @@ class UsageZone: return self._id @property - def internal_gains(self) -> List[InternalGains]: + def not_detailed_source_mean_annual_internal_gains(self) -> List[InternalGains]: """ - Get usage zone internal gains + Get usage zone internal gains with unknown energy source :return: [InternalGains] """ - return self._internal_gains + return self._not_detailed_source_mean_annual_internal_gains - @internal_gains.setter - def internal_gains(self, value): + @not_detailed_source_mean_annual_internal_gains.setter + def not_detailed_source_mean_annual_internal_gains(self, value): """ - Set usage zone internal gains + Set usage zone internal gains with unknown energy source :param value: [InternalGains] """ - self._internal_gains = value - - @property - def heating_setpoint(self) -> Union[None, float]: - """ - Get usage zone heating set point in Celsius - :return: None or float - """ - return self._heating_setpoint - - @heating_setpoint.setter - def heating_setpoint(self, value): - """ - Set usage zone heating set point in Celsius - :param value: float - """ - if value is not None: - self._heating_setpoint = float(value) - - @property - def heating_setback(self) -> Union[None, float]: - """ - Get usage zone heating setback in Celsius - :return: None or float - """ - return self._heating_setback - - @heating_setback.setter - def heating_setback(self, value): - """ - Set usage zone heating setback in Celsius - :param value: float - """ - if value is not None: - self._heating_setback = float(value) - - @property - def cooling_setpoint(self) -> Union[None, float]: - """ - Get usage zone cooling setpoint in Celsius - :return: None or float - """ - return self._cooling_setpoint - - @cooling_setpoint.setter - def cooling_setpoint(self, value): - """ - Set usage zone cooling setpoint in Celsius - :param value: float - """ - if value is not None: - self._cooling_setpoint = float(value) + self._not_detailed_source_mean_annual_internal_gains = value @property def hours_day(self) -> Union[None, float]: @@ -184,89 +124,6 @@ class UsageZone: if value is not None: self._usage = str(value) - @property - def occupants(self) -> List[Occupants]: - """ - Get occupants data - :return: [Occupants] - """ - return self._occupants - - @occupants.setter - def occupants(self, values): - """ - Set occupants data - :param values: [Occupants] - """ - self._occupants = values - - @property - def schedules(self) -> List[Schedule]: - """ - Get usage zone schedules - :return: List[Schedule] - """ - return self._schedules - - @schedules.setter - def schedules(self, values): - """ - Set usage zone schedules - :param values: List[Schedule] - """ - self._schedules = values - - @property - def occupancy_density(self) -> Union[None, float]: - """ - Get density in persons per m2 - :return: None or float - """ - return self._occupancy_density - - @occupancy_density.setter - def occupancy_density(self, value): - """ - Set density in persons per m2 - :param value: float - """ - if value is not None: - self._occupancy_density = float(value) - - @property - def dhw_average_volume_pers_day(self) -> Union[None, float]: - """ - Get average DHW consumption in cubic meters per person per day - :return: None or float - """ - return self._dhw_average_volume_pers_day - - @dhw_average_volume_pers_day.setter - def dhw_average_volume_pers_day(self, value): - """ - Set average DHW consumption in cubic meters per person per day - :param value: float - """ - if value is not None: - self._dhw_average_volume_pers_day = float(value) - - @property - def dhw_preparation_temperature(self) -> Union[None, float]: - """ - Get preparation temperature of the DHW in Celsius - :return: None or float - """ - return self._dhw_preparation_temperature - - @dhw_preparation_temperature.setter - def dhw_preparation_temperature(self, value): - """ - Set preparation temperature of the DHW in Celsius - :param value: float - """ - if value is not None: - self._dhw_preparation_temperature = float(value) - @property def electrical_app_average_consumption_sqm_year(self) -> Union[None, float]: """ @@ -285,60 +142,93 @@ class UsageZone: self._electrical_app_average_consumption_sqm_year = float(value) @property - def volume_geometry(self) -> Polyhedron: + def occupancy(self) -> Union[None, Occupancy]: """ - Get the polyhedron defined by the usage zone - :return: Polyhedron + Get occupancy in the usage zone + :return: None or Occupancy """ - return self._volume_geometry + return self._occupancy + + @occupancy.setter + def occupancy(self, value): + """ + Set occupancy in the usage zone + :param value: Occupancy + """ + self._occupancy = value @property - def volume(self) -> Union[None, float]: + def lighting(self) -> Union[None, Lighting]: """ - Get the volume in cubic meters - :return: None or float + Get lighting information + :return: None or Lighting """ - return self._volume + return self._lighting - @volume.setter - def volume(self, value): + @lighting.setter + def lighting(self, value): """ - Set volume in cubic meters - :param value: float + Set lighting information + :param value: Lighting """ - if value is not None: - self._volume = float(value) + self._lighting = value @property - def is_heated(self) -> Union[None, bool]: + def appliances(self) -> Union[None, Appliances]: """ - Get thermal zone heated flag - :return: None or Boolean + Get appliances information + :return: None or Appliances """ - return self._is_heated + return self._appliances - @is_heated.setter - def is_heated(self, value): + @appliances.setter + def appliances(self, value): """ - Set thermal zone heated flag - :param value: Boolean + Set appliances information + :param value: Appliances """ - if value is not None: - self._is_heated = ast.literal_eval(value) + self._appliances = value - @property - def is_cooled(self) -> Union[None, bool]: + def get_internal_gains(self) -> [InternalGains]: """ - Get thermal zone cooled flag - :return: None or Boolean + Calculates and returns the list of all internal gains defined + :return: InternalGains """ - return self._is_cooled - - @is_cooled.setter - def is_cooled(self, value): - """ - Set thermal zone cooled flag - :param value: Boolean - """ - if value is not None: - self._is_cooled = ast.literal_eval(value) + if self.occupancy is not None: + if self.occupancy.latent_internal_gain is not None: + _internal_gain = InternalGains() + _internal_gain.type = cte.OCCUPANCY + _total_heat_gain = (self.occupancy.sensible_convective_internal_gain + + self.occupancy.sensible_radiant_internal_gain + + self.occupancy.latent_internal_gain) + _internal_gain.average_internal_gain = _total_heat_gain + _internal_gain.latent_fraction = self.occupancy.latent_internal_gain / _total_heat_gain + _internal_gain.radiative_fraction = self.occupancy.sensible_radiant_internal_gain / _total_heat_gain + _internal_gain.convective_fraction = self.occupancy.sensible_convective_internal_gain / _total_heat_gain + _internal_gain.schedule = self.occupancy.occupancy_schedule + self._internal_gains = [_internal_gain] + if self.lighting is not None: + _internal_gain = InternalGains() + _internal_gain.type = cte.LIGHTING + _internal_gain.average_internal_gain = self.lighting.lighting_density + _internal_gain.latent_fraction = self.lighting.latent_fraction + _internal_gain.radiative_fraction = self.lighting.radiant_fraction + _internal_gain.convective_fraction = self.lighting.convective_fraction + _internal_gain.schedule = self.lighting.schedule + if self._internal_gains is not None: + self._internal_gains.append(_internal_gain) + else: + self._internal_gains = [_internal_gain] + if self.appliances is not None: + _internal_gain = InternalGains() + _internal_gain.type = cte.APPLIANCES + _internal_gain.average_internal_gain = self.appliances.lighting_density + _internal_gain.latent_fraction = self.appliances.latent_fraction + _internal_gain.radiative_fraction = self.appliances.radiant_fraction + _internal_gain.convective_fraction = self.appliances.convective_fraction + _internal_gain.schedule = self.appliances.schedule + if self._internal_gains is not None: + self._internal_gains.append(_internal_gain) + else: + self._internal_gains = [_internal_gain] + return self._internal_gains diff --git a/city_model_structure/city.py b/city_model_structure/city.py index 8145665f..cf3e78b0 100644 --- a/city_model_structure/city.py +++ b/city_model_structure/city.py @@ -20,11 +20,10 @@ from city_model_structure.buildings_cluster import BuildingsCluster from city_model_structure.parts_consisting_building import PartsConsistingBuilding from city_model_structure.subway_entrance import SubwayEntrance from city_model_structure.fuel import Fuel -from city_model_structure.machine import Machine from helpers.geometry_helper import GeometryHelper from helpers.location import Location from city_model_structure.energy_system import EnergySystem -from city_model_structure.lca_material import LcaMaterial + class City: """ @@ -53,9 +52,6 @@ class City: self._city_objects = None self._energy_systems = None self._fuels = None - self._machines = None - self._stations = [] - self._lca_materials = None @property def fuels(self) -> [Fuel]: @@ -65,14 +61,6 @@ class City: def fuels(self, value): self._fuels = value - @property - def machines(self) -> [Machine]: - return self._machines - - @machines.setter - def machines(self, value): - self._machines = value - def _get_location(self) -> Location: if self._location is None: gps = pyproj.CRS('EPSG:4326') # LatLon with WGS84 datum used by GPS units and Google Earth @@ -195,7 +183,7 @@ class City: :return: None or CityObject """ for city_object in self.buildings: - if str(city_object.name) == str(name): + if city_object.name == name: return city_object return None @@ -361,19 +349,11 @@ class City: @property def energy_systems(self) -> Union[List[EnergySystem], None]: """ - Get energy systems belonging to the city - :return: None or [EnergySystem] - """ + Get energy systems belonging to the city + :return: None or [EnergySystem] + """ return self._energy_systems - @property - def stations(self) -> [Station]: - """ - Get the sensors stations belonging to the city - :return: [Station] - """ - return self._stations - @property def city_objects_clusters(self) -> Union[List[CityObjectsCluster], None]: """ @@ -404,29 +384,3 @@ class City: self._parts_consisting_buildings.append(new_city_objects_cluster) else: raise NotImplementedError - - @property - def lca_materials(self) -> Union[List[LcaMaterial], None]: - """ - Get life cycle materials for the city - :return: [LcaMaterial] or - """ - return self._lca_materials - - @lca_materials.setter - def lca_materials(self, value): - """ - Set life cycle materials for the city - """ - self._lca_materials = value - - def lca_material(self, lca_id) -> LcaMaterial: - """ - Get the lca materiol matching the given Id - :return: LcaMaterial or None - """ - for lca_material in self.lca_materials: - if str(lca_material.id) == str(lca_id): - return lca_material - return None - diff --git a/city_model_structure/city_object.py b/city_model_structure/city_object.py index 2147909b..d71e3cea 100644 --- a/city_model_structure/city_object.py +++ b/city_model_structure/city_object.py @@ -36,13 +36,21 @@ class CityObject: self._beam = dict() self._sensors = [] + @property + def name(self): + """ + Get building name + :return: str + """ + return self._name + @property def lod(self) -> int: """ Get city object level of detail 1, 2, 3 or 4 :return: int """ - lod = math.log(self._lod, 2) + 1 + lod = int(math.log(self._lod, 2) + 1) return lod @property diff --git a/city_model_structure/energy_systems/hvac_system.py b/city_model_structure/energy_systems/hvac_system.py new file mode 100644 index 00000000..4bc6edf3 --- /dev/null +++ b/city_model_structure/energy_systems/hvac_system.py @@ -0,0 +1,32 @@ +""" +HvacSystem module +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2022 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +""" +from typing import Union + + +class HvacSystem: + """ + HvacSystem class + """ + def __init__(self): + self._type = None + + @property + def type(self) -> Union[None, str]: + """ + Get hvac system type a thermal zone + :return: None or str + """ + return self._type + + @type.setter + def type(self, value): + """ + Set heating set point defined for a thermal zone + :param value: str + """ + if value is not None: + self._type = str(value) + diff --git a/city_model_structure/iot/concordia_energy_sensor.py b/city_model_structure/iot/concordia_energy_sensor.py new file mode 100644 index 00000000..afc7b930 --- /dev/null +++ b/city_model_structure/iot/concordia_energy_sensor.py @@ -0,0 +1,45 @@ +""" +Energy Sensor module +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@concordia.ca +""" + +import pandas as pd +from city_model_structure.iot.sensor import Sensor + + +class ConcordiaEnergySensor(Sensor): + """ + Concordia energy sensor. + """ + + def __init__(self, name): + super().__init__() + self._name = name + self._interval = 5 + self._interval_units = 'minutes' + self._type = 'ConcordiaEnergySensor' + self._units = 'kW' + self._measures = pd.DataFrame(columns=["Date time", "Energy consumption"]) + + @property + def measures(self) -> pd.DataFrame: + """ + Get sensor measures [yyyy-mm-dd, hh:mm:ss kW] + :return: DataFrame["Date time", "Energy consumption"] + """ + return self._measures + + @measures.deleter + def measures(self): + """ + Delete sensor measures + """ + self._measures.drop = None + + def add_period(self, measures): + """ + Add or update a period measures to the dataframe + """ + measures = self._measures.append(measures, ignore_index=True) + self._measures = measures.drop_duplicates('Date time', keep='last') diff --git a/city_model_structure/iot/concordia_gas_flow_sensor.py b/city_model_structure/iot/concordia_gas_flow_sensor.py new file mode 100644 index 00000000..86da31bd --- /dev/null +++ b/city_model_structure/iot/concordia_gas_flow_sensor.py @@ -0,0 +1,45 @@ +""" +Gas Flow Sensor module +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +""" + +import pandas as pd +from city_model_structure.iot.sensor import Sensor + + +class ConcordiaGasFlowSensor(Sensor): + """ + Concordia gas flow sensor. + """ + + def __init__(self, name): + super().__init__() + self._name = name + self._interval = 5 + self._interval_units = 'minutes' + self._type = 'ConcordiaGasFlowSensor' + self._units = 'm3' + self._measures = pd.DataFrame(columns=["Date time", "Gas Flow Cumulative Monthly"]) + + @property + def measures(self) -> pd.DataFrame: + """ + Get sensor measures [yyyy-mm-dd, hh:mm:ss m3] + :return: DataFrame["Date time", "Gas Flow Cumulative Monthly"] + """ + return self._measures + + @measures.deleter + def measures(self): + """ + Delete sensor measures + """ + self._measures.drop = None + + def add_period(self, measures): + """ + Add or update a period measures to the dataframe + """ + measures = self._measures.append(measures, ignore_index=True) + self._measures = measures.drop_duplicates('Date time', keep='last') diff --git a/city_model_structure/iot/concordia_temperature_sensor.py b/city_model_structure/iot/concordia_temperature_sensor.py new file mode 100644 index 00000000..9ae6079c --- /dev/null +++ b/city_model_structure/iot/concordia_temperature_sensor.py @@ -0,0 +1,45 @@ +""" +Temperature Sensor module +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +""" + +import pandas as pd +from city_model_structure.iot.sensor import Sensor + + +class ConcordiaTemperatureSensor(Sensor): + """ + Concordia temperature sensor. + """ + + def __init__(self, name): + super().__init__() + self._name = name + self._interval = 5 + self._interval_units = 'minutes' + self._type = 'ConcordiaTemperatureSensor' + self._units = 'Celsius' + self._measures = pd.DataFrame(columns=["Date time", "Temperature"]) + + @property + def measures(self) -> pd.DataFrame: + """ + Get sensor measures [yyyy-mm-dd, hh:mm:ss Celsius] + :return: DataFrame["Date time", "Temperature"] + """ + return self._measures + + @measures.deleter + def measures(self): + """ + Delete sensor measures + """ + self._measures.drop = None + + def add_period(self, measures): + """ + Add or update a period measures to the dataframe + """ + measures = self._measures.append(measures, ignore_index=True) + self._measures = measures.drop_duplicates('Date time', keep='last') diff --git a/city_model_structure/iot/sensor.py b/city_model_structure/iot/sensor.py index c3801749..4eceafd1 100644 --- a/city_model_structure/iot/sensor.py +++ b/city_model_structure/iot/sensor.py @@ -1,73 +1,76 @@ """ Sensor module SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2022 Project Author Guille Gutierrez guillermo.gutierrezmorote@concordia.ca +Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@concordia.ca """ -import uuid -from city_model_structure.iot.sensor_measure import SensorMeasure -from city_model_structure.iot.sensor_type import SensorType +from helpers.location import Location class Sensor: """ Sensor abstract class """ - def __init__(self, sensor_id=None, model=None, sensor_type=None, indoor=False, board=None): - self._id = sensor_id - self._model = model - self._type = sensor_type - self._indoor = indoor - self._board = board - self._measures = [] + def __init__(self): + self._name = None + self._type = None + self._units = None + self._location = None @property - def id(self): + def name(self): """ - Get the sensor id a random uuid will be assigned if no ID was provided to the constructor - :return: Id + Get sensor name + :return: str """ - if self._id is None: - self._id = uuid.uuid4() - return self._id + if self._name is None: + raise ValueError('Undefined sensor name') + return self._name + + @name.setter + def name(self, value): + """ + Set sensor name + :param value: str + """ + if value is not None: + self._name = str(value) @property - def type(self) -> SensorType: + def type(self): """ Get sensor type - :return: SensorTypeEnum or Error + :return: str """ - if self._type is None: - raise ValueError('Unknown sensor type') return self._type @property - def model(self): + def units(self): """ - Get sensor model is any - :return: str or None + Get sensor units + :return: str """ - return self._model + return self._units @property - def board(self): + def location(self) -> Location: """ - Get sensor board if any - :return: str or None + Get sensor location + :return: Location """ - return self._model + return self._location + + @location.setter + def location(self, value): + """ + Set sensor location + :param value: Location + """ + self._location = value @property - def indoor(self): - """ - Get is the sensor it's located indoor or outdoor - :return: boolean - """ - return self._indoor - - @property - def measures(self) -> [SensorMeasure]: + def measures(self): """ Raises not implemented error """ - return self._measures + raise NotImplementedError diff --git a/city_model_structure/iot/sensor_measure.py b/city_model_structure/iot/sensor_measure.py deleted file mode 100644 index afdd40db..00000000 --- a/city_model_structure/iot/sensor_measure.py +++ /dev/null @@ -1,40 +0,0 @@ -""" -Sensor measure module -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2022 Project Author Guille Gutierrez guillermo.gutierrezmorote@concordia.ca -""" - -class SensorMeasure: - def __init__(self, latitude, longitude, utc_timestamp, value): - self._latitude = latitude - self._longitude = longitude - self._utc_timestamp = utc_timestamp - self._value = value - - @property - def latitude(self): - """ - Get measure latitude - """ - return self._latitude - - @property - def longitude(self): - """ - Get measure longitude - """ - return self._longitude - - @property - def utc_timestamp(self): - """ - Get measure timestamp in utc - """ - return self._utc_timestamp - - @property - def value(self): - """ - Get sensor measure value - """ - return self._value \ No newline at end of file diff --git a/city_model_structure/iot/sensor_type.py b/city_model_structure/iot/sensor_type.py deleted file mode 100644 index 414b6f60..00000000 --- a/city_model_structure/iot/sensor_type.py +++ /dev/null @@ -1,20 +0,0 @@ -""" -Sensor type module -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2022 Project Author Guille Gutierrez guillermo.gutierrezmorote@concordia.ca -""" - -from enum import Enum - -class SensorType(Enum): - HUMIDITY = 0 - TEMPERATURE = 1 - CO2 = 2 - NOISE = 3 - PRESSURE = 4 - DIRECT_RADIATION = 5 - DIFFUSE_RADIATION = 6 - GLOBAL_RADIATION = 7 - AIR_QUALITY = 8 - GAS_FLOW = 9 - ENERGY = 10 diff --git a/city_model_structure/iot/station.py b/city_model_structure/iot/station.py deleted file mode 100644 index 8a15e62d..00000000 --- a/city_model_structure/iot/station.py +++ /dev/null @@ -1,41 +0,0 @@ -""" -Station -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2022 Project Author Guille Gutierrez guillermo.gutierrezmorote@concordia.ca -""" -import uuid - -from city_model_structure.iot.sensor import Sensor - - -class Station: - def __init__(self, station_id=None, _mobile=False): - self._id = station_id - self._mobile = _mobile - self._sensors = [] - - @property - def id(self): - """ - Get the station id a random uuid will be assigned if no ID was provided to the constructor - :return: Id - """ - if self._id is None: - self._id = uuid.uuid4() - return self._id - - @property - def _mobile(self): - """ - Get if the station is mobile or not - :return: bool - """ - return self._mobile - - @property - def sensors(self) -> [Sensor]: - """ - Get the sensors belonging to the station - :return: [Sensor] - """ - return self._sensors diff --git a/city_model_structure/lca_calculations.py b/city_model_structure/lca_calculations.py new file mode 100644 index 00000000..032edcd6 --- /dev/null +++ b/city_model_structure/lca_calculations.py @@ -0,0 +1,23 @@ +""" +LifeCycleAssessment retrieve the specific Life Cycle Assessment module for the given region +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2020 Project Author Atiya +""" +from city_model_structure.machine import Machine + +class LcaCalculations: + """ + LCA Calculations class + """ + + def __init__(self): + print("lca calculations class") + + def emission_disposal_machines(self, ): + return Machine.work_efficiency * Machine.energy_consumption_rate * Machine.carbon_emission_factor + + def emission_transportation(self, weight, distance ): + return weight * distance * Machine.energy_consumption_rate * Machine.carbon_emission_factor + + + diff --git a/city_model_structure/lca_material.py b/city_model_structure/lca_material.py deleted file mode 100644 index 6d73384b..00000000 --- a/city_model_structure/lca_material.py +++ /dev/null @@ -1,242 +0,0 @@ -""" -Material module -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2020 Project Author Atiya atiya.atiya@mail.concordia.ca -Contributor Mohammad Reza mohammad.seyedabadi@mail.concordia.ca -""" - -from typing import Union - -class LcaMaterial: - def __init__(self): - self._id = None - self._type = None - self._name = None - self._density = None - self._density_unit = None - self._embodied_carbon = None - self._embodied_carbon_unit = None - self._recycling_ratio = None - self._company_recycling_ratio = None - self._onsite_recycling_ratio = None - self._landfilling_ratio = None - self._cost = None - self._cost_unit = None - - @property - def id(self): - """ - Get material id - :return: int - """ - return self._id - - @id.setter - def id(self, value): - """ - Set material id - :param value: int - """ - self._id = int(value) - - @property - def type(self): - """ - Get material type - :return: str - """ - return self._type - - @type.setter - def type(self, value): - """ - Set material type - :param value: string - """ - self._type = str(value) - - @property - def name(self): - """ - Get material name - :return: str - """ - return self._name - - @name.setter - def name(self, value): - """ - Set material name - :param value: string - """ - self._name = str(value) - - @property - def density(self) -> Union[None, float]: - """ - Get material density in kg/m3 - :return: None or float - """ - return self._density - - @density.setter - def density(self, value): - """ - Set material density - :param value: float - """ - if value is not None: - self._density = float(value) - - @property - def density_unit(self) -> Union[None, str]: - """ - Get material density unit - :return: None or string - """ - return self._density_unit - - @density_unit.setter - def density_unit(self, value): - """ - Set material density unit - :param value: string - """ - if value is not None: - self._density_unit = str(value) - - @property - def embodied_carbon(self) -> Union[None, float]: - """ - Get material embodied carbon - :return: None or float - """ - return self._embodied_carbon - - @embodied_carbon.setter - def embodied_carbon(self, value): - """ - Set material embodied carbon - :param value: float - """ - if value is not None: - self._embodied_carbon = float(value) - - @property - def embodied_carbon_unit(self) -> Union[None, str]: - """ - Get material embodied carbon unit - :return: None or string - """ - return self._embodied_carbon - - @embodied_carbon_unit.setter - def embodied_carbon_unit(self, value): - """ - Set material embodied carbon unit - :param value: string - """ - if value is not None: - self._embodied_carbon_unit = str(value) - - @property - def recycling_ratio(self) -> Union[None, float]: - """ - Get material recycling ratio - :return: None or float - """ - return self._recycling_ratio - - @recycling_ratio.setter - def recycling_ratio(self, value): - """ - Set material recycling ratio - :param value: float - """ - if value is not None: - self._recycling_ratio = float(value) - - @property - def onsite_recycling_ratio(self) -> Union[None, float]: - """ - Get material onsite recycling ratio - :return: None or float - """ - return self._onsite_recycling_ratio - - @onsite_recycling_ratio.setter - def onsite_recycling_ratio(self, value): - """ - Set material onsite recycling ratio - :param value: float - """ - if value is not None: - self._onsite_recycling_ratio = float(value) - - @property - def company_recycling_ratio(self) -> Union[None, float]: - """ - Get material company recycling ratio - :return: None or float - """ - return self._company_recycling_ratio - - @company_recycling_ratio.setter - def company_recycling_ratio(self, value): - """ - Set material company recycling ratio - :param value: float - """ - if value is not None: - self._company_recycling_ratio = float(value) - - @property - def landfilling_ratio(self) -> Union[None, float]: - """ - Get material landfilling ratio - :return: None or float - """ - return self._landfilling_ratio - - @landfilling_ratio.setter - def landfilling_ratio(self, value): - """ - Set material landfilling ratio - :param value: float - """ - if value is not None: - self._landfilling_ratio = float(value) - - @property - def cost(self) -> Union[None, float]: - """ - Get material cost - :return: None or float - """ - return self._cost - - @cost.setter - def cost(self, value): - """ - Set material cost - :param value: float - """ - if value is not None: - self._cost = float(value) - - @property - def cost_unit(self) -> Union[None, str]: - """ - Get material cost unit - :return: None or string - """ - return self._cost_unit - - @cost_unit.setter - def cost_unit(self, value): - """ - Set material cost unit - :param value: string - """ - if value is not None: - self._cost_unit = float(value) \ No newline at end of file diff --git a/config/configuration.ini b/config/configuration.ini index b2ddfb04..399649f1 100644 --- a/config/configuration.ini +++ b/config/configuration.ini @@ -11,3 +11,5 @@ comnet_occupancy_sensible_radiant = 0.1 comnet_plugs_latent = 0 comnet_plugs_convective = 0.75 comnet_plugs_radiant = 0.25 +convective_heat_transfer_coefficient_interior = 3.5 +convective_heat_transfer_coefficient_exterior = 20 diff --git a/data/construction/ca_constructions_reduced.xml b/data/construction/ca_constructions_reduced.xml index 38a217d9..42bf460e 100644 --- a/data/construction/ca_constructions_reduced.xml +++ b/data/construction/ca_constructions_reduced.xml @@ -105,84 +105,52 @@ #wall below grade 0.512 - 0 - 0 0.512 - 0 - 0 0.67 - 0 - 0 0.848 - 0 - 0 1.048 - 0 - 0 1.154 - 0 - 0 1.243 - 0 - 0 1.425 - 0 - 0 #slab on grade 0.512 - 0 - 0 0.67 - 0 - 0 0.67 - 0 - 0 0.848 - 0 - 0 0.848 - 0 - 0 1.05 - 0 - 0 1.154 - 0 - 0 1.154 - 0 - 0 diff --git a/data/construction/us_constructions.xml b/data/construction/us_constructions.xml index 2d4a27f8..b7a2ab51 100644 --- a/data/construction/us_constructions.xml +++ b/data/construction/us_constructions.xml @@ -39,7 +39,7 @@ - + 1.311 2240 836.8 @@ -47,14 +47,14 @@ 0.7 0.7 - + true 0.21648 0.9 0.7 0.8 - + 0.045 265 836.8 @@ -62,7 +62,7 @@ 0.7 0.7 - + 0.6918 1858 837 @@ -70,7 +70,7 @@ 0.92 0.92 - + 1.7296 2243 837 @@ -78,7 +78,7 @@ 0.65 0.65 - + 0.0432 91 837 @@ -86,7 +86,7 @@ 0.5 0.5 - + 0.16 784.9 830 @@ -94,7 +94,7 @@ 0.92 0.92 - + 0.11 544.62 1210 @@ -102,7 +102,7 @@ 0.78 0.78 - + 0.115 513 1255 @@ -110,7 +110,7 @@ 0.78 0.78 - + 0.1211 593 2510 @@ -118,7 +118,7 @@ 0.78 0.78 - + 0.049 265 836.8 @@ -126,14 +126,14 @@ 0.7 0.7 - + true 0.36256 0.9 0.7 0.7 - + 45.006 7680 418.4 @@ -141,7 +141,7 @@ 0.7 0.3 - + 0.16 1121.29 1460 @@ -149,28 +149,28 @@ 0.7 0.7 - + true 0.21648 0.9 0.7 0.8 - + true 0.21648 0.9 0.7 0.8 - + true 0.21648 0.9 0.7 0.8 - + 0.045 265 836.8 @@ -178,7 +178,7 @@ 0.7 0.7 - + 44.96 7688.86 410 diff --git a/data/greenery/ecore_greenery_catalog.xml b/data/greenery/ecore_greenery_catalog.xml deleted file mode 100644 index 41b7801c..00000000 --- a/data/greenery/ecore_greenery_catalog.xml +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/data/life_cycle_assessment/lca_data.xml b/data/life_cycle_assessment/lca_data.xml index 6e231b60..29bb356d 100644 --- a/data/life_cycle_assessment/lca_data.xml +++ b/data/life_cycle_assessment/lca_data.xml @@ -1,394 +1,5 @@ - - - 1.8 - 560 - 0.8 - 0.3 - 0.7 - 0.2 - .... - - - 1.2 - 310 - 0.8 - 0.3 - 0.7 - 0.2 - .... - - - 2 - 3080 - 0.8 - 0.3 - 0.7 - 0.2 - .... - - - 1.4 - 300 - 0.8 - 0.3 - 0.7 - 0.2 - .... - - - 1.6 - 900 - 0.8 - 0 - 1 - 0.2 - .... - - - 1.6 - 2340 - 0.8 - 0 - 1 - 0.2 - .... - - - 1.6 - 1570 - 0.8 - 0 - 1 - 0.2 - .... - - - 1.4 - 1840 - 0.8 - 0 - 1 - 0.2 - .... - - - 1.3 - 410 - 0.8 - 0 - 1 - 0.2 - .... - - - 2.3 - 160 - 0.8 - 0 - 1 - 0.2 - .... - - - 2.3 - 170 - 0.8 - 0 - 1 - 0.2 - .... - - - 2.3 - 230 - 0.8 - 0 - 1 - 0.2 - .... - - - 2.4 - 240 - 0.8 - 0 - 1 - 0.2 - .... - - - 2.4 - 280 - 0.8 - 0 - 1 - 0.2 - .... - - - 2.3 - 170 - 0.8 - 0 - 1 - 0.2 - .... - - - 1.2 - 440 - 0.8 - 0 - 1 - 0.2 - .... - - - 2.58 - 2660 - 0.95 - 0 - 1 - 0.05 - .... - - - 2.58 - 5260 - 0.95 - 0 - 1 - 0.05 - .... - - - 0.06 - 1760 - 0.9 - 0 - 1 - 0.1 - .... - - - 0.122 - 3080 - 0.9 - 0 - 1 - 0.1 - .... - - - 0.028 - 3180 - 0.9 - 0 - 1 - 0.1 - .... - - - 0.024 - 5140 - 0.9 - 0 - 1 - 0.1 - .... - - - 0.1 - 6040 - 0.9 - 0 - 1 - 0.1 - .... - - - 0.3 - 5380 - 0.9 - 0 - 1 - 0.1 - .... - - - 0.032 - 2150 - 0.9 - 0 - 1 - 0.1 - .... - - - 0.9 - 3420 - 0.6 - 0 - 1 - 0.4 - .... - - - 0.7 - 1430 - 0.6 - 0 - 1 - 0.4 - .... - - - 0.65 - 2780 - 0.6 - 0 - 1 - 0.4 - .... - - - 0.72 - 2190 - 0.6 - 0 - 1 - 0.4 - .... - - - 1.43 - 1070 - 0 - 0 - 0 - 1 - .... - - - 1.43 - 240 - 0 - 0 - 0 - 1 - .... - - - 1.43 - 430 - 0 - 0 - 0 - 1 - .... - - - 1.43 - 340 - 0 - 0 - 0 - 1 - .... - - - 1.2 - 440 - 0.8 - 0 - 1 - 0.2 - .... - - - 2.1 - 1410 - 0.8 - 0 - 1 - 0.2 - .... - - - 1.43 - 250 - 0 - 0 - 0 - 1 - .... - - - 1.44 - 1480 - 0.8 - 0 - 1 - 0.2 - .... - - - 1.44 - 2220 - 0.8 - 0 - 1 - 0.2 - .... - - - 1.27 - 3960 - 0.8 - 0 - 1 - 0.2 - .... - - - 1.15 - 760 - 0.8 - 0 - 1 - 0.2 - .... - - - 8 - 3160 - 0.98 - 0 - 1 - 0.02 - .... - - - 2.7 - 5370 - 0.98 - 0 - 1 - 0.02 - .... - - - 7.85 - 3910 - 0.98 - 0 - 1 - 0.02 - .... - - 0.32 @@ -556,4 +167,393 @@ 1.00000 + + + 1.8 + 560 + 0.8 + 0.3 + 0.7 + 0.2 + .... + + + 1.2 + 310 + 0.8 + 0.3 + 0.7 + 0.2 + .... + + + 2 + 3080 + 0.8 + 0.3 + 0.7 + 0.2 + .... + + + 1.4 + 300 + 0.8 + 0.3 + 0.7 + 0.2 + .... + + + 1.6 + 900 + 0.8 + 0 + 1 + 0.2 + .... + + + 1.6 + 2340 + 0.8 + 0 + 1 + 0.2 + .... + + + 1.6 + 1570 + 0.8 + 0 + 1 + 0.2 + .... + + + 1.4 + 1840 + 0.8 + 0 + 1 + 0.2 + .... + + + 1.3 + 410 + 0.8 + 0 + 1 + 0.2 + .... + + + 2.3 + 160 + 0.8 + 0 + 1 + 0.2 + .... + + + 2.3 + 170 + 0.8 + 0 + 1 + 0.2 + .... + + + 2.3 + 230 + 0.8 + 0 + 1 + 0.2 + .... + + + 2.4 + 240 + 0.8 + 0 + 1 + 0.2 + .... + + + 2.4 + 280 + 0.8 + 0 + 1 + 0.2 + .... + + + 2.3 + 170 + 0.8 + 0 + 1 + 0.2 + .... + + + 1.2 + 440 + 0.8 + 0 + 1 + 0.2 + .... + + + 2.58 + 2660 + 0.95 + 0 + 1 + 0.05 + .... + + + 2.58 + 5260 + 0.95 + 0 + 1 + 0.05 + .... + + + 0.06 + 1760 + 0.9 + 0 + 1 + 0.1 + .... + + + 0.122 + 3080 + 0.9 + 0 + 1 + 0.1 + .... + + + 0.028 + 3180 + 0.9 + 0 + 1 + 0.1 + .... + + + 0.024 + 5140 + 0.9 + 0 + 1 + 0.1 + .... + + + 0.1 + 6040 + 0.9 + 0 + 1 + 0.1 + .... + + + 0.3 + 5380 + 0.9 + 0 + 1 + 0.1 + .... + + + 0.032 + 2150 + 0.9 + 0 + 1 + 0.1 + .... + + + 0.9 + 3420 + 0.6 + 0 + 1 + 0.4 + .... + + + 0.7 + 1430 + 0.6 + 0 + 1 + 0.4 + .... + + + 0.65 + 2780 + 0.6 + 0 + 1 + 0.4 + .... + + + 0.72 + 2190 + 0.6 + 0 + 1 + 0.4 + .... + + + 1.43 + 1070 + 0 + 0 + 0 + 1 + .... + + + 1.43 + 240 + 0 + 0 + 0 + 1 + .... + + + 1.43 + 430 + 0 + 0 + 0 + 1 + .... + + + 1.43 + 340 + 0 + 0 + 0 + 1 + .... + + + 1.2 + 440 + 0.8 + 0 + 1 + 0.2 + .... + + + 2.1 + 1410 + 0.8 + 0 + 1 + 0.2 + .... + + + 1.43 + 250 + 0 + 0 + 0 + 1 + .... + + + 1.44 + 1480 + 0.8 + 0 + 1 + 0.2 + .... + + + 1.44 + 2220 + 0.8 + 0 + 1 + 0.2 + .... + + + 1.27 + 3960 + 0.8 + 0 + 1 + 0.2 + .... + + + 1.15 + 760 + 0.8 + 0 + 1 + 0.2 + .... + + + 8 + 3160 + 0.98 + 0 + 1 + 0.02 + .... + + + 2.7 + 5370 + 0.98 + 0 + 1 + 0.02 + .... + + + 7.85 + 3910 + 0.98 + 0 + 1 + 0.02 + .... + + \ No newline at end of file diff --git a/exports/formats/energy_ade.py b/exports/formats/energy_ade.py index 60c10af2..cf335397 100644 --- a/exports/formats/energy_ade.py +++ b/exports/formats/energy_ade.py @@ -329,7 +329,7 @@ class EnergyAde: }, 'energy:area': { '@uom': 'm2', - '#text': f'{thermal_boundary.area}' + '#text': f'{thermal_boundary.opaque_area}' }, 'energy:surfaceGeometry': { 'gml:MultiSurface': { diff --git a/helpers/configuration_helper.py b/helpers/configuration_helper.py index 85677ceb..51d9a3d8 100644 --- a/helpers/configuration_helper.py +++ b/helpers/configuration_helper.py @@ -105,3 +105,19 @@ class ConfigurationHelper: :return: 0.1 """ return self._config.getfloat('buildings', 'comnet_occupancy_sensible_radiant').real + + @property + def convective_heat_transfer_coefficient_interior(self) -> float: + """ + Get configured convective heat transfer coefficient for surfaces inside the building + :return: 3.5 + """ + return self._config.getfloat('buildings', 'convective_heat_transfer_coefficient_interior').real + + @property + def convective_heat_transfer_coefficient_exterior(self) -> float: + """ + Get configured convective heat transfer coefficient for surfaces outside the building + :return: 20 + """ + return self._config.getfloat('buildings', 'convective_heat_transfer_coefficient_exterior').real diff --git a/helpers/constants.py b/helpers/constants.py index 4ce228d4..b55cf955 100644 --- a/helpers/constants.py +++ b/helpers/constants.py @@ -85,11 +85,22 @@ EDUCATION = 'education' LIGHTING = 'Lights' OCCUPANCY = 'Occupancy' -RECEPTACLE = 'Receptacle' +APPLIANCES = 'Appliances' HVAC_AVAILABILITY = 'HVAC Avail' INFILTRATION = 'Infiltration' COOLING_SET_POINT = 'ClgSetPt' HEATING_SET_POINT = 'HtgSetPt' -# todo: are any of these two the receptacle concept?? EQUIPMENT = 'Equipment' ACTIVITY = 'Activity' + +# Geometry +EPSILON = 0.0000001 + +# HVAC types +ONLY_HEATING = 'Heating' +ONLY_COOLING = 'Colling' +ONLY_VENTILATION = 'Ventilation' +HEATING_AND_VENTILATION = 'Heating and ventilation' +COOLING_AND_VENTILATION = 'Cooling and ventilation' +HEATING_AND_COLLING = 'Heating and cooling' +FULL_HVAC = 'Heating and cooling and ventilation' diff --git a/helpers/monthly_to_hourly_demand.py b/helpers/monthly_to_hourly_demand.py index 4251507f..029a42dd 100644 --- a/helpers/monthly_to_hourly_demand.py +++ b/helpers/monthly_to_hourly_demand.py @@ -5,7 +5,7 @@ Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.mons """ import calendar as cal import pandas as pd -from city_model_structure.building_demand.occupants import Occupants +from city_model_structure.building_demand.occupant import Occupant import helpers.constants as cte @@ -34,7 +34,7 @@ class MonthlyToHourlyDemand: # todo: if these are data frames, then they should be called as (Occupancy should be in low case): # usage_zone.schedules.Occupancy # self._conditioning_seasons.heating - occupancy = Occupants().get_complete_year_schedule(usage_zone.schedules['Occupancy']) + occupancy = Occupant().get_complete_year_schedule(usage_zone.schedules['Occupancy']) heating_schedule = self._conditioning_seasons['heating'] hourly_heating = [] @@ -92,7 +92,7 @@ class MonthlyToHourlyDemand: for usage_zone in self._building.usage_zones: temp_set = float(usage_zone.cooling_setpoint) temp_back = 100 - occupancy = Occupants().get_complete_year_schedule(usage_zone.schedules['Occupancy']) + occupancy = Occupant().get_complete_year_schedule(usage_zone.schedules['Occupancy']) cooling_schedule = self._conditioning_seasons['cooling'] hourly_cooling = [] diff --git a/imports/construction/ca_physics_parameters.py b/imports/construction/ca_physics_parameters.py index 25f3e1de..d04c81ea 100644 --- a/imports/construction/ca_physics_parameters.py +++ b/imports/construction/ca_physics_parameters.py @@ -33,7 +33,14 @@ class CaPhysicsParameters(NrelPhysicsInterface): f'and building year of construction: {building.year_of_construction}\n') continue - self._create_storeys(building, archetype) + # if building has no thermal zones defined from geometry, one thermal zone per storey is assigned + if building.thermal_zones is None: + self._create_storeys(building, archetype) + thermal_zones = [] + for storey in building.storeys: + thermal_zones.append(storey.thermal_zone) + building.thermal_zones = thermal_zones + self._assign_values(building, archetype) def _search_archetype(self, function, year_of_construction): @@ -60,11 +67,15 @@ class CaPhysicsParameters(NrelPhysicsInterface): thermal_boundary.u_value = thermal_boundary_archetype.overall_u_value thermal_boundary.outside_solar_absorptance = thermal_boundary_archetype.outside_solar_absorptance thermal_boundary.construction_name = thermal_boundary_archetype.construction_name - thermal_boundary.window_ratio = thermal_boundary_archetype.window_ratio + try: + thermal_boundary.window_ratio = thermal_boundary_archetype.window_ratio + except ValueError: + # This is the normal operation way when the windows are defined in the geometry + continue if thermal_boundary.thermal_openings is not None: for thermal_opening in thermal_boundary.thermal_openings: - if thermal_boundary_archetype.thermal_opening is not None: - thermal_opening_archetype = thermal_boundary_archetype.thermal_opening + if thermal_boundary_archetype.thermal_opening_archetype is not None: + thermal_opening_archetype = thermal_boundary_archetype.thermal_opening_archetype thermal_opening.frame_ratio = thermal_opening_archetype.frame_ratio thermal_opening.g_value = thermal_opening_archetype.g_value thermal_opening.overall_u_value = thermal_opening_archetype.overall_u_value diff --git a/imports/construction/data_classes/nrel_building_achetype.py b/imports/construction/data_classes/building_achetype.py similarity index 89% rename from imports/construction/data_classes/nrel_building_achetype.py rename to imports/construction/data_classes/building_achetype.py index 3956fb98..fb35bad6 100644 --- a/imports/construction/data_classes/nrel_building_achetype.py +++ b/imports/construction/data_classes/building_achetype.py @@ -1,15 +1,15 @@ """ -NrelBuildingArchetype stores construction information by building archetypes +BuildingArchetype stores construction information by building archetypes SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ from typing import List -from imports.construction.data_classes.nrel_thermal_boundary_archetype import NrelThermalBoundaryArchetype +from imports.construction.data_classes.thermal_boundary_archetype import ThermalBoundaryArchetype -class NrelBuildingArchetype: +class BuildingArchetype: """ - NrelBuildingArchetype class + BuildingArchetype class """ def __init__(self, archetype_keys, average_storey_height, storeys_above_ground, effective_thermal_capacity, additional_thermal_bridge_u_value, indirectly_heated_area_ratio, infiltration_rate_system_off, @@ -89,7 +89,7 @@ class NrelBuildingArchetype: return self._infiltration_rate_system_on @property - def thermal_boundary_archetypes(self) -> List[NrelThermalBoundaryArchetype]: + def thermal_boundary_archetypes(self) -> List[ThermalBoundaryArchetype]: """ Get thermal boundary archetypes associated to the building archetype :return: list of boundary archetypes diff --git a/imports/construction/data_classes/nrel_layer_archetype.py b/imports/construction/data_classes/layer_archetype.py similarity index 67% rename from imports/construction/data_classes/nrel_layer_archetype.py rename to imports/construction/data_classes/layer_archetype.py index 46e0ea64..adad7439 100644 --- a/imports/construction/data_classes/nrel_layer_archetype.py +++ b/imports/construction/data_classes/layer_archetype.py @@ -1,17 +1,16 @@ """ -NrelLayerArchetype stores layer and materials information, complementing the NrelBuildingArchetype class +LayerArchetype stores layer and materials information, complementing the BuildingArchetype class SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ -class NrelLayerArchetype: +class LayerArchetype: """ - NrelLayerArchetype class + LayerArchetype class """ def __init__(self, name, solar_absorptance, thermal_absorptance, visible_absorptance, thickness=None, - conductivity=None, specific_heat=None, density=None, no_mass=False, thermal_resistance=None, - lca_id=None): + conductivity=None, specific_heat=None, density=None, no_mass=False, thermal_resistance=None): self._thickness = thickness self._conductivity = conductivity self._specific_heat = specific_heat @@ -22,12 +21,11 @@ class NrelLayerArchetype: self._no_mass = no_mass self._name = name self._thermal_resistance = thermal_resistance - self._lca_id = lca_id @property def thickness(self): """ - Get nrel layer archetype thickness in meters + Get thickness in meters :return: float """ return self._thickness @@ -35,7 +33,7 @@ class NrelLayerArchetype: @property def conductivity(self): """ - Get nrel layer archetype conductivity in W/mK + Get conductivity in W/mK :return: float """ return self._conductivity @@ -43,7 +41,7 @@ class NrelLayerArchetype: @property def specific_heat(self): """ - Get nrel layer archetype conductivity in J/kgK + Get specific heat in J/kgK :return: float """ return self._specific_heat @@ -51,7 +49,7 @@ class NrelLayerArchetype: @property def density(self): """ - Get nrel layer archetype density in kg/m3 + Get density in kg/m3 :return: float """ return self._density @@ -59,7 +57,7 @@ class NrelLayerArchetype: @property def solar_absorptance(self): """ - Get nrel layer archetype solar absorptance + Get solar absorptance :return: float """ return self._solar_absorptance @@ -67,7 +65,7 @@ class NrelLayerArchetype: @property def thermal_absorptance(self): """ - Get nrel layer archetype thermal absorptance + Get thermal absorptance :return: float """ return self._thermal_absorptance @@ -75,7 +73,7 @@ class NrelLayerArchetype: @property def visible_absorptance(self): """ - Get nrel layer archetype visible absorptance + Get visible absorptance :return: float """ return self._visible_absorptance @@ -83,7 +81,7 @@ class NrelLayerArchetype: @property def no_mass(self) -> bool: """ - Get nrel layer archetype no mass flag + Get no mass flag :return: Boolean """ return self._no_mass @@ -91,7 +89,7 @@ class NrelLayerArchetype: @property def name(self): """ - Get nrel layer archetype name + Get name :return: str """ return self._name @@ -99,15 +97,7 @@ class NrelLayerArchetype: @property def thermal_resistance(self): """ - Get nrel layer archetype thermal resistance in m2K/W + Get thermal resistance in m2K/W :return: float """ return self._thermal_resistance - - @property - def lca_id(self): - """ - Get nrel lca_id equivalent for the material - :return: int - """ - return self._lca_id diff --git a/imports/construction/data_classes/nrel_thermal_boundary_archetype.py b/imports/construction/data_classes/nrel_thermal_boundary_archetype.py deleted file mode 100644 index 78f78cb6..00000000 --- a/imports/construction/data_classes/nrel_thermal_boundary_archetype.py +++ /dev/null @@ -1,99 +0,0 @@ -""" -NrelThermalBoundaryArchetype stores thermal boundaries information, complementing the NrelBuildingArchetype class -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca -""" -from typing import List - -from imports.construction.data_classes.nrel_layer_archetype import NrelLayerArchetype -from imports.construction.data_classes.nrel_thermal_opening_archetype import NrelThermalOpeningArchetype - - -class NrelThermalBoundaryArchetype: - """ - NrelThermalBoundaryArchetype class - """ - def __init__(self, boundary_type, window_ratio, construction_name, layers, thermal_opening, - outside_solar_absorptance=None, outside_thermal_absorptance=None, outside_visible_absorptance=None, - overall_u_value=None): - self._boundary_type = boundary_type - self._outside_solar_absorptance = outside_solar_absorptance - self._outside_thermal_absorptance = outside_thermal_absorptance - self._outside_visible_absorptance = outside_visible_absorptance - self._window_ratio = window_ratio - self._construction_name = construction_name - self._overall_u_value = overall_u_value - self._layers = layers - self._thermal_opening = thermal_opening - - @property - def boundary_type(self): - """ - Get nrel thermal boundary archetype type - :return: str - """ - return self._boundary_type - - @property - def outside_solar_absorptance(self): - """ - Get nrel thermal boundary archetype outside solar absorptance - :return: float - """ - return self._outside_solar_absorptance - - @property - def outside_thermal_absorptance(self): - """ - Get nrel thermal boundary archetype outside thermal absorptance - :return: float - """ - return self._outside_thermal_absorptance - - @property - def outside_visible_absorptance(self): - """ - Get nrel thermal boundary archetype outside visible absorptance - :return: float - """ - return self._outside_visible_absorptance - - @property - def window_ratio(self): - """ - Get nrel thermal boundary archetype window ratio - :return: float - """ - return self._window_ratio - - @property - def construction_name(self): - """ - Get nrel thermal boundary archetype construction name - :return: str - """ - return self._construction_name - - @property - def layers(self) -> List[NrelLayerArchetype]: - """ - Get nrel thermal boundary archetype layers - :return: [NrelLayerArchetype] - """ - return self._layers - - @property - def thermal_opening(self) -> NrelThermalOpeningArchetype: - """ - Get nrel thermal boundary archetype - :return: NrelThermalOpeningArchetype - """ - return self._thermal_opening - - @property - def overall_u_value(self): - """ - Get nrel thermal boundary archetype overall U-value in W/m2K - :return: float - """ - return self._overall_u_value diff --git a/imports/construction/data_classes/thermal_boundary_archetype.py b/imports/construction/data_classes/thermal_boundary_archetype.py new file mode 100644 index 00000000..11aecf44 --- /dev/null +++ b/imports/construction/data_classes/thermal_boundary_archetype.py @@ -0,0 +1,136 @@ +""" +ThermalBoundaryArchetype stores thermal boundaries information, complementing the BuildingArchetype class +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2022 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +""" +from typing import List + +from imports.construction.data_classes.layer_archetype import LayerArchetype +from imports.construction.data_classes.thermal_opening_archetype import ThermalOpeningArchetype + + +class ThermalBoundaryArchetype: + """ + ThermalBoundaryArchetype class + """ + def __init__(self, boundary_type, window_ratio, construction_name, layers, thermal_opening, + outside_solar_absorptance=None, outside_thermal_absorptance=None, outside_visible_absorptance=None, + overall_u_value=None, shortwave_reflectance=None, inside_emissivity=None, alpha_coefficient=None, + radiative_coefficient=None): + self._boundary_type = boundary_type + self._outside_solar_absorptance = outside_solar_absorptance + self._outside_thermal_absorptance = outside_thermal_absorptance + self._outside_visible_absorptance = outside_visible_absorptance + self._window_ratio = window_ratio + self._construction_name = construction_name + self._overall_u_value = overall_u_value + self._layers = layers + self._thermal_opening_archetype = thermal_opening + self._shortwave_reflectance = shortwave_reflectance + self._inside_emissivity = inside_emissivity + self._alpha_coefficient = alpha_coefficient + self._radiative_coefficient = radiative_coefficient + + @property + def boundary_type(self): + """ + Get type + :return: str + """ + return self._boundary_type + + @property + def outside_solar_absorptance(self): + """ + Get outside solar absorptance + :return: float + """ + return self._outside_solar_absorptance + + @property + def outside_thermal_absorptance(self): + """ + Get outside thermal absorptance + :return: float + """ + return self._outside_thermal_absorptance + + @property + def outside_visible_absorptance(self): + """ + Get outside visible absorptance + :return: float + """ + return self._outside_visible_absorptance + + @property + def window_ratio(self): + """ + Get window ratio + :return: float + """ + return self._window_ratio + + @property + def construction_name(self): + """ + Get construction name + :return: str + """ + return self._construction_name + + @property + def layers(self) -> List[LayerArchetype]: + """ + Get layers + :return: [NrelLayerArchetype] + """ + return self._layers + + @property + def thermal_opening_archetype(self) -> ThermalOpeningArchetype: + """ + Get thermal opening archetype + :return: ThermalOpeningArchetype + """ + return self._thermal_opening_archetype + + @property + def overall_u_value(self): + """ + Get overall U-value in W/m2K + :return: float + """ + return self._overall_u_value + + @property + def shortwave_reflectance(self): + """ + Get shortwave reflectance + :return: float + """ + return self._shortwave_reflectance + + @property + def inside_emissivity(self): + """ + Get emissivity inside + :return: float + """ + return self._inside_emissivity + + @property + def alpha_coefficient(self): + """ + Get alpha coefficient + :return: float + """ + return self._alpha_coefficient + + @property + def radiative_coefficient(self): + """ + Get radiative coefficient + :return: float + """ + return self._radiative_coefficient diff --git a/imports/construction/data_classes/nrel_thermal_opening_archetype.py b/imports/construction/data_classes/thermal_opening_archetype.py similarity index 56% rename from imports/construction/data_classes/nrel_thermal_opening_archetype.py rename to imports/construction/data_classes/thermal_opening_archetype.py index 9f696116..e1966267 100644 --- a/imports/construction/data_classes/nrel_thermal_opening_archetype.py +++ b/imports/construction/data_classes/thermal_opening_archetype.py @@ -1,30 +1,34 @@ """ -NrelThermalOpeningArchetype stores thermal openings information, complementing the NrelBuildingArchetype class +ThermalOpeningArchetype stores thermal openings information, complementing the BuildingArchetype class SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ -class NrelThermalOpeningArchetype: +class ThermalOpeningArchetype: """ - NrelThermalOpeningArchetype class + ThermalOpeningArchetype class """ def __init__(self, conductivity=None, frame_ratio=None, g_value=None, thickness=None, back_side_solar_transmittance_at_normal_incidence=None, - front_side_solar_transmittance_at_normal_incidence=None, shgc=None, overall_u_value=None): + front_side_solar_transmittance_at_normal_incidence=None, overall_u_value=None, + openable_ratio=None, inside_emissivity=None, alpha_coefficient=None, radiative_coefficient=None): self._conductivity = conductivity self._frame_ratio = frame_ratio self._g_value = g_value self._thickness = thickness self._back_side_solar_transmittance_at_normal_incidence = back_side_solar_transmittance_at_normal_incidence self._front_side_solar_transmittance_at_normal_incidence = front_side_solar_transmittance_at_normal_incidence - self._shgc = shgc self._overall_u_value = overall_u_value + self._openable_ratio = openable_ratio + self._inside_emissivity = inside_emissivity + self._alpha_coefficient = alpha_coefficient + self._radiative_coefficient = radiative_coefficient @property def conductivity(self): """ - Get nrel thermal opening archetype conductivity in W/mK + Get conductivity in W/mK :return: float """ return self._conductivity @@ -32,7 +36,7 @@ class NrelThermalOpeningArchetype: @property def frame_ratio(self): """ - Get nrel thermal opening archetype frame ratio + Get frame ratio :return: float """ return self._frame_ratio @@ -40,7 +44,7 @@ class NrelThermalOpeningArchetype: @property def g_value(self): """ - Get nrel thermal opening archetype g-value + Get g-value, also called shgc :return: float """ return self._g_value @@ -48,7 +52,7 @@ class NrelThermalOpeningArchetype: @property def thickness(self): """ - Get nrel thermal opening archetype thickness in meters + Get thickness in meters :return: float """ return self._thickness @@ -56,7 +60,7 @@ class NrelThermalOpeningArchetype: @property def back_side_solar_transmittance_at_normal_incidence(self): """ - Get nrel thermal opening archetype back side solar transmittance at normal incidence + Get back side solar transmittance at normal incidence :return: float """ return self._back_side_solar_transmittance_at_normal_incidence @@ -64,23 +68,47 @@ class NrelThermalOpeningArchetype: @property def front_side_solar_transmittance_at_normal_incidence(self): """ - Get nrel thermal opening archetype front side solar transmittance at normal incidence + Get front side solar transmittance at normal incidence :return: float """ return self._front_side_solar_transmittance_at_normal_incidence - @property - def shgc(self): - """ - Get nrel thermal opening archetype shcg - :return: float - """ - return self._shgc - @property def overall_u_value(self): """ - Get nrel thermal opening archetype overall U-value in W/m2K - :param value: float + Get overall U-value in W/m2K + :return: float """ return self._overall_u_value + + @property + def openable_ratio(self): + """ + Get openable ratio + :return: float + """ + return self._openable_ratio + + @property + def inside_emissivity(self): + """ + Get emissivity inside + :return: float + """ + return self._inside_emissivity + + @property + def alpha_coefficient(self): + """ + Get alpha coefficient + :return: float + """ + return self._alpha_coefficient + + @property + def radiative_coefficient(self): + """ + Get radiative coefficient + :return: float + """ + return self._radiative_coefficient diff --git a/imports/construction/helpers/storeys_generation.py b/imports/construction/helpers/storeys_generation.py index ae89cebb..b67a820d 100644 --- a/imports/construction/helpers/storeys_generation.py +++ b/imports/construction/helpers/storeys_generation.py @@ -1,7 +1,7 @@ """ Storeys generation helper SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +Copyright © 2022 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ import sys import math @@ -22,6 +22,9 @@ class StoreysGeneration: self._building = building self._divide_in_storeys = divide_in_storeys self._storeys = None + self._floor_area = 0 + for ground in building.grounds: + self._floor_area += ground.perimeter_polygon.area @property def storeys(self) -> [Storey]: @@ -34,7 +37,7 @@ class StoreysGeneration: self._building.storeys_above_ground) number_of_storeys = 1 if not self._divide_in_storeys or number_of_storeys == 1: - return [Storey('storey_0', self._building.surfaces, [None, None], self._building.volume)] + return [Storey('storey_0', self._building.surfaces, [None, None], self._building.volume, self._floor_area)] if number_of_storeys == 0: raise Exception('Number of storeys cannot be 0') @@ -79,13 +82,13 @@ class StoreysGeneration: surfaces_child.append(ceiling) volume = ceiling.area_above_ground * height total_volume += volume - storeys.append(Storey(name, surfaces_child, neighbours, volume)) + storeys.append(Storey(name, surfaces_child, neighbours, volume, self._floor_area)) name = 'storey_' + str(number_of_storeys - 1) neighbours = ['storey_' + str(number_of_storeys - 2), None] volume = self._building.volume - total_volume if volume < 0: raise Exception('Error in storeys creation, volume of last storey cannot be lower that 0') - storeys.append(Storey(name, surfaces_child_last_storey, neighbours, volume)) + storeys.append(Storey(name, surfaces_child_last_storey, neighbours, volume, self._floor_area)) return storeys @staticmethod @@ -144,12 +147,12 @@ class StoreysGeneration: """ for storey in self._building.storeys: for thermal_boundary in storey.thermal_boundaries: - if thermal_boundary.surface.type != cte.INTERIOR_WALL or thermal_boundary.surface.type != cte.INTERIOR_SLAB: + if thermal_boundary.type != cte.INTERIOR_WALL or thermal_boundary.type != cte.INTERIOR_SLAB: # external thermal boundary -> only one thermal zone thermal_zones = [storey.thermal_zone] else: # internal thermal boundary -> two thermal zones - grad = np.rad2deg(thermal_boundary.surface.inclination) + grad = np.rad2deg(thermal_boundary.inclination) if grad >= 170: thermal_zones = [storey.thermal_zone, storey.neighbours[0]] else: diff --git a/imports/construction/nrel_physics_interface.py b/imports/construction/nrel_physics_interface.py index f01297b8..9065ca9e 100644 --- a/imports/construction/nrel_physics_interface.py +++ b/imports/construction/nrel_physics_interface.py @@ -6,10 +6,10 @@ Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.mons """ import xmltodict -from imports.construction.data_classes.nrel_building_achetype import NrelBuildingArchetype as nba -from imports.construction.data_classes.nrel_thermal_boundary_archetype import NrelThermalBoundaryArchetype as ntba -from imports.construction.data_classes.nrel_thermal_opening_archetype import NrelThermalOpeningArchetype as ntoa -from imports.construction.data_classes.nrel_layer_archetype import NrelLayerArchetype as nla +from imports.construction.data_classes.building_achetype import BuildingArchetype as nba +from imports.construction.data_classes.thermal_boundary_archetype import ThermalBoundaryArchetype as ntba +from imports.construction.data_classes.thermal_opening_archetype import ThermalOpeningArchetype as ntoa +from imports.construction.data_classes.layer_archetype import LayerArchetype as nla class NrelPhysicsInterface: @@ -71,7 +71,6 @@ class NrelPhysicsInterface: for current_layer in c_lib['layers']['layer']: material_lib = self._search_construction_type('material', current_layer['material']) name = material_lib['@name'] - lca_id = material_lib['@lca_id'] solar_absorptance = material_lib['solar_absorptance']['#text'] thermal_absorptance = material_lib['thermal_absorptance']['#text'] visible_absorptance = material_lib['visible_absorptance']['#text'] @@ -82,7 +81,7 @@ class NrelPhysicsInterface: if units != 'm2 K/W': raise Exception(f'thermal resistance units = {units}, expected m2 K/W') layer = nla(name, solar_absorptance, thermal_absorptance, visible_absorptance, no_mass=no_mass, - thermal_resistance=thermal_resistance, lca_id=lca_id) + thermal_resistance=thermal_resistance) else: thickness = current_layer['thickness']['#text'] units = current_layer['thickness']['@units'] @@ -101,7 +100,7 @@ class NrelPhysicsInterface: if units != 'kg/m3': raise Exception(f'density units = {units}, expected kg/m3') layer = nla(name, solar_absorptance, thermal_absorptance, visible_absorptance, thickness=thickness, - conductivity=conductivity, specific_heat=specific_heat, density=density, lca_id=lca_id) + conductivity=conductivity, specific_heat=specific_heat, density=density) layers.append(layer) thermal_opening = None @@ -150,10 +149,14 @@ class NrelPhysicsInterface: units = c_lib['overall_u_value']['@units'] if units != 'W/m2 K': raise Exception(f'overall U-value units = {units}, expected W/m2 K') - outside_solar_absorptance = c_lib['outside_solar_absorptance']['#text'] - thermal_boundary_archetype = ntba(construction_type, window_ratio, construction_name, layers, - thermal_opening, outside_solar_absorptance=outside_solar_absorptance, - overall_u_value=overall_u_value) + if 'outside_solar_absorptance' in c_lib: + outside_solar_absorptance = c_lib['outside_solar_absorptance']['#text'] + thermal_boundary_archetype = ntba(construction_type, window_ratio, construction_name, layers, + thermal_opening, outside_solar_absorptance=outside_solar_absorptance, + overall_u_value=overall_u_value) + else: + thermal_boundary_archetype = ntba(construction_type, window_ratio, construction_name, layers, + thermal_opening, overall_u_value=overall_u_value) else: thermal_boundary_archetype = ntba(construction_type, window_ratio, construction_name, layers, thermal_opening) diff --git a/imports/construction/us_physics_parameters.py b/imports/construction/us_physics_parameters.py index 76d7b0f4..6e19e96d 100644 --- a/imports/construction/us_physics_parameters.py +++ b/imports/construction/us_physics_parameters.py @@ -41,7 +41,14 @@ class UsPhysicsParameters(NrelPhysicsInterface): f'and building year of construction: {building.year_of_construction}\n') continue - self._create_storeys(building, archetype) + # if building has no thermal zones defined from geometry, one thermal zone per storey is assigned + if building.thermal_zones is None: + self._create_storeys(building, archetype) + thermal_zones = [] + for storey in building.storeys: + thermal_zones.append(storey.thermal_zone) + building.thermal_zones = thermal_zones + self._assign_values(building, archetype) def _search_archetype(self, building_type, standard, climate_zone): @@ -63,12 +70,15 @@ class UsPhysicsParameters(NrelPhysicsInterface): for thermal_boundary in thermal_zone.thermal_boundaries: construction_type = ConstructionHelper.nrel_construction_types[thermal_boundary.type] thermal_boundary_archetype = self._search_construction_in_archetype(archetype, construction_type) - if thermal_boundary_archetype.outside_solar_absorptance is not None: - thermal_boundary.outside_solar_absorptance = thermal_boundary_archetype.outside_solar_absorptance + thermal_boundary.outside_solar_absorptance = thermal_boundary_archetype.outside_solar_absorptance thermal_boundary.outside_thermal_absorptance = thermal_boundary_archetype.outside_thermal_absorptance thermal_boundary.outside_visible_absorptance = thermal_boundary_archetype.outside_visible_absorptance thermal_boundary.construction_name = thermal_boundary_archetype.construction_name - thermal_boundary.window_ratio = thermal_boundary_archetype.window_ratio + try: + thermal_boundary.window_ratio = thermal_boundary_archetype.window_ratio + except ValueError: + # This is the normal operation way when the windows are defined in the geometry + continue thermal_boundary.layers = [] for layer_archetype in thermal_boundary_archetype.layers: layer = Layer() @@ -83,13 +93,11 @@ class UsPhysicsParameters(NrelPhysicsInterface): material.thermal_absorptance = layer_archetype.thermal_absorptance material.visible_absorptance = layer_archetype.visible_absorptance material.thermal_resistance = layer_archetype.thermal_resistance - if layer_archetype.lca_id is not None: - material.lca_id = layer_archetype.lca_id layer.material = material thermal_boundary.layers.append(layer) for thermal_opening in thermal_boundary.thermal_openings: - if thermal_boundary_archetype.thermal_opening is not None: - thermal_opening_archetype = thermal_boundary_archetype.thermal_opening + if thermal_boundary_archetype.thermal_opening_archetype is not None: + thermal_opening_archetype = thermal_boundary_archetype.thermal_opening_archetype thermal_opening.frame_ratio = thermal_opening_archetype.frame_ratio thermal_opening.g_value = thermal_opening_archetype.g_value thermal_opening.conductivity = thermal_opening_archetype.conductivity diff --git a/imports/customized_imports/sanam_customized_usage_parameters.py b/imports/customized_imports/sanam_customized_usage_parameters.py index 6604a8e5..442ec9d3 100644 --- a/imports/customized_imports/sanam_customized_usage_parameters.py +++ b/imports/customized_imports/sanam_customized_usage_parameters.py @@ -7,7 +7,7 @@ Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.mons import sys import xmltodict from imports.geometry.helpers.geometry_helper import GeometryHelper as gh -from imports.usage.data_classes.hft_usage_zone_archetype import HftUsageZoneArchetype as huza +from imports.usage.data_classes.usage_zone_archetype import UsageZoneArchetype as huza import helpers.constants as cte diff --git a/imports/geometry/citygml.py b/imports/geometry/citygml.py index 847c2bef..09803d55 100644 --- a/imports/geometry/citygml.py +++ b/imports/geometry/citygml.py @@ -83,7 +83,7 @@ class CityGml: surfaces = CityGmlLod2(city_object).surfaces else: raise NotImplementedError("Not supported level of detail") - return Building(name, lod, surfaces, year_of_construction, function, self._lower_corner, []) + return Building(name, lod, surfaces, year_of_construction, function, self._lower_corner, terrains=None) def _create_parts_consisting_building(self, city_object): name = city_object['@id'] diff --git a/imports/geometry/obj.py b/imports/geometry/obj.py index 6349921b..3ce1fae3 100644 --- a/imports/geometry/obj.py +++ b/imports/geometry/obj.py @@ -76,6 +76,6 @@ class Obj: perimeter_polygon = solid_polygon surface = Surface(solid_polygon, perimeter_polygon) surfaces.append(surface) - building = Building(name, lod, surfaces, year_of_construction, function, self._lower_corner) + building = Building(name, lod, surfaces, year_of_construction, function, self._lower_corner, terrains=None) self._city.add_city_object(building) return self._city diff --git a/imports/geometry/rhino.py b/imports/geometry/rhino.py index 38c350f0..b9007ecb 100644 --- a/imports/geometry/rhino.py +++ b/imports/geometry/rhino.py @@ -3,16 +3,18 @@ Rhino module parses rhino files and import the geometry into the city model stru SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@concordia.ca """ -import numpy as np from numpy import inf -from rhino3dm import * -from rhino3dm._rhino3dm import MeshType +#from rhino3dm import * +#from rhino3dm._rhino3dm import MeshType +from city_model_structure.attributes.point import Point +import numpy as np +from helpers.configuration_helper import ConfigurationHelper from city_model_structure.attributes.polygon import Polygon from city_model_structure.building import Building -from city_model_structure.building_demand.surface import Surface as LibsSurface from city_model_structure.city import City -from helpers.configuration_helper import ConfigurationHelper +from city_model_structure.building_demand.surface import Surface as LibsSurface +from helpers.constants import EPSILON from imports.geometry.helpers.geometry_helper import GeometryHelper @@ -25,7 +27,19 @@ class Rhino: self._max_x = self._max_y = self._max_z = min_float @staticmethod - def _solid_points(coordinates): + def _in_perimeter(wall, corner): + res = wall.contains_point(Point(corner)) + print(f'belong: {res} wall:({wall.coordinates}) corner: ({corner})') + return res + + @staticmethod + def _add_hole(solid_polygon, hole): + first = solid_polygon.points[0] + points = first + hole.points + solid_polygon.points + return Polygon(points) + + @staticmethod + def _solid_points(coordinates) -> np.ndarray: solid_points = np.fromstring(coordinates, dtype=float, sep=' ') solid_points = GeometryHelper.to_points_matrix(solid_points) return solid_points @@ -82,6 +96,7 @@ class Rhino: windows.append(Polygon(surface.perimeter_polygon.inverse)) else: buildings.append(rhino_object) + print(f'windows: {len(windows)}') # todo: this method will be pretty inefficient for hole in windows: corner = hole.coordinates[0] diff --git a/imports/life_cycle_assessment/lca_machine.py b/imports/life_cycle_assessment/lca_machine.py index dbfe1760..87c35c5c 100644 --- a/imports/life_cycle_assessment/lca_machine.py +++ b/imports/life_cycle_assessment/lca_machine.py @@ -8,7 +8,6 @@ import xmltodict from pathlib import Path from city_model_structure.machine import Machine - class LcaMachine: def __init__(self, city, base_path): self._city = city @@ -17,15 +16,12 @@ class LcaMachine: def enrich(self): self._city.machines = [] - # print(self._base_path) path = Path(self._base_path / 'lca_data.xml').resolve() with open(path) as xml: self._lca = xmltodict.parse(xml.read()) - for machine in self._lca["library"]["machines"]['machine']: - self._city.machines.append(Machine(machine['@id'], machine['@name'], machine['work_efficiency']['#text'], - machine['work_efficiency']['@unit'], - machine['energy_consumption_rate']['#text'], - machine['energy_consumption_rate']['@unit'], - machine['carbon_emission_factor']['#text'], - machine['carbon_emission_factor']['@unit'])) + for machine in self._lca["library"]["machines"]['machine']: + self._city.machines.append(Machine(machine['@id'], machine['@name'], machine['work_efficiency']['#text'], + machine['work_efficiency']['@unit'], machine['energy_consumption_rate']['#text'], + machine['energy_consumption_rate']['@unit'], machine['carbon_emission_factor']['#text'], + machine['carbon_emission_factor']['@unit'])) diff --git a/imports/life_cycle_assessment/lca_material.py b/imports/life_cycle_assessment/lca_material.py index e0081b7c..5044b315 100644 --- a/imports/life_cycle_assessment/lca_material.py +++ b/imports/life_cycle_assessment/lca_material.py @@ -7,8 +7,6 @@ Contributor Mohammad Reza mohammad.seyedabadi@mail.concordia.ca import xmltodict from pathlib import Path from city_model_structure.building_demand.material import Material -from city_model_structure.lca_material import LcaMaterial as LcaMat - class LcaMaterial: def __init__(self, city, base_path): @@ -17,25 +15,26 @@ class LcaMaterial: self._lca = None def enrich(self): - self._city.lca_materials = [] + self._city.materials = [] path = Path(self._base_path / 'lca_data.xml').resolve() with open(path) as xml: self._lca = xmltodict.parse(xml.read()) for material in self._lca["library"]["building_materials"]['material']: - _lca_material = LcaMat() - _lca_material.type = material['@type'] - _lca_material.id = material['@id'] - _lca_material.name = material['@name'] - _lca_material.density = material['density']['#text'] - _lca_material.density_unit = material['density']['@unit'] - _lca_material.embodied_carbon = material['embodied_carbon']['#text'] - _lca_material.embodied_carbon_unit = material['embodied_carbon']['@unit'] - _lca_material.recycling_ratio = material['recycling_ratio'] - _lca_material.onsite_recycling_ratio = material['onsite_recycling_ratio'] - _lca_material.company_recycling_ratio = material['company_recycling_ratio'] - _lca_material.landfilling_ratio = material['landfilling_ratio'] - _lca_material.cost = 10 # todo: change this into material['cost']['#text'] - _lca_material._cost_unit = material['cost']['@unit'] - self._city.lca_materials.append(_lca_material) + _material = Material() + _material.type = material['@type'] + _material.id = material['@id'] + _material.name = material['@name'] + _material.density=material['density']['#text'] + _material.density_unit=material['density']['@unit'] + _material.embodied_carbon=material['embodied_carbon']['#text'] + _material.embodied_carbon_unit=material['embodied_carbon']['@unit'] + _material.recycling_ratio=material['recycling_ratio'] + _material.onsite_recycling_ratio=material['onsite_recycling_ratio'] + _material.company_recycling_ratio=material['company_recycling_ratio'] + _material.landfilling_ratio=material['landfilling_ratio'] + _material.cost=material['cost']['#text'] + _material._cost_unit=material['cost']['@unit'] + + self._city.materials.append(_material) diff --git a/imports/life_cycle_assessment/lca_vehicle.py b/imports/life_cycle_assessment/lca_vehicle.py index 78efead7..05ae7876 100644 --- a/imports/life_cycle_assessment/lca_vehicle.py +++ b/imports/life_cycle_assessment/lca_vehicle.py @@ -8,7 +8,6 @@ import xmltodict from pathlib import Path from city_model_structure.vehicle import Vehicle - class LcaVehicle: def __init__(self, city, base_path): self._city = city @@ -23,6 +22,5 @@ class LcaVehicle: self._lca = xmltodict.parse(xml.read()) for vehicle in self._lca["library"]["vehicles"]['vehicle']: self._city.vehicles.append(Vehicle(vehicle['@id'], vehicle['@name'], vehicle['fuel_consumption_rate']['#text'], - vehicle['fuel_consumption_rate']['@unit'], - vehicle['carbon_emission_factor']['#text'], - vehicle['carbon_emission_factor']['@unit'])) + vehicle['fuel_consumption_rate']['@unit'], vehicle['carbon_emission_factor']['#text'], + vehicle['carbon_emission_factor']['@unit'])) diff --git a/imports/sensors/concordia_energy_consumption.py b/imports/sensors/concordia_energy_consumption.py index e9b233ad..0b006045 100644 --- a/imports/sensors/concordia_energy_consumption.py +++ b/imports/sensors/concordia_energy_consumption.py @@ -5,6 +5,7 @@ Copyright © 2021 Project Author Guille Gutierrez guillermo.gutierrezmorote@conc """ import pandas as pd from imports.sensors.concordia_file_report import ConcordiaFileReport +from city_model_structure.iot.concordia_energy_sensor import ConcordiaEnergySensor class ConcordiaEnergyConsumption(ConcordiaFileReport): @@ -22,9 +23,6 @@ class ConcordiaEnergyConsumption(ConcordiaFileReport): building_measures = [self._measures["Date time"], self._measures[self._sensor_point[self._sensors[i]]]] building_headers = ["Date time", "Energy consumption"] building_energy_consumption = pd.concat(building_measures, keys=building_headers, axis=1) - print(building_energy_consumption) - - """ sensor = ConcordiaEnergySensor(self._sensors[i]) sensor_exist = False for j in range(len(obj.sensors)): @@ -35,4 +33,3 @@ class ConcordiaEnergyConsumption(ConcordiaFileReport): if not sensor_exist: sensor.add_period(building_energy_consumption) obj.sensors.append(sensor) - """ diff --git a/imports/sensors/concordia_gas_flow.py b/imports/sensors/concordia_gas_flow.py index 11dec2e2..e9176fc7 100644 --- a/imports/sensors/concordia_gas_flow.py +++ b/imports/sensors/concordia_gas_flow.py @@ -5,7 +5,7 @@ Copyright © 2021 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.mons """ import pandas as pd from imports.sensors.concordia_file_report import ConcordiaFileReport - +from city_model_structure.iot.concordia_gas_flow_sensor import ConcordiaGasFlowSensor class ConcordiaGasFlow(ConcordiaFileReport): @@ -24,7 +24,6 @@ class ConcordiaGasFlow(ConcordiaFileReport): building_measures = [self._measures["Date time"], self._measures[self._sensor_point[self._sensors[i]]]] building_headers = ["Date time", "Gas Flow Cumulative Monthly"] building_energy_consumption = pd.concat(building_measures, keys=building_headers, axis=1) - """ sensor = ConcordiaGasFlowSensor(self._sensors[i]) sensor_exist = False for j in range(len(obj.sensors)): @@ -35,4 +34,3 @@ class ConcordiaGasFlow(ConcordiaFileReport): if not sensor_exist: sensor.add_period(building_energy_consumption) obj.sensors.append(sensor) - """ \ No newline at end of file diff --git a/imports/sensors/concordia_temperature.py b/imports/sensors/concordia_temperature.py index e1f1cb32..f4b937a8 100644 --- a/imports/sensors/concordia_temperature.py +++ b/imports/sensors/concordia_temperature.py @@ -5,6 +5,7 @@ Copyright © 2021 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.mons """ import pandas as pd from imports.sensors.concordia_file_report import ConcordiaFileReport +from city_model_structure.iot.concordia_temperature_sensor import ConcordiaTemperatureSensor class ConcordiaTemperature(ConcordiaFileReport): @@ -22,7 +23,6 @@ class ConcordiaTemperature(ConcordiaFileReport): building_measures = [self._measures["Date time"], self._measures[self._sensor_point[self._sensors[i]]]] building_headers = ["Date time", "Temperature"] building_energy_consumption = pd.concat(building_measures, keys=building_headers, axis=1) - """ sensor = ConcordiaTemperatureSensor(self._sensors[i]) sensor_exist = False for j in range(len(obj.sensors)): @@ -33,4 +33,3 @@ class ConcordiaTemperature(ConcordiaFileReport): if not sensor_exist: sensor.add_period(building_energy_consumption) obj.sensors.append(sensor) - """ \ No newline at end of file diff --git a/imports/usage/ca_usage_parameters.py b/imports/usage/ca_usage_parameters.py index 01cc85a5..cd921332 100644 --- a/imports/usage/ca_usage_parameters.py +++ b/imports/usage/ca_usage_parameters.py @@ -58,14 +58,14 @@ class CaUsageParameters(HftUsageInterface): # usage_zone.internal_gains when writing usage_zone.internal_gains = archetype.internal_gains. # Therefore, this walk around has been done. internal_gains = [] - for archetype_internal_gain in archetype.internal_gains: + for archetype_internal_gain in archetype.not_detailed_source_mean_annual_internal_gains: internal_gain = InternalGains() internal_gain.average_internal_gain = archetype_internal_gain.average_internal_gain internal_gain.convective_fraction = archetype_internal_gain.convective_fraction internal_gain.radiative_fraction = archetype_internal_gain.radiative_fraction internal_gain.latent_fraction = archetype_internal_gain.latent_fraction internal_gains.append(internal_gain) - usage_zone.internal_gains = internal_gains + usage_zone.not_detailed_source_mean_annual_internal_gains = internal_gains usage_zone.heating_setpoint = archetype.heating_setpoint usage_zone.heating_setback = archetype.heating_setback usage_zone.cooling_setpoint = archetype.cooling_setpoint diff --git a/imports/usage/comnet_usage_parameters.py b/imports/usage/comnet_usage_parameters.py index 5d10312c..0d0a835d 100644 --- a/imports/usage/comnet_usage_parameters.py +++ b/imports/usage/comnet_usage_parameters.py @@ -12,9 +12,10 @@ from helpers.configuration_helper import ConfigurationHelper as ch from imports.geometry.helpers.geometry_helper import GeometryHelper from imports.usage.helpers.usage_helper import UsageHelper from city_model_structure.building_demand.usage_zone import UsageZone +from city_model_structure.building_demand.lighting import Lighting +from city_model_structure.building_demand.occupancy import Occupancy +from city_model_structure.building_demand.appliances import Appliances from city_model_structure.building_demand.internal_gains import InternalGains -from imports.usage.data_classes.hft_usage_zone_archetype import HftUsageZoneArchetype as huza -from imports.usage.data_classes.hft_internal_gains_archetype import HftInternalGainsArchetype as higa class ComnetUsageParameters: @@ -68,45 +69,44 @@ class ComnetUsageParameters: 'process': process_data} @staticmethod - def _parse_zone_usage_type(usage, data): - if data['occupancy'][usage][0] <= 0: - occupancy_density = 0 - else: - occupancy_density = 1 / data['occupancy'][usage][0] - mechanical_air_change = data['ventilation rate'][usage][0] - internal_gains = [] - # lighting - latent_fraction = ch().comnet_lighting_latent - convective_fraction = ch().comnet_lighting_convective - radiative_fraction = ch().comnet_lighting_radiant - average_internal_gain = data['lighting'][usage][4] - internal_gains.append(higa(internal_gains_type=cte.LIGHTING, average_internal_gain=average_internal_gain, - convective_fraction=convective_fraction, radiative_fraction=radiative_fraction, - latent_fraction=latent_fraction)) - # occupancy - latent_fraction = data['occupancy'][usage][2] / (data['occupancy'][usage][1] + data['occupancy'][usage][2]) - sensible_fraction = float(1 - latent_fraction) - convective_fraction = sensible_fraction * ch().comnet_occupancy_sensible_convective - radiative_fraction = sensible_fraction * ch().comnet_occupancy_sensible_radiant - average_internal_gain = (data['occupancy'][usage][1] + data['occupancy'][usage][2]) \ - * occupancy_density * cte.BTU_H_TO_WATTS - internal_gains.append(higa(internal_gains_type=cte.OCCUPANCY, average_internal_gain=average_internal_gain, - convective_fraction=convective_fraction, radiative_fraction=radiative_fraction, - latent_fraction=latent_fraction)) - # plug loads - if data['plug loads'][usage][0] != 'n.a.': - latent_fraction = ch().comnet_plugs_latent - convective_fraction = ch().comnet_plugs_convective - radiative_fraction = ch().comnet_plugs_radiant - average_internal_gain = data['plug loads'][usage][0] - internal_gains.append(higa(internal_gains_type=cte.RECEPTACLE, average_internal_gain=average_internal_gain, - convective_fraction=convective_fraction, radiative_fraction=radiative_fraction, - latent_fraction=latent_fraction)) + def _parse_zone_usage_type(usage, height, data): + _usage_zone = UsageZone() + _usage_zone.usage = usage - usage_zone_archetype = huza(usage=usage, internal_gains=internal_gains, - occupancy_density=occupancy_density, - mechanical_air_change=mechanical_air_change) - return usage_zone_archetype + # lighting + _lighting = Lighting() + _lighting.latent_fraction = ch().comnet_lighting_latent + _lighting.convective_fraction = ch().comnet_lighting_convective + _lighting.radiative_fraction = ch().comnet_lighting_radiant + _lighting.average_internal_gain = data['lighting'][usage][4] + + # plug loads + _appliances = None + if data['plug loads'][usage][0] != 'n.a.': + _appliances = Appliances() + _appliances.latent_fraction = ch().comnet_plugs_latent + _appliances.convective_fraction = ch().comnet_plugs_convective + _appliances.radiative_fraction = ch().comnet_plugs_radiant + _appliances.average_internal_gain = data['plug loads'][usage][0] + + # occupancy + _occupancy = Occupancy() + _occupancy.occupancy_density = data['occupancy'][usage][0] * cte.METERS_TO_FEET**2 + _occupancy.sensible_convective_internal_gain = data['occupancy'][usage][1] \ + * ch().comnet_occupancy_sensible_convective + _occupancy.sensible_radiant_internal_gain = data['occupancy'][usage][1] * ch().comnet_occupancy_sensible_radiant + _occupancy.latent_internal_gain = data['occupancy'][usage][2] + + if _occupancy.occupancy_density <= 0: + _usage_zone.mechanical_air_change = 0 + else: + _usage_zone.mechanical_air_change = data['ventilation rate'][usage][0] / _occupancy.occupancy_density \ + * cte.HOUR_TO_MINUTES / cte.METERS_TO_FEET**3 / height + + _usage_zone.occupancy = _occupancy + _usage_zone.lighting = _lighting + _usage_zone.appliances = _appliances + return _usage_zone def enrich_buildings(self): """ @@ -151,12 +151,11 @@ class ComnetUsageParameters: for archetype_internal_gain in archetype.internal_gains: internal_gain = InternalGains() internal_gain.type = archetype_internal_gain.type - internal_gain.average_internal_gain = archetype_internal_gain.average_internal_gain * cte.METERS_TO_FEET**2 + internal_gain.average_internal_gain = archetype_internal_gain.average_internal_gain internal_gain.convective_fraction = archetype_internal_gain.convective_fraction internal_gain.radiative_fraction = archetype_internal_gain.radiative_fraction internal_gain.latent_fraction = archetype_internal_gain.latent_fraction internal_gains.append(internal_gain) usage_zone.internal_gains = internal_gains - usage_zone.occupancy_density = archetype.occupancy_density * cte.METERS_TO_FEET**2 - usage_zone.mechanical_air_change = archetype.mechanical_air_change * usage_zone.occupancy_density \ - * cte.HOUR_TO_MINUTES / cte.METERS_TO_FEET**3 / height + usage_zone.occupancy_density = archetype.occupancy_density + usage_zone.mechanical_air_change = archetype.mechanical_air_change \ No newline at end of file diff --git a/imports/usage/data_classes/hft_usage_zone_archetype.py b/imports/usage/data_classes/hft_usage_zone_archetype.py deleted file mode 100644 index b945e9c9..00000000 --- a/imports/usage/data_classes/hft_usage_zone_archetype.py +++ /dev/null @@ -1,144 +0,0 @@ -""" -HftUsageZoneArchetype stores usage information by building archetypes -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca -""" -from typing import List -from imports.usage.data_classes.hft_internal_gains_archetype import HftInternalGainsArchetype - - -class HftUsageZoneArchetype: - """ - HftUsageZoneArchetype class - """ - def __init__(self, usage=None, internal_gains=None, heating_set_point=None, heating_set_back=None, - cooling_set_point=None, occupancy_density=None, hours_day=None, days_year=None, - dhw_average_volume_pers_day=None, dhw_preparation_temperature=None, - electrical_app_average_consumption_sqm_year=None, mechanical_air_change=None, - occupancy=None, schedules=None): - self._usage = usage - self._internal_gains = internal_gains - self._heating_setpoint = heating_set_point - self._heating_setback = heating_set_back - self._cooling_setpoint = cooling_set_point - self._occupancy_density = occupancy_density - self._hours_day = hours_day - self._days_year = days_year - self._dhw_average_volume_pers_day = dhw_average_volume_pers_day - self._dhw_preparation_temperature = dhw_preparation_temperature - self._electrical_app_average_consumption_sqm_year = electrical_app_average_consumption_sqm_year - self._mechanical_air_change = mechanical_air_change - self._occupancy = occupancy - self._schedules = schedules - - @property - def internal_gains(self) -> List[HftInternalGainsArchetype]: - """ - Get usage zone internal gains - :return: [InternalGains] - """ - return self._internal_gains - - @property - def heating_setpoint(self): - """ - Get usage zone heating set point in celsius grads - :return: float - """ - return self._heating_setpoint - - @property - def heating_setback(self): - """ - Get usage zone heating setback in celsius grads - :return: float - """ - return self._heating_setback - - @property - def cooling_setpoint(self): - """ - Get usage zone cooling setpoint in celsius grads - :return: float - """ - return self._cooling_setpoint - - @property - def hours_day(self): - """ - Get usage zone usage hours per day - :return: float - """ - return self._hours_day - - @property - def days_year(self): - """ - Get usage zone usage days per year - :return: float - """ - return self._days_year - - @property - def mechanical_air_change(self): - """ - Set usage zone mechanical air change in air change per hour (ACH) - :return: float - """ - return self._mechanical_air_change - - @property - def usage(self): - """ - Get usage zone usage - :return: str - """ - return self._usage - - @property - def occupancy(self): - """ - Get schedules data - :return: [Occupancy] - """ - return self._occupancy - - @property - def schedules(self): - """ - Get schedules - :return: [Schedule_Values] - """ - return self._schedules - - @property - def occupancy_density(self): - """ - Get schedules density in persons per m2 - :return: float - """ - return self._occupancy_density - - @property - def dhw_average_volume_pers_day(self): - """ - Get average DHW consumption in m3 per person per day - :return: float - """ - return self._dhw_average_volume_pers_day - - @property - def dhw_preparation_temperature(self): - """ - Get preparation temperature of the DHW in degree Celsius - :return: float - """ - return self._dhw_preparation_temperature - - @property - def electrical_app_average_consumption_sqm_year(self): - """ - Get average consumption of electrical appliances in Joules per m2 and year (J/m2yr) - :return: float - """ - return self._electrical_app_average_consumption_sqm_year diff --git a/imports/usage/data_classes/usage_zone_archetype.py b/imports/usage/data_classes/usage_zone_archetype.py new file mode 100644 index 00000000..67d1f964 --- /dev/null +++ b/imports/usage/data_classes/usage_zone_archetype.py @@ -0,0 +1,98 @@ +""" +UsageZoneArchetype stores usage information by usage type +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +""" + +from typing import List +from imports.usage.data_classes.hft_internal_gains_archetype import HftInternalGainsArchetype + + +class UsageZoneArchetype: + """ + UsageZoneArchetype class + """ + def __init__(self, usage=None, not_detailed_source_mean_annual_internal_gains=None, hours_day=None, days_year=None, + electrical_app_average_consumption_sqm_year=None, mechanical_air_change=None, occupancy=None, + lighting=None, appliances=None): + self._usage = usage + self._not_detailed_source_mean_annual_internal_gains = not_detailed_source_mean_annual_internal_gains + self._hours_day = hours_day + self._days_year = days_year + self._electrical_app_average_consumption_sqm_year = electrical_app_average_consumption_sqm_year + self._mechanical_air_change = mechanical_air_change + self._occupancy = occupancy + self._lighting = lighting + self._appliances = appliances + + @property + def not_detailed_source_mean_annual_internal_gains(self) -> List[HftInternalGainsArchetype]: + """ + Get usage zone internal gains from not detailed heating source in W/m2 + :return: [InternalGains] + """ + return self._not_detailed_source_mean_annual_internal_gains + + @property + def hours_day(self): + """ + Get usage zone usage hours per day + :return: float + """ + return self._hours_day + + @property + def days_year(self): + """ + Get usage zone usage days per year + :return: float + """ + return self._days_year + + @property + def mechanical_air_change(self): + """ + Set usage zone mechanical air change in air change per hour (ACH) + :return: float + """ + return self._mechanical_air_change + + @property + def usage(self): + """ + Get usage zone usage + :return: str + """ + return self._usage + + @property + def electrical_app_average_consumption_sqm_year(self): + """ + Get average consumption of electrical appliances in Joules per m2 and year (J/m2yr) + :return: float + """ + return self._electrical_app_average_consumption_sqm_year + + @property + def occupancy(self): + """ + Get occupancy data + :return: Occupancy + """ + return self._occupancy + + @property + def lighting(self): + """ + Get lighting data + :return: Lighting + """ + return self._lighting + + @property + def appliances(self): + """ + Get appliances data + :return: Appliances + """ + return self._appliances diff --git a/imports/usage/hft_usage_interface.py b/imports/usage/hft_usage_interface.py index 3da6457d..ed6dfefc 100644 --- a/imports/usage/hft_usage_interface.py +++ b/imports/usage/hft_usage_interface.py @@ -5,7 +5,7 @@ SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ import xmltodict -from imports.usage.data_classes.hft_usage_zone_archetype import HftUsageZoneArchetype as huza +from imports.usage.data_classes.usage_zone_archetype import UsageZoneArchetype as huza from imports.usage.data_classes.hft_internal_gains_archetype import HftInternalGainsArchetype as higa @@ -96,7 +96,7 @@ class HftUsageInterface: # todo: for internal_gain in usage_zone_variant['schedules']['internGains']:???????????????? # There are no more internal gains? How is it saved when more than one??? # for internal_gain in usage_zone.internal_gains: - internal_gains = usage_zone.internal_gains[0] + internal_gains = usage_zone.not_detailed_source_mean_annual_internal_gains[0] latent_fraction = internal_gains.latent_fraction convective_fraction = internal_gains.convective_fraction average_internal_gain = internal_gains.average_internal_gain diff --git a/imports/usage/hft_usage_parameters.py b/imports/usage/hft_usage_parameters.py index fc254aa2..d2deeeae 100644 --- a/imports/usage/hft_usage_parameters.py +++ b/imports/usage/hft_usage_parameters.py @@ -58,14 +58,14 @@ class HftUsageParameters(HftUsageInterface): # usage_zone.internal_gains when writing usage_zone.internal_gains = archetype.internal_gains. # Therefore, this walk around has been done. internal_gains = [] - for archetype_internal_gain in archetype.internal_gains: + for archetype_internal_gain in archetype.not_detailed_source_mean_annual_internal_gains: internal_gain = InternalGains() internal_gain.average_internal_gain = archetype_internal_gain.average_internal_gain internal_gain.convective_fraction = archetype_internal_gain.convective_fraction internal_gain.radiative_fraction = archetype_internal_gain.radiative_fraction internal_gain.latent_fraction = archetype_internal_gain.latent_fraction internal_gains.append(internal_gain) - usage_zone.internal_gains = internal_gains + usage_zone.not_detailed_source_mean_annual_internal_gains = internal_gains usage_zone.heating_setpoint = archetype.heating_setpoint usage_zone.heating_setback = archetype.heating_setback usage_zone.cooling_setpoint = archetype.cooling_setpoint diff --git a/requirements.txt b/requirements.txt index 457ca623..82bb41b2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,6 +13,6 @@ openpyxl~=3.0.7 networkx~=2.5.1 parseidf~=1.0.0 ply~=3.11 -rhino3dm~=7.11.1 +rhino3dm~=7.7.0 scipy==1.7.1 PyYAML==6.0 \ No newline at end of file diff --git a/unittests/test_construction_factory.py b/unittests/test_construction_factory.py index 03266f95..e0ece40a 100644 --- a/unittests/test_construction_factory.py +++ b/unittests/test_construction_factory.py @@ -1,15 +1,16 @@ """ TestConstructionFactory test and validate the city model structure construction parameters SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +Copyright © 2022 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ from pathlib import Path from unittest import TestCase +import helpers.constants as cte from imports.geometry_factory import GeometryFactory from imports.construction_factory import ConstructionFactory from imports.geometry.helpers.geometry_helper import GeometryHelper -from imports.life_cycle_assessment_factory import LifeCycleAssessment + class TestConstructionFactory(TestCase): """ @@ -23,46 +24,138 @@ class TestConstructionFactory(TestCase): self._city = None self._example_path = (Path(__file__).parent / 'tests_data').resolve() - def _get_citygml(self, file=None): - if file is None: - file = 'pluto_building.gml' + def _get_citygml(self, file): file_path = (self._example_path / file).resolve() self._city = GeometryFactory('citygml', file_path).city - LifeCycleAssessment('material', self._city).enrich() self.assertIsNotNone(self._city, 'city is none') return self._city - def test_city_with_construction_extended_library(self): - """ - Enrich the city with the physic information and verify it - :return: None - """ - - city = self._get_citygml() + def _check_buildings(self, city): for building in city.buildings: - building.function = GeometryHelper.pluto_to_function[building.function] - self.assertIsNotNone(building.surfaces, 'Building has no surfaces') + self.assertIsNotNone(building.name, 'building name is none') + self.assertIsNotNone(building.lod, 'building lod is none') + self.assertIsNotNone(building.type, 'building type is none') + self.assertIsNotNone(building.volume, 'building volume is none') + self.assertIsNotNone(building.detailed_polyhedron, 'building detailed polyhedron is none') + self.assertIsNotNone(building.simplified_polyhedron, 'building simplified polyhedron is none') + self.assertIsNotNone(building.surfaces, 'building surfaces is none') + self.assertIsNotNone(building.centroid, 'building centroid is none') + self.assertIsNotNone(building.max_height, 'building max_height is none') + self.assertEqual(len(building.external_temperature), 0, 'building external temperature is calculated') + self.assertEqual(len(building.global_horizontal), 0, 'building global horizontal is calculated') + self.assertEqual(len(building.diffuse), 0, 'building diffuse is calculated') + self.assertEqual(len(building.beam), 0, 'building beam is calculated') + self.assertIsNotNone(building.lower_corner, 'building lower corner is none') + self.assertEqual(len(building.sensors), 0, 'building sensors are assigned') + self.assertIsNotNone(building.geometrical_zones, 'no geometrical zones created') + self.assertIsNotNone(building.grounds, 'building grounds is none') + self.assertIsNotNone(building.walls, 'building walls is none') + self.assertIsNotNone(building.roofs, 'building roofs is none') + self.assertIsNone(building.usage_zones, 'usage zones are defined') + self.assertTrue(len(building.thermal_zones) > 0, 'thermal zones are not defined') + self.assertIsNone(building.basement_heated, 'building basement_heated is not none') + self.assertIsNone(building.attic_heated, 'building attic_heated is not none') + self.assertIsNone(building.terrains, 'building terrains is not none') + self.assertIsNotNone(building.year_of_construction, 'building year_of_construction is none') + self.assertIsNotNone(building.function, 'building function is none') + self.assertIsNotNone(building.average_storey_height, 'building average_storey_height is none') + self.assertIsNotNone(building.storeys_above_ground, 'building storeys_above_ground is none') + self.assertEqual(len(building.heating), 0, 'building heating is not none') + self.assertEqual(len(building.cooling), 0, 'building cooling is not none') + self.assertIsNotNone(building.eave_height, 'building eave height is none') + self.assertIsNotNone(building.storeys, 'building storeys are not defined') + self.assertIsNotNone(building.roof_type, 'building roof type is none') + self.assertIsNotNone(building.floor_area, 'building floor_area is none') + self.assertIsNone(building.households, 'building households is not none') + self.assertFalse(building.is_conditioned, 'building is conditioned') - # case 1: NREL - ConstructionFactory('nrel', city).enrich() + def _check_thermal_zones(self, building): + for thermal_zone in building.thermal_zones: + self.assertIsNotNone(thermal_zone.id, 'thermal_zone id is none') + self.assertIsNotNone(thermal_zone.floor_area, 'thermal_zone floor area is none') + self.assertTrue(len(thermal_zone.thermal_boundaries) > 0, 'thermal_zone thermal_boundaries not defined') + self.assertIsNotNone(thermal_zone.additional_thermal_bridge_u_value, 'additional_thermal_bridge_u_value is none') + self.assertIsNotNone(thermal_zone.effective_thermal_capacity, 'thermal_zone effective_thermal_capacity is none') + self.assertIsNotNone(thermal_zone.indirectly_heated_area_ratio, + 'thermal_zone indirectly_heated_area_ratio is none') + self.assertIsNotNone(thermal_zone.infiltration_rate_system_off, + 'thermal_zone infiltration_rate_system_off is none') + self.assertIsNotNone(thermal_zone.infiltration_rate_system_on, 'thermal_zone infiltration_rate_system_on is none') + self.assertIsNone(thermal_zone.usage_zones, 'thermal_zone usage_zones is not none') + self.assertIsNotNone(thermal_zone.volume, 'thermal_zone volume is none') + self.assertIsNone(thermal_zone.ordinate_number, 'thermal_zone ordinate number is not none') + self.assertIsNone(thermal_zone.thermal_control, 'thermal_zone thermal_control is not none') + self.assertIsNone(thermal_zone.hvac_system, 'thermal_zone hvac_system is not none') + + def _check_thermal_boundaries(self, thermal_zone): + for thermal_boundary in thermal_zone.thermal_boundaries: + self.assertIsNotNone(thermal_boundary.id, 'thermal_boundary id is none') + self.assertIsNotNone(thermal_boundary.parent_surface, 'thermal_boundary surface is none') + self.assertIsNotNone(thermal_boundary.thermal_zones, 'thermal_boundary delimits is none') + self.assertIsNotNone(thermal_boundary.opaque_area, 'thermal_boundary area is none') + self.assertIsNotNone(thermal_boundary.azimuth, 'thermal_boundary azimuth is none') + self.assertIsNotNone(thermal_boundary.inclination, 'thermal_boundary inclination is none') + self.assertIsNotNone(thermal_boundary.thickness, 'thermal_boundary thickness is none') + self.assertIsNotNone(thermal_boundary.type, 'thermal_boundary type is none') + if thermal_boundary.type is not cte.GROUND: + self.assertIsNotNone(thermal_boundary.outside_solar_absorptance, 'outside_solar_absorptance is none') + self.assertIsNotNone(thermal_boundary.shortwave_reflectance, 'shortwave_reflectance is none') + else: + self.assertIsNone(thermal_boundary.outside_solar_absorptance, 'outside_solar_absorptance is not none') + self.assertIsNone(thermal_boundary.shortwave_reflectance, 'shortwave_reflectance is not none') + self.assertIsNotNone(thermal_boundary.thermal_openings, 'thermal_openings is none') + self.assertIsNotNone(thermal_boundary.construction_name, 'construction_name is none') + self.assertIsNotNone(thermal_boundary.window_ratio, 'window_ratio is none') + self.assertIsNone(thermal_boundary.windows_areas, 'windows_areas is not none') + self.assertIsNotNone(thermal_boundary.u_value, 'u_value is none') + self.assertIsNotNone(thermal_boundary.hi, 'hi is none') + self.assertIsNotNone(thermal_boundary.he, 'he is none') + self.assertIsNotNone(thermal_boundary.virtual_internal_surface, 'virtual_internal_surface is none') + self.assertIsNone(thermal_boundary.inside_emissivity, 'inside_emissivity is not none') + self.assertIsNone(thermal_boundary.alpha_coefficient, 'alpha_coefficient is not none') + self.assertIsNone(thermal_boundary.radiative_coefficient, 'radiative_coefficient is not none') + + def _check_thermal_openings(self, thermal_boundary): + for thermal_opening in thermal_boundary.thermal_openings: + self.assertIsNotNone(thermal_opening.id, 'thermal opening id is not none') + self.assertIsNotNone(thermal_opening.area, 'thermal opening area is not none') + self.assertRaises(Exception, lambda: thermal_opening.openable_ratio, + 'thermal_opening openable_ratio is not raising an exception') + self.assertIsNotNone(thermal_opening.frame_ratio, 'thermal opening frame_ratio is not none') + self.assertIsNotNone(thermal_opening.g_value, 'thermal opening g_value is not none') + self.assertIsNotNone(thermal_opening.overall_u_value, 'thermal opening overall_u_value is not none') + self.assertIsNotNone(thermal_opening.hi, 'thermal opening hi is not none') + self.assertIsNotNone(thermal_opening.he, 'thermal opening he is not none') + self.assertIsNone(thermal_opening.inside_emissivity, 'thermal opening inside_emissivity is not none') + self.assertIsNone(thermal_opening.alpha_coefficient, 'thermal opening alpha_coefficient is not none') + self.assertIsNone(thermal_opening.radiative_coefficient, 'thermal opening radiative_coefficient is not none') + + def test_citygml_function(self): + """ + Test city objects' functions in the city + """ + # case 1: hft + file = 'one_building_in_kelowna.gml' + function_format = 'hft' + city = self._get_citygml(file) for building in city.buildings: - self.assertIsNotNone(building.average_storey_height, 'average_storey_height is none') - self.assertIsNotNone(building.storeys_above_ground, 'storeys_above_ground is none') - self.assertIsNot(len(building.thermal_zones), 0, 'no building thermal_zones defined') - for thermal_zone in building.thermal_zones: - self.assertIsNotNone(thermal_zone.effective_thermal_capacity, 'effective_thermal_capacity is none') - self.assertIsNotNone(thermal_zone.additional_thermal_bridge_u_value, - 'additional_thermal_bridge_u_value is none') - self.assertIsNotNone(thermal_zone.indirectly_heated_area_ratio, 'indirectly_heated_area_ratio is none') - self.assertIsNotNone(thermal_zone.infiltration_rate_system_on, 'infiltration_rate_system_on is none') - self.assertIsNotNone(thermal_zone.infiltration_rate_system_off, 'infiltration_rate_system_off is none') - self.assertIsNotNone(thermal_zone.thermal_boundaries, 'thermal_boundaries is none') - for thermal_boundary in thermal_zone.thermal_boundaries: - if thermal_boundary.surface.type != 'Ground': - self.assertIsNotNone(thermal_boundary.outside_solar_absorptance, 'outside_solar_absorptance is none') - self.assertIsNotNone(thermal_boundary.window_ratio, 'window_ratio is none') - for layer in thermal_boundary.layers: - self.assertTrue(city.lca_material(layer.material.lca_id) is not None) + building.function = self._internal_function(function_format, building.function) + self.assertEqual(building.function, 'residential', 'format hft') + + # case 2: Pluto + file = 'pluto_building.gml' + function_format = 'pluto' + city = self._get_citygml(file) + for building in city.buildings: + building.function = self._internal_function(function_format, building.function) + self.assertEqual(building.function, 'secondary school', 'format pluto') + + # case 3: Alkis + file = 'one_building_in_kelowna_alkis.gml' + function_format = 'alkis' + city = self._get_citygml(file) + for building in city.buildings: + self.assertRaises(Exception, lambda: self._internal_function(function_format, building.function)) def test_city_with_construction_reduced_library(self): """ @@ -72,21 +165,69 @@ class TestConstructionFactory(TestCase): city = self._get_citygml(file) for building in city.buildings: building.function = GeometryHelper.hft_to_function[building.function] - # case 2: NRCAN ConstructionFactory('nrcan', city).enrich() + + self._check_buildings(city) for building in city.buildings: - self.assertIsNotNone(building.average_storey_height, 'average_storey_height is none') - self.assertIsNotNone(building.storeys_above_ground, 'storeys_above_ground is none') - self.assertIsNot(len(building.thermal_zones), 0, 'no building thermal_zones defined') + self._check_thermal_zones(building) for thermal_zone in building.thermal_zones: - self.assertIsNotNone(thermal_zone.effective_thermal_capacity, 'effective_thermal_capacity is none') - self.assertIsNotNone(thermal_zone.additional_thermal_bridge_u_value, - 'additional_thermal_bridge_u_value is none') - self.assertIsNotNone(thermal_zone.indirectly_heated_area_ratio, 'indirectly_heated_area_ratio is none') - self.assertIsNotNone(thermal_zone.infiltration_rate_system_on, 'infiltration_rate_system_on is none') - self.assertIsNotNone(thermal_zone.infiltration_rate_system_off, 'infiltration_rate_system_off is none') - self.assertIsNotNone(thermal_zone.thermal_boundaries, 'thermal_boundaries is none') - self.assertIsNot(len(thermal_zone.thermal_boundaries), 0, 'no boundaries of thermal_zone defined') + self._check_thermal_boundaries(thermal_zone) for thermal_boundary in thermal_zone.thermal_boundaries: - self.assertIsNotNone(thermal_boundary.outside_solar_absorptance, 'outside_solar_absorptance is none') - self.assertIsNotNone(thermal_boundary.window_ratio, 'window_ratio is none') + self.assertIsNone(thermal_boundary.outside_thermal_absorptance, 'outside_thermal_absorptance is not none') + self.assertIsNone(thermal_boundary.outside_visible_absorptance, 'outside_visible_absorptance is not none') + self.assertIsNone(thermal_boundary.layers, 'layers is not none') + + self._check_thermal_openings(thermal_boundary) + for thermal_opening in thermal_boundary.thermal_openings: + self.assertIsNone(thermal_opening.conductivity, 'thermal_opening conductivity is not none') + self.assertIsNone(thermal_opening.thickness, 'thermal opening thickness is not none') + self.assertIsNone(thermal_opening.front_side_solar_transmittance_at_normal_incidence, + 'thermal opening front_side_solar_transmittance_at_normal_incidence is not none') + self.assertIsNone(thermal_opening.back_side_solar_transmittance_at_normal_incidence, + 'thermal opening back_side_solar_transmittance_at_normal_incidence is not none') + + def test_city_with_construction_extended_library(self): + """ + Enrich the city with the construction information and verify it + """ + file = 'pluto_building.gml' + city = self._get_citygml(file) + for building in city.buildings: + building.function = GeometryHelper.pluto_to_function[building.function] + ConstructionFactory('nrel', city).enrich() + + self._check_buildings(city) + for building in city.buildings: + self._check_thermal_zones(building) + for thermal_zone in building.thermal_zones: + self._check_thermal_boundaries(thermal_zone) + for thermal_boundary in thermal_zone.thermal_boundaries: + if thermal_boundary.type is not cte.GROUND: + self.assertIsNotNone(thermal_boundary.outside_thermal_absorptance, 'outside_thermal_absorptance is none') + self.assertIsNotNone(thermal_boundary.outside_visible_absorptance, 'outside_visible_absorptance is none') + else: + self.assertIsNone(thermal_boundary.outside_thermal_absorptance, 'outside_thermal_absorptance is not none') + self.assertIsNone(thermal_boundary.outside_visible_absorptance, 'outside_visible_absorptance is not none') + self.assertIsNotNone(thermal_boundary.layers, 'layers is none') + + self._check_thermal_openings(thermal_boundary) + for thermal_opening in thermal_boundary.thermal_openings: + self.assertIsNotNone(thermal_opening.conductivity, 'thermal_opening conductivity is none') + self.assertIsNotNone(thermal_opening.thickness, 'thermal opening thickness is none') + self.assertIsNotNone(thermal_opening.front_side_solar_transmittance_at_normal_incidence, + 'thermal opening front_side_solar_transmittance_at_normal_incidence is none') + self.assertIsNotNone(thermal_opening.back_side_solar_transmittance_at_normal_incidence, + 'thermal opening back_side_solar_transmittance_at_normal_incidence is none') + + @staticmethod + def _internal_function(function_format, original_function): + if function_format == 'hft': + new_function = GeometryHelper.hft_to_function[original_function] + elif function_format == 'pluto': + new_function = GeometryHelper.pluto_to_function[original_function] + elif function_format == 'alkis': + # todo: not implemented yet!! + raise NotImplementedError + else: + raise Exception('Function key not recognized. Implemented only "hft" and "pluto"') + return new_function diff --git a/unittests/test_geometry_factory.py b/unittests/test_geometry_factory.py index 77d17256..b9142591 100644 --- a/unittests/test_geometry_factory.py +++ b/unittests/test_geometry_factory.py @@ -6,8 +6,6 @@ Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.mons from pathlib import Path from unittest import TestCase from imports.geometry_factory import GeometryFactory -from imports.geometry.helpers.geometry_helper import GeometryHelper -from city_model_structure.building_demand.thermal_opening import ThermalOpening class TestGeometryFactory(TestCase): @@ -36,20 +34,65 @@ class TestGeometryFactory(TestCase): self.assertIsNotNone(self._city, 'city is none') return self._city - @staticmethod - def _internal_function(function_format, original_function): - new_function = original_function - if function_format == 'hft': - new_function = GeometryHelper.hft_to_function[original_function] - elif function_format == 'pluto': - new_function = GeometryHelper.pluto_to_function[original_function] - elif function_format == 'alkis': - # todo: not implemented yet!! - raise NotImplementedError - return new_function + def _check_buildings(self, city): + for building in city.buildings: + self.assertIsNotNone(building.name, 'building name is none') + self.assertIsNotNone(building.lod, 'building lod is none') + self.assertIsNotNone(building.type, 'building type is none') + self.assertIsNotNone(building.volume, 'building volume is none') + self.assertIsNotNone(building.detailed_polyhedron, 'building detailed polyhedron is none') + self.assertIsNotNone(building.simplified_polyhedron, 'building simplified polyhedron is none') + self.assertIsNotNone(building.surfaces, 'building surfaces is none') + self.assertIsNotNone(building.centroid, 'building centroid is none') + self.assertIsNotNone(building.max_height, 'building max_height is none') + self.assertEqual(len(building.external_temperature), 0, 'building external temperature is calculated') + self.assertEqual(len(building.global_horizontal), 0, 'building global horizontal is calculated') + self.assertEqual(len(building.diffuse), 0, 'building diffuse is calculated') + self.assertEqual(len(building.beam), 0, 'building beam is calculated') + self.assertIsNotNone(building.lower_corner, 'building lower corner is none') + self.assertEqual(len(building.sensors), 0, 'building sensors are assigned') + self.assertIsNotNone(building.geometrical_zones, 'no geometrical zones created') + self.assertIsNotNone(building.grounds, 'building grounds is none') + self.assertIsNotNone(building.walls, 'building walls is none') + self.assertIsNotNone(building.roofs, 'building roofs is none') + self.assertIsNone(building.usage_zones, 'usage zones are defined') + self.assertIsNone(building.thermal_zones, 'thermal zones are defined') + self.assertIsNone(building.basement_heated, 'building basement_heated is not none') + self.assertIsNone(building.attic_heated, 'building attic_heated is not none') + self.assertIsNone(building.terrains, 'building terrains is not none') + self.assertIsNotNone(building.year_of_construction, 'building year_of_construction is none') + self.assertIsNotNone(building.function, 'building function is none') + self.assertIsNone(building.average_storey_height, 'building average_storey_height is not none') + self.assertIsNone(building.storeys_above_ground, 'building storeys_above_ground is not none') + self.assertEqual(len(building.heating), 0, 'building heating is not none') + self.assertEqual(len(building.cooling), 0, 'building cooling is not none') + self.assertIsNotNone(building.eave_height, 'building eave height is none') + self.assertIsNone(building.storeys, 'building storeys are defined') + self.assertIsNotNone(building.roof_type, 'building roof type is none') + self.assertIsNotNone(building.floor_area, 'building floor_area is none') + self.assertIsNone(building.households, 'building households is not none') + self.assertFalse(building.is_conditioned, 'building is_conditioned is conditioned') + + def _check_surfaces(self, building): + for surface in building.surfaces: + self.assertIsNotNone(surface.name, 'surface name is none') + self.assertIsNotNone(surface.id, 'surface id is none') + self.assertIsNone(surface.swr, 'surface swr is not none') + self.assertIsNotNone(surface.lower_corner, 'surface envelope_lower_corner is none') + self.assertIsNotNone(surface.upper_corner, 'surface envelope_upper_corner is none') + self.assertIsNotNone(surface.area_below_ground, 'surface area_below_ground is none') + self.assertIsNotNone(surface.area_above_ground, 'surface area_above_ground is none') + self.assertIsNotNone(surface.azimuth, 'surface azimuth is none') + self.assertIsNotNone(surface.type, 'surface type is none') + self.assertIsNotNone(surface.inclination, 'surface inclination is none') + self.assertEqual(len(surface.global_irradiance), 0, 'global irradiance is calculated') + self.assertIsNotNone(surface.perimeter_polygon, 'surface perimeter_polygon is none') + self.assertIsNone(surface.holes_polygons, 'surface hole_polygons is not none') + self.assertIsNotNone(surface.solid_polygon, 'surface solid_polygon is none') + self.assertIsNone(surface.pv_system_installed, 'surface PV system installed is not none') # citygml_classes - def test_citygml_buildings(self): + def test_import_citygml(self): """ Test city objects in the city :return: None @@ -57,175 +100,9 @@ class TestGeometryFactory(TestCase): file = 'one_building_in_kelowna.gml' city = self._get_citygml(file) self.assertTrue(len(city.buildings) == 1) + self._check_buildings(city) for building in city.buildings: - self.assertIsNotNone(building.name, 'building name is none') - self.assertIsNotNone(building.lod, 'building lod is none') - self.assertIsNotNone(building.centroid, 'building centroid is none') - self.assertIsNotNone(building.year_of_construction, 'building year_of_construction is none') - self.assertIsNotNone(building.function, 'building function is none') - self.assertIsNotNone(building.volume, 'building volume is none') - self.assertIsNotNone(building.surfaces, 'building surfaces is none') - self.assertIsNotNone(building.surfaces[0].name, 'surface not found') - self.assertIsNone(building.basement_heated, 'building basement_heated is not none') - self.assertIsNone(building.attic_heated, 'building attic_heated is not none') - self.assertIsNotNone(building.terrains, 'building terrains is none') - self.assertIsNone(building.average_storey_height, 'building average_storey_height is not none') - self.assertIsNotNone(building.type, 'building type is none') - self.assertIsNotNone(building.max_height, 'building max_height is none') - self.assertIsNotNone(building.floor_area, 'building floor_area is none') - - def test_citygml_surfaces(self): - """ - Test surfaces in city objects - :return: None - """ - file = 'one_building_in_kelowna.gml' - city = self._get_citygml(file) - for building in city.buildings: - for surface in building.surfaces: - self.assertIsNotNone(surface.name, 'surface name is none') - self.assertIsNotNone(surface.type, 'surface type is none') - self.assertIsNotNone(surface.azimuth, 'surface azimuth is none') - self.assertIsNotNone(surface.inclination, 'surface inclination is none') - self.assertIsNotNone(surface.area_below_ground, 'surface area_below_ground is none') - self.assertIsNotNone(surface.area_above_ground, 'surface area_above_ground is none') - self.assertIsNotNone(surface.global_irradiance, 'monthly irradiance is none') - self.assertIsNone(surface.swr, 'surface swr is not none') - self.assertIsNotNone(surface.lower_corner, 'surface envelope_lower_corner is none') - self.assertIsNotNone(surface.upper_corner, 'surface envelope_upper_corner is none') - self.assertIsNotNone(surface.area_above_ground, 'surface area_above_ground is none') - self.assertIsNotNone(surface.perimeter_polygon, 'surface perimeter_polygon is none') - self.assertIsNone(surface.holes_polygons, 'surface hole_polygons is not none') - self.assertIsNotNone(surface.solid_polygon, 'surface solid_polygon is none') - - def test_citygml_thermal_zone(self): - """ - Test thermal zones in city objects - :return: None - """ - file = 'one_building_in_kelowna.gml' - city = self._get_citygml(file) - for building in city.buildings: - self.assertIsNot(len(building.thermal_zones), 0, 'no building thermal_zones defined') - for thermal_zone in building.thermal_zones: - self.assertIsNotNone(thermal_zone.thermal_boundaries, 'thermal_zone thermal_boundaries is none') - self.assertIsNotNone(thermal_zone.floor_area, 'thermal_zone floor_area is none') - self.assertIsNone(thermal_zone.additional_thermal_bridge_u_value, - 'thermal_zone additional_thermal_bridge_u_value is not none') - self.assertIsNone(thermal_zone.effective_thermal_capacity, - 'thermal_zone effective_thermal_capacity is not none') - self.assertIsNone(thermal_zone.indirectly_heated_area_ratio, - 'thermal_zone indirectly_heated_area_ratio is not none') - self.assertIsNone(thermal_zone.infiltration_rate_system_off, - 'thermal_zone infiltration_rate_system_off is not none') - self.assertIsNone(thermal_zone.infiltration_rate_system_on, - 'thermal_zone infiltration_rate_system_on is not none') - self.assertIsNone(thermal_zone.usage_zones, - 'thermal_zone usage_zones are not none') - - def test_citygml_thermal_boundary(self): - """ - Test thermal boundaries in thermal zones - :return: None - """ - file = 'one_building_in_kelowna.gml' - city = self._get_citygml(file) - for building in city.buildings: - self.assertIsNot(len(building.thermal_zones), 0, 'no building thermal_zones defined') - for thermal_zone in building.thermal_zones: - self.assertIsNot(len(thermal_zone.thermal_boundaries), 0, 'no building thermal_boundaries defined') - for thermal_boundary in thermal_zone.thermal_boundaries: - print(thermal_boundary.surface.type) - print(thermal_boundary.surface.area_above_ground) - self.assertIsNotNone(thermal_boundary.surface, 'thermal_boundary surface is none') - self.assertIsNotNone(thermal_boundary.type, 'thermal_boundary type is none') - self.assertIsNotNone(thermal_boundary.area, 'thermal_boundary area is none') - self.assertIsNotNone(thermal_boundary.azimuth, 'thermal_boundary azimuth is none') - self.assertIsNotNone(thermal_boundary.thermal_zones, 'thermal_boundary delimits is none') - self.assertIsNotNone(thermal_boundary.inclination, 'thermal_boundary inclination is none') - - def test_citygml_thermal_opening(self): - """ - Test thermal openings in thermal zones - :return: None - """ - file = 'one_building_in_kelowna.gml' - city = self._get_citygml(file) - for building in city.buildings: - self.assertIsNot(len(building.thermal_zones), 0, 'no building thermal_zones defined') - for thermal_zone in building.thermal_zones: - self.assertIsNot(len(thermal_zone.thermal_boundaries), 0, 'no building thermal_boundaries defined') - for thermal_boundary in thermal_zone.thermal_boundaries: - thermal_opening: ThermalOpening - for thermal_opening in thermal_boundary.thermal_openings: - self.assertIsNone(thermal_opening.frame_ratio, 'thermal_opening frame_ratio was initialized') - self.assertIsNone(thermal_opening.g_value, 'thermal_opening g_value was initialized') - self.assertIsNone(thermal_opening.conductivity, 'thermal_opening conductivity_w_mk was initialized') - self.assertIsNone(thermal_opening.back_side_solar_transmittance_at_normal_incidence, - 'thermal_opening back_side_solar_transmittance_at_normal_incidence was initialized') - self.assertRaises(Exception, lambda: thermal_opening.openable_ratio, - 'thermal_opening openable_ratio is not raising an exception') - self.assertIsNone(thermal_opening.front_side_solar_transmittance_at_normal_incidence, - 'thermal_opening front_side_solar_transmittance_at_normal_incidence was initialized') - self.assertIsNone(thermal_opening.thickness, 'thermal_opening thickness_m was initialized') - self.assertRaises(Exception, lambda: thermal_opening.overall_u_value, - 'thermal_opening u_value was initialized') - self.assertIsNone(thermal_opening.overall_u_value, 'thermal_opening overall_u_value was initialized') - self.assertIsNone(thermal_opening.hi, 'thermal_opening hi was initialized') - self.assertIsNone(thermal_opening.he, 'thermal_opening he was initialized') - - def test_citygml_function(self): - """ - Test city objects' functions in the city - :return: None - """ - # case 1: hft - file = 'one_building_in_kelowna.gml' - function_format = 'hft' - city = self._get_citygml(file) - for building in city.buildings: - building.function = self._internal_function(function_format, building.function) - self.assertEqual(building.function, 'residential', 'format hft') - - # case 2: Pluto - file = 'pluto_building.gml' - function_format = 'pluto' - city = self._get_citygml(file) - for building in city.buildings: - building.function = self._internal_function(function_format, building.function) - self.assertEqual(building.function, 'secondary school', 'format pluto') - - # case 3: Alkis - file = 'one_building_in_kelowna_alkis.gml' - function_format = 'alkis' - city = self._get_citygml(file) - for building in city.buildings: - self.assertRaises(Exception, lambda: self._internal_function(function_format, building.function)) - - def test_citygml_storeys(self): - """ - Test division by storeys of buildings - :return: None - """ - - file = 'one_building_in_kelowna.gml' - city = self._get_citygml(file) - for building in city.buildings: - print('building') - for surface in building.surfaces: - print(surface.name) - print(surface.type) - print(surface.perimeter_polygon.area) - building.average_storey_height = 1.5 - building.storeys_above_ground = 2 - storeys = building.storeys - for storey in storeys: - print(storey.name) - print(storey.neighbours) - for surface in storey.surfaces: - print(surface.name) - print(surface.type) - print(surface.perimeter_polygon.area) + self._check_surfaces(building) # obj def test_import_obj(self): @@ -235,8 +112,10 @@ class TestGeometryFactory(TestCase): file = 'kelowna.obj' city = self._get_obj(file) self.assertIsNotNone(city, 'city is none') + self.assertTrue(len(city.buildings) == 1) + self._check_buildings(city) for building in city.buildings: - self.assertIsNotNone(building, 'building is none') + self._check_surfaces(building) # osm def test_subway(self): diff --git a/unittests/test_life_cycle_assessment_factory.py b/unittests/test_life_cycle_assessment_factory.py index 9e2e3e62..f7134f20 100644 --- a/unittests/test_life_cycle_assessment_factory.py +++ b/unittests/test_life_cycle_assessment_factory.py @@ -47,10 +47,8 @@ class TestLifeCycleAssessment(TestCase): city_file = "../unittests/tests_data/C40_Final.gml" city = GeometryFactory('citygml', city_file).city LifeCycleAssessment('material', city).enrich() - self.assertTrue(len(city.lca_materials) > 0) - for lca_material in city.lca_materials: - lca_mat = city.lca_material(lca_material.id) - self.assertTrue(lca_mat is not None) + for material in city.materials: + self.assertTrue(len(city.materials) > 0) diff --git a/unittests/test_schedules_factory.py b/unittests/test_schedules_factory.py index 349c5c08..0506abae 100644 --- a/unittests/test_schedules_factory.py +++ b/unittests/test_schedules_factory.py @@ -67,3 +67,4 @@ class TestSchedulesFactory(TestCase): for usage_zone in building.usage_zones: for schedule in usage_zone.schedules: print(schedule) + print(usage_zone.schedules[schedule]) diff --git a/unittests/test_sensors_factory.py b/unittests/test_sensors_factory.py index f0de55c6..55b13e01 100644 --- a/unittests/test_sensors_factory.py +++ b/unittests/test_sensors_factory.py @@ -50,8 +50,8 @@ class TestSensorsFactory(TestCase): Load concordia sensors and verify it """ SensorsFactory('cec', self._city, self._end_point).enrich() - # SensorsFactory('cgf', self._city, self._end_point).enrich() - # SensorsFactory('ct', self._city, self._end_point).enrich() + SensorsFactory('cgf', self._city, self._end_point).enrich() + SensorsFactory('ct', self._city, self._end_point).enrich() for city_object in self._city.city_objects: print(city_object.name, len(city_object.sensors)) for sensor in city_object.sensors: diff --git a/unittests/test_usage_factory.py b/unittests/test_usage_factory.py index 111e581f..3a5fb70b 100644 --- a/unittests/test_usage_factory.py +++ b/unittests/test_usage_factory.py @@ -7,8 +7,8 @@ from pathlib import Path from unittest import TestCase from imports.geometry_factory import GeometryFactory -from imports.construction_factory import ConstructionFactory from imports.usage_factory import UsageFactory +from imports.schedules_factory import SchedulesFactory from imports.construction_factory import ConstructionFactory from imports.geometry.helpers.geometry_helper import GeometryHelper @@ -22,29 +22,119 @@ class TestUsageFactory(TestCase): Configure test environment :return: """ + self._city = None self._example_path = (Path(__file__).parent / 'tests_data').resolve() def _get_citygml(self, file): file_path = (self._example_path / file).resolve() - _city = GeometryFactory('citygml', file_path).city - self.assertIsNotNone(_city, 'city is none') - return _city + self._city = GeometryFactory('citygml', file_path).city + self.assertIsNotNone(self._city, 'city is none') + return self._city - def test_comnet(self): + def _check_buildings(self, city): + for building in city.buildings: + self.assertIsNotNone(building.name, 'building name is none') + self.assertIsNotNone(building.lod, 'building lod is none') + self.assertIsNotNone(building.type, 'building type is none') + self.assertIsNotNone(building.volume, 'building volume is none') + self.assertIsNotNone(building.detailed_polyhedron, 'building detailed polyhedron is none') + self.assertIsNotNone(building.simplified_polyhedron, 'building simplified polyhedron is none') + self.assertIsNotNone(building.surfaces, 'building surfaces is none') + self.assertIsNotNone(building.centroid, 'building centroid is none') + self.assertIsNotNone(building.max_height, 'building max_height is none') + self.assertEqual(len(building.external_temperature), 0, 'building external temperature is calculated') + self.assertEqual(len(building.global_horizontal), 0, 'building global horizontal is calculated') + self.assertEqual(len(building.diffuse), 0, 'building diffuse is calculated') + self.assertEqual(len(building.beam), 0, 'building beam is calculated') + self.assertIsNotNone(building.lower_corner, 'building lower corner is none') + self.assertEqual(len(building.sensors), 0, 'building sensors are assigned') + self.assertIsNotNone(building.geometrical_zones, 'no geometrical zones created') + self.assertIsNotNone(building.grounds, 'building grounds is none') + self.assertIsNotNone(building.walls, 'building walls is none') + self.assertIsNotNone(building.roofs, 'building roofs is none') + self.assertTrue(len(building.usage_zones) > 0, 'usage zones are not defined') + self.assertTrue(len(building.thermal_zones) > 0, 'thermal zones are not defined') + self.assertIsNotNone(building.basement_heated, 'building basement_heated is none') + self.assertIsNotNone(building.attic_heated, 'building attic_heated is none') + self.assertIsNone(building.terrains, 'building terrains is not none') + self.assertIsNotNone(building.year_of_construction, 'building year_of_construction is none') + self.assertIsNotNone(building.function, 'building function is none') + self.assertIsNotNone(building.average_storey_height, 'building average_storey_height is none') + self.assertIsNotNone(building.storeys_above_ground, 'building storeys_above_ground is none') + self.assertEqual(len(building.heating), 0, 'building heating is not none') + self.assertEqual(len(building.cooling), 0, 'building cooling is not none') + self.assertIsNotNone(building.eave_height, 'building eave height is none') + self.assertIsNotNone(building.storeys, 'building storeys are not defined') + self.assertIsNotNone(building.roof_type, 'building roof type is none') + self.assertIsNotNone(building.floor_area, 'building floor_area is none') + self.assertIsNone(building.households, 'building households is not none') + self.assertTrue(building.is_conditioned, 'building is not conditioned') + + def _check_thermal_zones(self, building): + for thermal_zone in building.thermal_zones: + self.assertIsNotNone(thermal_zone.id, 'thermal_zone id is none') + self.assertIsNotNone(thermal_zone.floor_area, 'thermal_zone floor area is none') + self.assertTrue(len(thermal_zone.thermal_boundaries) > 0, 'thermal_zone thermal_boundaries not defined') + self.assertIsNotNone(thermal_zone.additional_thermal_bridge_u_value, 'additional_thermal_bridge_u_value is none') + self.assertIsNotNone(thermal_zone.effective_thermal_capacity, 'thermal_zone effective_thermal_capacity is none') + self.assertIsNotNone(thermal_zone.indirectly_heated_area_ratio, + 'thermal_zone indirectly_heated_area_ratio is none') + self.assertIsNotNone(thermal_zone.infiltration_rate_system_off, + 'thermal_zone infiltration_rate_system_off is none') + self.assertIsNotNone(thermal_zone.infiltration_rate_system_on, 'thermal_zone infiltration_rate_system_on is none') + self.assertIsNotNone(thermal_zone.usage_zones, 'thermal_zone usage_zones is none') + self.assertIsNotNone(thermal_zone.volume, 'thermal_zone volume is none') + self.assertIsNone(thermal_zone.ordinate_number, 'thermal_zone ordinate number is not none') + self.assertIsNotNone(thermal_zone.thermal_control, 'thermal_zone thermal_control is not none') + self.assertIsNotNone(thermal_zone.hvac_system, 'thermal_zone hvac_system is not none') + + def _check_usage_zones(self, building): + for usage_zone in building.usage_zones: + self.assertIsNotNone(usage_zone.usage, 'usage is none') + self.assertIsNotNone(usage_zone.not_detailed_source_mean_annual_internal_gains, 'usage is none') + self.assertIsNotNone(usage_zone.cooling_setpoint, 'usage is none') + self.assertIsNotNone(usage_zone.heating_setback, 'usage is none') + self.assertIsNotNone(usage_zone.heating_setpoint, 'usage is none') + self.assertIsNotNone(usage_zone.occupancy_density, 'usage is none') + self.assertIsNotNone(usage_zone.hours_day, 'usage is none') + self.assertIsNotNone(usage_zone.days_year, 'usage is none') + self.assertIsNotNone(usage_zone.dhw_average_volume_pers_day, 'usage is none') + self.assertIsNotNone(usage_zone.dhw_preparation_temperature, 'usage is none') + self.assertIsNotNone(usage_zone.electrical_app_average_consumption_sqm_year, 'usage is none') + self.assertIsNotNone(usage_zone.is_heated, 'thermal_zone heated is none') + self.assertIsNotNone(usage_zone.is_cooled, 'thermal_zone cooled is none') + + def _check_hvac(self, thermal_zone): + self.assertIsNotNone(None, 'hvac') + + def _check_control(self, thermal_zone): + self.assertIsNotNone(None, 'control') + + def test_import_comnet(self): + """ + Enrich the city with the usage information from comnet and verify it + """ file = 'pluto_building.gml' city = self._get_citygml(file) for building in city.buildings: building.function = GeometryHelper.pluto_to_function[building.function] - ConstructionFactory('nrel', city).enrich() - UsageFactory('comnet', city).enrich() - def test_city_with_usage(self): + UsageFactory('comnet', city).enrich() + SchedulesFactory('comnet', city).enrich() + self._check_buildings(city) + for building in city.buildings: + self._check_thermal_zones(building) + for thermal_zone in building.thermal_zones: + self._check_hvac(thermal_zone) + self._check_control(thermal_zone) + self.assertIsNotNone(building.usage_zones, 'building usage zones is none') + self._check_usage_zones(building) + + def test_import_hft(self): """ - Enrich the city with the usage information and verify it - :return: None + Enrich the city with the usage information from hft and verify it """ - # case 1: HFT file = 'pluto_building.gml' city = self._get_citygml(file) for building in city.buildings: @@ -55,7 +145,7 @@ class TestUsageFactory(TestCase): self.assertIsNot(len(building.usage_zones), 0, 'no building usage_zones defined') for usage_zone in building.usage_zones: self.assertIsNotNone(usage_zone.usage, 'usage is none') - self.assertIsNotNone(usage_zone.internal_gains, 'usage is none') + self.assertIsNotNone(usage_zone.not_detailed_source_mean_annual_internal_gains, 'usage is none') self.assertIsNotNone(usage_zone.cooling_setpoint, 'usage is none') self.assertIsNotNone(usage_zone.heating_setback, 'usage is none') self.assertIsNotNone(usage_zone.heating_setpoint, 'usage is none') @@ -68,8 +158,10 @@ class TestUsageFactory(TestCase): self.assertIsNotNone(usage_zone.is_heated, 'thermal_zone heated is none') self.assertIsNotNone(usage_zone.is_cooled, 'thermal_zone cooled is none') - - # case 2: CA + def test_import_ca(self): + """ + Enrich the city with the usage information from hft and verify it + """ file = 'one_building_in_kelowna.gml' city = self._get_citygml(file) ConstructionFactory('nrel', city).enrich() @@ -78,7 +170,7 @@ class TestUsageFactory(TestCase): self.assertIsNot(len(building.usage_zones), 0, 'no building usage_zones defined') for usage_zone in building.usage_zones: self.assertIsNotNone(usage_zone.usage, 'usage is none') - self.assertIsNotNone(usage_zone.internal_gains, 'usage is none') + self.assertIsNotNone(usage_zone.not_detailed_source_mean_annual_internal_gains, 'usage is none') self.assertIsNotNone(usage_zone.cooling_setpoint, 'usage is none') self.assertIsNotNone(usage_zone.heating_setback, 'usage is none') self.assertIsNotNone(usage_zone.heating_setpoint, 'usage is none') From 69169b45b190d79ced44542b4fd10360413c9c89 Mon Sep 17 00:00:00 2001 From: Pilar Date: Tue, 8 Mar 2022 20:08:03 -0500 Subject: [PATCH 02/19] partially finished usage factory test --- city_model_structure/building.py | 31 +++-- .../building_demand/internal_zone.py | 84 ++++++++++++++ .../building_demand/occupant.py | 24 ---- imports/usage/ca_usage_parameters.py | 17 ++- imports/usage/comnet_usage_parameters.py | 28 ++--- imports/usage/hft_usage_parameters.py | 20 ++-- imports/usage_factory.py | 4 - unittests/test_usage_factory.py | 109 +++++++----------- 8 files changed, 173 insertions(+), 144 deletions(-) create mode 100644 city_model_structure/building_demand/internal_zone.py diff --git a/city_model_structure/building.py b/city_model_structure/building.py index f7d2dc16..1aff3b84 100644 --- a/city_model_structure/building.py +++ b/city_model_structure/building.py @@ -12,6 +12,7 @@ from city_model_structure.building_demand.usage_zone import UsageZone from city_model_structure.building_demand.storey import Storey from city_model_structure.city_object import CityObject from city_model_structure.building_demand.household import Household +from city_model_structure.building_demand.internal_zone import InternalZone from city_model_structure.attributes.polyhedron import Polyhedron ThermalZone = TypeVar('ThermalZone') @@ -34,7 +35,8 @@ class Building(CityObject): self._floor_area = None self._roof_type = None self._storeys = None - self._geometrical_zones = None + self._internal_zones = None + self._shell = None self._thermal_zones = None self._usage_zones = None self._type = 'building' @@ -61,13 +63,26 @@ class Building(CityObject): self._internal_walls.append(surface) @property - def geometrical_zones(self) -> List[Polyhedron]: - if self._geometrical_zones is None: - polygons = [] - for surface in self.surfaces: - polygons.append(surface.perimeter_polygon) - self._geometrical_zones = [Polyhedron(polygons)] - return self._geometrical_zones + def shell(self) -> Polyhedron: + """ + Get building shell + :return: [Polyhedron] + """ + if self._shell is None: + self._shell = Polyhedron(self.surfaces) + return self._shell + + @property + def internal_zones(self) -> List[InternalZone]: + """ + Get building internal zones + For Lod up to 3, there is only one internal zone which corresponds to the building shell. + In LoD 4 there can be more than one. In this case the definition of surfaces and floor area must be redefined. + :return: [InternalZone] + """ + if self._internal_zones is None: + self._internal_zones = [InternalZone(self.surfaces, self.floor_area)] + return self._internal_zones @property def grounds(self) -> List[Surface]: diff --git a/city_model_structure/building_demand/internal_zone.py b/city_model_structure/building_demand/internal_zone.py new file mode 100644 index 00000000..222174bb --- /dev/null +++ b/city_model_structure/building_demand/internal_zone.py @@ -0,0 +1,84 @@ +""" +InternalZone module. It saves the original geometrical information from interiors together with some attributes of those +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2022 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +""" +import uuid +from city_model_structure.building_demand.usage_zone import UsageZone +from city_model_structure.attributes.polyhedron import Polyhedron + + +class InternalZone: + """ + InternalZone class + """ + def __init__(self, surfaces, area): + self._surfaces = surfaces + self._id = None + self._geometry = None + self._volume = None + self._area = area + self._usage_zones = [] + + @property + def id(self): + """ + Get internal zone id, an universally unique identifier randomly generated + :return: str + """ + if self._id is None: + self._id = uuid.uuid4() + return self._id + + @property + def geometry(self) -> Polyhedron: + """ + Get internal zone geometry + :return: Polyhedron + """ + if self._geometry is None: + polygons = [] + for surface in self.surfaces: + polygons.append(surface.perimeter_polygon) + self._geometry = Polyhedron(polygons) + return self._geometry + + @property + def surfaces(self): + """ + Get internal zone surfaces + :return: [Surface] + """ + return self._surfaces + + @property + def volume(self): + """ + Get internal zone volume in cubic meters + :return: float + """ + return self.geometry.volume + + @property + def area(self): + """ + Get internal zone area in square meters + :return: float + """ + return self._area + + @property + def usage_zones(self) -> [UsageZone]: + """ + Get internal zone usage zones + :return: [UsageZone] + """ + return self._usage_zones + + @usage_zones.setter + def usage_zones(self, value): + """ + Set internal zone usage zones + :param value: [UsageZone] + """ + self._usage_zones = value diff --git a/city_model_structure/building_demand/occupant.py b/city_model_structure/building_demand/occupant.py index 8a7dd533..878d278c 100644 --- a/city_model_structure/building_demand/occupant.py +++ b/city_model_structure/building_demand/occupant.py @@ -144,27 +144,3 @@ class Occupant: """ # todo @Sanam: what format are you expecting here?? self._pd_of_meetings_duration = value - - def get_complete_year_schedule(self, schedules): - """ - Get the a non-leap year (8760 h), starting on Monday schedules out of archetypal days of week - :return: [float] - """ - if self._complete_year_schedule is None: - self._complete_year_schedule = [] - for i in range(1, 13): - month_range = cal.monthrange(2015, i)[1] - for day in range(1, month_range+1): - if cal.weekday(2015, i, day) < 5: - for j in range(0, 24): - week_schedule = schedules['WD'][j] - self._complete_year_schedule.append(week_schedule) - elif cal.weekday(2015, i, day) == 5: - for j in range(0, 24): - week_schedule = schedules['Sat'][j] - self._complete_year_schedule.append(week_schedule) - else: - for j in range(0, 24): - week_schedule = schedules['Sun'][j] - self._complete_year_schedule.append(week_schedule) - return self._complete_year_schedule diff --git a/imports/usage/ca_usage_parameters.py b/imports/usage/ca_usage_parameters.py index cd921332..cea058f6 100644 --- a/imports/usage/ca_usage_parameters.py +++ b/imports/usage/ca_usage_parameters.py @@ -35,15 +35,13 @@ class CaUsageParameters(HftUsageInterface): f' {building.function}, that assigns building usage as ' f'{gh.usage_from_function(building.function)}\n') continue - # todo: what to do with mix-usage usage from gml? - mix_usage = False - if not mix_usage: - # just one usage_zone - for thermal_zone in building.thermal_zones: + + for internal_zone in building.internal_zones: usage_zone = UsageZone() + usage_zone.usage = building.function self._assign_values(usage_zone, archetype) - usage_zone.volume = thermal_zone.volume - thermal_zone.usage_zones = [usage_zone] + usage_zone.percentage = 1 + internal_zone.usage_zones = [usage_zone] def _search_archetype(self, building_usage): for building_archetype in self._usage_archetypes: @@ -53,19 +51,18 @@ class CaUsageParameters(HftUsageInterface): @staticmethod def _assign_values(usage_zone, archetype): - usage_zone.usage = archetype.usage # Due to the fact that python is not a typed language, the wrong object type is assigned to # usage_zone.internal_gains when writing usage_zone.internal_gains = archetype.internal_gains. # Therefore, this walk around has been done. internal_gains = [] - for archetype_internal_gain in archetype.not_detailed_source_mean_annual_internal_gains: + for archetype_internal_gain in archetype.internal_gains: internal_gain = InternalGains() internal_gain.average_internal_gain = archetype_internal_gain.average_internal_gain internal_gain.convective_fraction = archetype_internal_gain.convective_fraction internal_gain.radiative_fraction = archetype_internal_gain.radiative_fraction internal_gain.latent_fraction = archetype_internal_gain.latent_fraction internal_gains.append(internal_gain) - usage_zone.not_detailed_source_mean_annual_internal_gains = internal_gains + usage_zone.internal_gains = internal_gains usage_zone.heating_setpoint = archetype.heating_setpoint usage_zone.heating_setback = archetype.heating_setback usage_zone.cooling_setpoint = archetype.cooling_setpoint diff --git a/imports/usage/comnet_usage_parameters.py b/imports/usage/comnet_usage_parameters.py index 0d0a835d..ed5d187e 100644 --- a/imports/usage/comnet_usage_parameters.py +++ b/imports/usage/comnet_usage_parameters.py @@ -69,7 +69,7 @@ class ComnetUsageParameters: 'process': process_data} @staticmethod - def _parse_zone_usage_type(usage, height, data): + def _parse_zone_usage_type(usage, data): _usage_zone = UsageZone() _usage_zone.usage = usage @@ -91,7 +91,7 @@ class ComnetUsageParameters: # occupancy _occupancy = Occupancy() - _occupancy.occupancy_density = data['occupancy'][usage][0] * cte.METERS_TO_FEET**2 + _occupancy.occupancy_density = data['occupancy'][usage][0] _occupancy.sensible_convective_internal_gain = data['occupancy'][usage][1] \ * ch().comnet_occupancy_sensible_convective _occupancy.sensible_radiant_internal_gain = data['occupancy'][usage][1] * ch().comnet_occupancy_sensible_radiant @@ -100,8 +100,7 @@ class ComnetUsageParameters: if _occupancy.occupancy_density <= 0: _usage_zone.mechanical_air_change = 0 else: - _usage_zone.mechanical_air_change = data['ventilation rate'][usage][0] / _occupancy.occupancy_density \ - * cte.HOUR_TO_MINUTES / cte.METERS_TO_FEET**3 / height + _usage_zone.mechanical_air_change = data['ventilation rate'][usage][0] / _occupancy.occupancy_density _usage_zone.occupancy = _occupancy _usage_zone.lighting = _lighting @@ -116,11 +115,6 @@ class ComnetUsageParameters: city = self._city for building in city.buildings: usage = GeometryHelper.usage_from_function(building.function) - height = building.average_storey_height - if height is None: - raise Exception('Average storey height not defined, ACH cannot be calculated') - if height <= 0: - raise Exception('Average storey height is zero, ACH cannot be calculated') archetype = self._search_archetype(UsageHelper.comnet_from_usage(usage)) if archetype is None: sys.stderr.write(f'Building {building.name} has unknown archetype for building function:' @@ -128,13 +122,12 @@ class ComnetUsageParameters: f'{GeometryHelper.usage_from_function(building.function)}\n') continue - # just one usage_zone - for thermal_zone in building.thermal_zones: + for internal_zone in building.internal_zones: usage_zone = UsageZone() usage_zone.usage = usage - self._assign_values(usage_zone, archetype, height) - usage_zone.volume = thermal_zone.volume - thermal_zone.usage_zones = [usage_zone] + self._assign_values(usage_zone, archetype, volume_per_area) + usage_zone.percentage = 1 + internal_zone.usage_zones = [usage_zone] def _search_archetype(self, building_usage): for building_archetype in self._usage_archetypes: @@ -143,7 +136,7 @@ class ComnetUsageParameters: return None @staticmethod - def _assign_values(usage_zone, archetype, height): + def _assign_values(usage_zone, archetype, volume_per_area): # Due to the fact that python is not a typed language, the wrong object type is assigned to # usage_zone.internal_gains when writing usage_zone.internal_gains = archetype.internal_gains. # Therefore, this walk around has been done. @@ -157,5 +150,6 @@ class ComnetUsageParameters: internal_gain.latent_fraction = archetype_internal_gain.latent_fraction internal_gains.append(internal_gain) usage_zone.internal_gains = internal_gains - usage_zone.occupancy_density = archetype.occupancy_density - usage_zone.mechanical_air_change = archetype.mechanical_air_change \ No newline at end of file + usage_zone.occupancy_density = archetype.occupancy_density * cte.METERS_TO_FEET**2 + usage_zone.mechanical_air_change = archetype.mechanical_air_change * cte.METERS_TO_FEET**2 \ + * cte.HOUR_TO_MINUTES / cte.METERS_TO_FEET**3 / volume_per_area \ No newline at end of file diff --git a/imports/usage/hft_usage_parameters.py b/imports/usage/hft_usage_parameters.py index d2deeeae..7411891e 100644 --- a/imports/usage/hft_usage_parameters.py +++ b/imports/usage/hft_usage_parameters.py @@ -18,9 +18,6 @@ class HftUsageParameters(HftUsageInterface): def __init__(self, city, base_path): super().__init__(base_path, 'de_library.xml') self._city = city - # todo: this is a wrong location for self._min_air_change -> re-think where to place this info - # and where it comes from - self._min_air_change = 0 def enrich_buildings(self): """ @@ -35,15 +32,13 @@ class HftUsageParameters(HftUsageInterface): f' {building.function}, that assigns building usage as ' f'{gh.usage_from_function(building.function)}\n') continue - # todo: what to do with mix-usage usage from gml? - mix_usage = False - if not mix_usage: - # just one usage_zone - for thermal_zone in building.thermal_zones: + + for internal_zone in building.internal_zones: usage_zone = UsageZone() + usage_zone.usage = building.function self._assign_values(usage_zone, archetype) - usage_zone.volume = thermal_zone.volume - thermal_zone.usage_zones = [usage_zone] + usage_zone.percentage = 1 + internal_zone.usage_zones = [usage_zone] def _search_archetype(self, building_usage): for building_archetype in self._usage_archetypes: @@ -53,19 +48,18 @@ class HftUsageParameters(HftUsageInterface): @staticmethod def _assign_values(usage_zone, archetype): - usage_zone.usage = archetype.usage # Due to the fact that python is not a typed language, the wrong object type is assigned to # usage_zone.internal_gains when writing usage_zone.internal_gains = archetype.internal_gains. # Therefore, this walk around has been done. internal_gains = [] - for archetype_internal_gain in archetype.not_detailed_source_mean_annual_internal_gains: + for archetype_internal_gain in archetype.internal_gains: internal_gain = InternalGains() internal_gain.average_internal_gain = archetype_internal_gain.average_internal_gain internal_gain.convective_fraction = archetype_internal_gain.convective_fraction internal_gain.radiative_fraction = archetype_internal_gain.radiative_fraction internal_gain.latent_fraction = archetype_internal_gain.latent_fraction internal_gains.append(internal_gain) - usage_zone.not_detailed_source_mean_annual_internal_gains = internal_gains + usage_zone.internal_gains = internal_gains usage_zone.heating_setpoint = archetype.heating_setpoint usage_zone.heating_setback = archetype.heating_setback usage_zone.cooling_setpoint = archetype.cooling_setpoint diff --git a/imports/usage_factory.py b/imports/usage_factory.py index 0e2875de..165e7f45 100644 --- a/imports/usage_factory.py +++ b/imports/usage_factory.py @@ -22,10 +22,6 @@ class UsageFactory: self._handler = '_' + handler.lower().replace(' ', '_') self._city = city self._base_path = base_path - for building in city.buildings: - if len(building.thermal_zones) == 0: - raise Exception('It seems that the usage factory is being called before the construction factory. ' - 'Please ensure that the construction factory is called first.') def _hft(self): """ diff --git a/unittests/test_usage_factory.py b/unittests/test_usage_factory.py index 3a5fb70b..5385d1e0 100644 --- a/unittests/test_usage_factory.py +++ b/unittests/test_usage_factory.py @@ -70,39 +70,6 @@ class TestUsageFactory(TestCase): self.assertIsNone(building.households, 'building households is not none') self.assertTrue(building.is_conditioned, 'building is not conditioned') - def _check_thermal_zones(self, building): - for thermal_zone in building.thermal_zones: - self.assertIsNotNone(thermal_zone.id, 'thermal_zone id is none') - self.assertIsNotNone(thermal_zone.floor_area, 'thermal_zone floor area is none') - self.assertTrue(len(thermal_zone.thermal_boundaries) > 0, 'thermal_zone thermal_boundaries not defined') - self.assertIsNotNone(thermal_zone.additional_thermal_bridge_u_value, 'additional_thermal_bridge_u_value is none') - self.assertIsNotNone(thermal_zone.effective_thermal_capacity, 'thermal_zone effective_thermal_capacity is none') - self.assertIsNotNone(thermal_zone.indirectly_heated_area_ratio, - 'thermal_zone indirectly_heated_area_ratio is none') - self.assertIsNotNone(thermal_zone.infiltration_rate_system_off, - 'thermal_zone infiltration_rate_system_off is none') - self.assertIsNotNone(thermal_zone.infiltration_rate_system_on, 'thermal_zone infiltration_rate_system_on is none') - self.assertIsNotNone(thermal_zone.usage_zones, 'thermal_zone usage_zones is none') - self.assertIsNotNone(thermal_zone.volume, 'thermal_zone volume is none') - self.assertIsNone(thermal_zone.ordinate_number, 'thermal_zone ordinate number is not none') - self.assertIsNotNone(thermal_zone.thermal_control, 'thermal_zone thermal_control is not none') - self.assertIsNotNone(thermal_zone.hvac_system, 'thermal_zone hvac_system is not none') - - def _check_usage_zones(self, building): - for usage_zone in building.usage_zones: - self.assertIsNotNone(usage_zone.usage, 'usage is none') - self.assertIsNotNone(usage_zone.not_detailed_source_mean_annual_internal_gains, 'usage is none') - self.assertIsNotNone(usage_zone.cooling_setpoint, 'usage is none') - self.assertIsNotNone(usage_zone.heating_setback, 'usage is none') - self.assertIsNotNone(usage_zone.heating_setpoint, 'usage is none') - self.assertIsNotNone(usage_zone.occupancy_density, 'usage is none') - self.assertIsNotNone(usage_zone.hours_day, 'usage is none') - self.assertIsNotNone(usage_zone.days_year, 'usage is none') - self.assertIsNotNone(usage_zone.dhw_average_volume_pers_day, 'usage is none') - self.assertIsNotNone(usage_zone.dhw_preparation_temperature, 'usage is none') - self.assertIsNotNone(usage_zone.electrical_app_average_consumption_sqm_year, 'usage is none') - self.assertIsNotNone(usage_zone.is_heated, 'thermal_zone heated is none') - self.assertIsNotNone(usage_zone.is_cooled, 'thermal_zone cooled is none') def _check_hvac(self, thermal_zone): self.assertIsNotNone(None, 'hvac') @@ -118,62 +85,68 @@ class TestUsageFactory(TestCase): city = self._get_citygml(file) for building in city.buildings: building.function = GeometryHelper.pluto_to_function[building.function] - ConstructionFactory('nrel', city).enrich() UsageFactory('comnet', city).enrich() SchedulesFactory('comnet', city).enrich() self._check_buildings(city) for building in city.buildings: - self._check_thermal_zones(building) - for thermal_zone in building.thermal_zones: - self._check_hvac(thermal_zone) - self._check_control(thermal_zone) - self.assertIsNotNone(building.usage_zones, 'building usage zones is none') - self._check_usage_zones(building) + for internal_zone in building.internal_zones: + self.assertIsNot(len(internal_zone.usage_zones), 0, 'no building usage_zones defined') + for usage_zone in internal_zone.usage_zones: + self._check_extended_usage(usage_zone) def test_import_hft(self): """ Enrich the city with the usage information from hft and verify it """ + # todo: read schedules!! file = 'pluto_building.gml' city = self._get_citygml(file) for building in city.buildings: building.function = GeometryHelper.pluto_to_function[building.function] - ConstructionFactory('nrel', city).enrich() + UsageFactory('hft', city).enrich() for building in city.buildings: - self.assertIsNot(len(building.usage_zones), 0, 'no building usage_zones defined') - for usage_zone in building.usage_zones: - self.assertIsNotNone(usage_zone.usage, 'usage is none') - self.assertIsNotNone(usage_zone.not_detailed_source_mean_annual_internal_gains, 'usage is none') - self.assertIsNotNone(usage_zone.cooling_setpoint, 'usage is none') - self.assertIsNotNone(usage_zone.heating_setback, 'usage is none') - self.assertIsNotNone(usage_zone.heating_setpoint, 'usage is none') - self.assertIsNotNone(usage_zone.occupancy_density, 'usage is none') - self.assertIsNotNone(usage_zone.hours_day, 'usage is none') - self.assertIsNotNone(usage_zone.days_year, 'usage is none') - self.assertIsNotNone(usage_zone.dhw_average_volume_pers_day, 'usage is none') - self.assertIsNotNone(usage_zone.dhw_preparation_temperature, 'usage is none') - self.assertIsNotNone(usage_zone.electrical_app_average_consumption_sqm_year, 'usage is none') - self.assertIsNotNone(usage_zone.is_heated, 'thermal_zone heated is none') - self.assertIsNotNone(usage_zone.is_cooled, 'thermal_zone cooled is none') + for internal_zone in building.internal_zones: + self.assertIsNot(len(internal_zone.usage_zones), 0, 'no building usage_zones defined') + for usage_zone in internal_zone.usage_zones: + self._check_extended_usage(usage_zone) def test_import_ca(self): """ - Enrich the city with the usage information from hft and verify it + Enrich the city with the usage information from canada and verify it """ file = 'one_building_in_kelowna.gml' city = self._get_citygml(file) - ConstructionFactory('nrel', city).enrich() UsageFactory('ca', city).enrich() for building in city.buildings: - self.assertIsNot(len(building.usage_zones), 0, 'no building usage_zones defined') - for usage_zone in building.usage_zones: - self.assertIsNotNone(usage_zone.usage, 'usage is none') - self.assertIsNotNone(usage_zone.not_detailed_source_mean_annual_internal_gains, 'usage is none') - self.assertIsNotNone(usage_zone.cooling_setpoint, 'usage is none') - self.assertIsNotNone(usage_zone.heating_setback, 'usage is none') - self.assertIsNotNone(usage_zone.heating_setpoint, 'usage is none') - self.assertIsNotNone(usage_zone.occupancy_density, 'usage is none') - self.assertIsNotNone(usage_zone.hours_day, 'usage is none') - self.assertIsNotNone(usage_zone.days_year, 'usage is none') + for internal_zone in building.internal_zones: + self.assertIsNot(len(internal_zone.usage_zones), 0, 'no building usage_zones defined') + for usage_zone in internal_zone.usage_zones: + self._check_reduced_usage(usage_zone) + + def _check_extended_usage(self, usage_zone): + self.assertIsNotNone(usage_zone.usage, 'usage is none') + self.assertIsNotNone(usage_zone.not_detailed_source_mean_annual_internal_gains, 'usage is none') + self.assertIsNotNone(usage_zone.cooling_setpoint, 'usage is none') + self.assertIsNotNone(usage_zone.heating_setback, 'usage is none') + self.assertIsNotNone(usage_zone.heating_setpoint, 'usage is none') + self.assertIsNotNone(usage_zone.occupancy_density, 'usage is none') + self.assertIsNotNone(usage_zone.hours_day, 'usage is none') + self.assertIsNotNone(usage_zone.days_year, 'usage is none') + self.assertIsNotNone(usage_zone.dhw_average_volume_pers_day, 'usage is none') + self.assertIsNotNone(usage_zone.dhw_preparation_temperature, 'usage is none') + self.assertIsNotNone(usage_zone.electrical_app_average_consumption_sqm_year, 'usage is none') + self.assertIsNotNone(usage_zone.is_heated, 'thermal_zone heated is none') + self.assertIsNotNone(usage_zone.is_cooled, 'thermal_zone cooled is none') + + + def _check_reduced_usage(self, usage_zone): + self.assertIsNotNone(usage_zone.usage, 'usage is none') + self.assertIsNotNone(usage_zone.internal_gains, 'usage is none') + self.assertIsNotNone(usage_zone.cooling_setpoint, 'usage is none') + self.assertIsNotNone(usage_zone.heating_setback, 'usage is none') + self.assertIsNotNone(usage_zone.heating_setpoint, 'usage is none') + self.assertIsNotNone(usage_zone.occupancy_density, 'usage is none') + self.assertIsNotNone(usage_zone.hours_day, 'usage is none') + self.assertIsNotNone(usage_zone.days_year, 'usage is none') From 77a8969a7e9b151b8a04f243e7b381e712327f3f Mon Sep 17 00:00:00 2001 From: Pilar Date: Thu, 17 Mar 2022 18:49:44 -0400 Subject: [PATCH 03/19] complete modification of the structure of the classes that define the buildig for a better adjustment to the different data sources and the connected tools. NOT finished --- city_model_structure/attributes/schedule.py | 136 +++--- city_model_structure/building.py | 51 +- .../building_demand/appliances.py | 50 +- .../building_demand/internal_gains.py | 18 +- .../building_demand/internal_zone.py | 41 +- .../building_demand/lighting.py | 34 +- .../building_demand/occupancy.py | 32 +- .../building_demand/thermal_control.py | 112 +++-- .../building_demand/thermal_zone.py | 61 ++- .../building_demand/usage_zone.py | 86 +++- data/usage/comnet_schedules_archetypes.xlsx | Bin 0 -> 348793 bytes data/usage/de_library.xml | 4 +- helpers/constants.py | 1 + imports/construction/ca_physics_parameters.py | 75 +-- .../helpers/construction_helper.py | 26 +- imports/construction/us_physics_parameters.py | 119 ++--- imports/geometry/helpers/geometry_helper.py | 459 ++++++++---------- imports/schedules/helpers/schedules_helper.py | 16 +- imports/usage/ca_usage_parameters.py | 66 +-- imports/usage/comnet_usage_parameters.py | 211 ++++++-- imports/usage/helpers/usage_helper.py | 51 +- imports/usage/hft_usage_interface.py | 297 ++++++++---- imports/usage/hft_usage_parameters.py | 61 ++- unittests/test_construction_factory.py | 95 ++-- unittests/test_enrichement.py | 145 ++++++ unittests/test_geometry_factory.py | 2 +- unittests/test_usage_factory.py | 160 +++--- 27 files changed, 1465 insertions(+), 944 deletions(-) create mode 100644 data/usage/comnet_schedules_archetypes.xlsx create mode 100644 unittests/test_enrichement.py diff --git a/city_model_structure/attributes/schedule.py b/city_model_structure/attributes/schedule.py index 1924dd7b..2d009f7f 100644 --- a/city_model_structure/attributes/schedule.py +++ b/city_model_structure/attributes/schedule.py @@ -9,131 +9,131 @@ from typing import Union, List class Schedule: - """ + """ Schedule class """ - def __init__(self): - self._id = None - self._type = None - self._values = None - self._data_type = None - self._time_step = None - self._time_range = None - self._day_types = None + def __init__(self): + self._id = None + self._type = None + self._values = None + self._data_type = None + self._time_step = None + self._time_range = None + self._day_types = None - @property - def id(self): - """ + @property + def id(self): + """ Get schedule id, an universally unique identifier randomly generated :return: str """ - if self._id is None: - self._id = uuid.uuid4() - return self._id + if self._id is None: + self._id = uuid.uuid4() + return self._id - @property - def type(self) -> Union[None, str]: - """ + @property + def type(self) -> Union[None, str]: + """ Get schedule type :return: None or str """ - return self._type + return self._type - @type.setter - def type(self, value): - """ + @type.setter + def type(self, value): + """ Set schedule type :param: str """ - if value is not None: - self._type = str(value) + if value is not None: + self._type = str(value) - @property - def values(self): - """ + @property + def values(self): + """ Get schedule values :return: [Any] """ - return self._values + return self._values - @values.setter - def values(self, value): - """ + @values.setter + def values(self, value): + """ Set schedule values :param: [Any] """ - self._values = value + self._values = value - @property - def data_type(self) -> Union[None, str]: - """ + @property + def data_type(self) -> Union[None, str]: + """ Get schedule data type from: ['any_number', 'fraction', 'on_off', 'temperature', 'humidity', 'control_type'] :return: None or str """ - return self._data_type + return self._data_type - @data_type.setter - def data_type(self, value): - """ + @data_type.setter + def data_type(self, value): + """ Set schedule data type :param: str """ - if value is not None: - self._data_type = str(value) + if value is not None: + self._data_type = str(value) - @property - def time_step(self) -> Union[None, str]: - """ + @property + def time_step(self) -> Union[None, str]: + """ Get schedule time step from: ['second', 'minute', 'hour', 'day', 'week', 'month'] :return: None or str """ - return self._time_step + return self._time_step - @time_step.setter - def time_step(self, value): - """ + @time_step.setter + def time_step(self, value): + """ Set schedule time step :param: str """ - if value is not None: - self._time_step = str(value) + if value is not None: + self._time_step = str(value) - @property - def time_range(self) -> Union[None, str]: - """ + @property + def time_range(self) -> Union[None, str]: + """ Get schedule time range from: ['minute', 'hour', 'day', 'week', 'month', 'year'] :return: None or str """ - return self._time_range + return self._time_range - @time_range.setter - def time_range(self, value): - """ + @time_range.setter + def time_range(self, value): + """ Set schedule time range :param: str """ - if value is not None: - self._time_range = str(value) + if value is not None: + self._time_range = str(value) - @property - def day_types(self) -> Union[None, List[str]]: - """ + @property + def day_types(self) -> Union[None, List[str]]: + """ Get schedule day types, as many as needed from: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday', 'holiday', 'winter_design_day', 'summer_design_day'] :return: None or [str] """ - return self._day_types + return self._day_types - @day_types.setter - def day_types(self, value): - """ + @day_types.setter + def day_types(self, value): + """ Set schedule day types :param: [str] """ - if value is not None: - self._day_types = [str(i) for i in value] + if value is not None: + self._day_types = [str(i) for i in value] diff --git a/city_model_structure/building.py b/city_model_structure/building.py index 1aff3b84..a94e55a6 100644 --- a/city_model_structure/building.py +++ b/city_model_structure/building.py @@ -5,18 +5,15 @@ Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@conc contributors: Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ -from typing import List, Union, TypeVar +from typing import List, Union import numpy as np from city_model_structure.building_demand.surface import Surface -from city_model_structure.building_demand.usage_zone import UsageZone from city_model_structure.building_demand.storey import Storey from city_model_structure.city_object import CityObject from city_model_structure.building_demand.household import Household from city_model_structure.building_demand.internal_zone import InternalZone from city_model_structure.attributes.polyhedron import Polyhedron -ThermalZone = TypeVar('ThermalZone') - class Building(CityObject): """ @@ -36,9 +33,7 @@ class Building(CityObject): self._roof_type = None self._storeys = None self._internal_zones = None - self._shell = None - self._thermal_zones = None - self._usage_zones = None + self._shell = None self._type = 'building' self._heating = dict() self._cooling = dict() @@ -108,22 +103,6 @@ class Building(CityObject): """ return self._walls - @property - def usage_zones(self) -> Union[None, List[UsageZone]]: - """ - Get city object usage zones - :return: [UsageZone] - """ - return self._usage_zones - - @usage_zones.setter - def usage_zones(self, value): - """ - Set city object usage zones - :param value: [UsageZone] - """ - self._usage_zones = value - @property def terrains(self) -> Union[None, List[Surface]]: """ @@ -166,22 +145,6 @@ class Building(CityObject): if value is not None: self._basement_heated = int(value) - @property - def thermal_zones(self) -> Union[None, List[ThermalZone]]: - """ - Get building thermal zones - :return: [ThermalZone] - """ - return self._thermal_zones - - @thermal_zones.setter - def thermal_zones(self, value): - """ - Set city object thermal zones - :param value: [ThermalZone] - """ - self._thermal_zones = value - @property def heated_volume(self): """ @@ -368,9 +331,11 @@ class Building(CityObject): Get building heated flag :return: Boolean """ - if self.thermal_zones is None: + if self.internal_zones is None: return False - for thermal_zone in self.thermal_zones: - if thermal_zone.hvac_system is not None: - return True + for internal_zone in self.internal_zones: + if internal_zone.usage_zones is not None: + for usage_zone in internal_zone.usage_zones: + if usage_zone.thermal_control is not None: + return True return False diff --git a/city_model_structure/building_demand/appliances.py b/city_model_structure/building_demand/appliances.py index ed09f795..8fd4f0f6 100644 --- a/city_model_structure/building_demand/appliances.py +++ b/city_model_structure/building_demand/appliances.py @@ -3,7 +3,7 @@ Appliances module SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2022 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ -from typing import Union +from typing import Union, List from city_model_structure.attributes.schedule import Schedule @@ -12,28 +12,28 @@ class Appliances: Appliances class """ def __init__(self): - self._lighting_density = None + self._appliances_density = None self._convective_fraction = None - self._radiant_fraction = None + self._radiative_fraction = None self._latent_fraction = None - self._schedule = None + self._schedules = None @property - def lighting_density(self) -> Union[None, float]: + def appliances_density(self) -> Union[None, float]: """ - Get lighting density in Watts per m2 + Get appliances density in Watts per m2 :return: None or float """ - return self._lighting_density + return self._appliances_density - @lighting_density.setter - def lighting_density(self, value): + @appliances_density.setter + def appliances_density(self, value): """ - Set lighting density in Watts per m2 + Set appliances density in Watts per m2 :param value: float """ if value is not None: - self._lighting_density = float(value) + self._appliances_density = float(value) @property def convective_fraction(self) -> Union[None, float]: @@ -53,21 +53,21 @@ class Appliances: self._convective_fraction = float(value) @property - def radiant_fraction(self) -> Union[None, float]: + def radiative_fraction(self) -> Union[None, float]: """ Get radiant fraction :return: None or float """ - return self._radiant_fraction + return self._radiative_fraction - @radiant_fraction.setter - def radiant_fraction(self, value): + @radiative_fraction.setter + def radiative_fraction(self, value): """ Set radiant fraction :param value: float """ if value is not None: - self._radiant_fraction = float(value) + self._radiative_fraction = float(value) @property def latent_fraction(self) -> Union[None, float]: @@ -87,17 +87,17 @@ class Appliances: self._latent_fraction = float(value) @property - def schedule(self) -> Union[None, Schedule]: + def schedules(self) -> Union[None, List[Schedule]]: """ - Get schedule - :return: None or Schedule + Get schedules + :return: None or [Schedule] """ - return self._schedule + return self._schedules - @schedule.setter - def schedule(self, value): + @schedules.setter + def schedules(self, value): """ - Set schedule - :param value: Schedule + Set schedules + :param value: [Schedule] """ - self._schedule = value + self._schedules = value diff --git a/city_model_structure/building_demand/internal_gains.py b/city_model_structure/building_demand/internal_gains.py index 4cfeabb9..9384cf21 100644 --- a/city_model_structure/building_demand/internal_gains.py +++ b/city_model_structure/building_demand/internal_gains.py @@ -4,7 +4,7 @@ SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@concordia.ca """ -from typing import Union +from typing import Union, List from city_model_structure.attributes.schedule import Schedule @@ -19,7 +19,7 @@ class InternalGains: self._convective_fraction = None self._radiative_fraction = None self._latent_fraction = None - self._schedule = None + self._schedules = None @property def type(self) -> Union[None, str]: @@ -107,17 +107,17 @@ class InternalGains: self._latent_fraction = float(value) @property - def schedule(self) -> Union[None, Schedule]: + def schedules(self) -> Union[None, List[Schedule]]: """ Get internal gain schedule - :return: Schedule + :return: [Schedule] """ - return self._schedule + return self._schedules - @schedule.setter - def schedule(self, value): + @schedules.setter + def schedules(self, value): """ Set internal gain schedule - :param value: Schedule + :param value: [Schedule] """ - self._schedule = value + self._schedules = value diff --git a/city_model_structure/building_demand/internal_zone.py b/city_model_structure/building_demand/internal_zone.py index 222174bb..74678473 100644 --- a/city_model_structure/building_demand/internal_zone.py +++ b/city_model_structure/building_demand/internal_zone.py @@ -3,9 +3,13 @@ InternalZone module. It saves the original geometrical information from interior SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2022 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ + import uuid +from typing import Union, List from city_model_structure.building_demand.usage_zone import UsageZone +from city_model_structure.building_demand.thermal_zone import ThermalZone from city_model_structure.attributes.polyhedron import Polyhedron +from city_model_structure.energy_systems.hvac_system import HvacSystem class InternalZone: @@ -18,7 +22,9 @@ class InternalZone: self._geometry = None self._volume = None self._area = area - self._usage_zones = [] + self._thermal_zones = None + self._usage_zones = None + self._hvac_system = None @property def id(self): @@ -82,3 +88,36 @@ class InternalZone: :param value: [UsageZone] """ self._usage_zones = value + + @property + def hvac_system(self) -> Union[None, HvacSystem]: + """ + Get HVAC system installed for this thermal zone + :return: None or HvacSystem + """ + return self._hvac_system + + @hvac_system.setter + def hvac_system(self, value): + """ + Set HVAC system installed for this thermal zone + :param value: HvacSystem + """ + self._hvac_system = value + + @property + def thermal_zones(self) -> Union[None, List[ThermalZone]]: + """ + Get building thermal zones + :return: [ThermalZone] + """ + return self._thermal_zones + + @thermal_zones.setter + def thermal_zones(self, value): + """ + Set city object thermal zones + :param value: [ThermalZone] + """ + self._thermal_zones = value + diff --git a/city_model_structure/building_demand/lighting.py b/city_model_structure/building_demand/lighting.py index 0407a432..0e72249f 100644 --- a/city_model_structure/building_demand/lighting.py +++ b/city_model_structure/building_demand/lighting.py @@ -3,7 +3,7 @@ Lighting module SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2022 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ -from typing import Union +from typing import Union, List from city_model_structure.attributes.schedule import Schedule @@ -14,9 +14,9 @@ class Lighting: def __init__(self): self._lighting_density = None self._convective_fraction = None - self._radiant_fraction = None + self._radiative_fraction = None self._latent_fraction = None - self._schedule = None + self._schedules = None @property def lighting_density(self) -> Union[None, float]: @@ -53,21 +53,21 @@ class Lighting: self._convective_fraction = float(value) @property - def radiant_fraction(self) -> Union[None, float]: + def radiative_fraction(self) -> Union[None, float]: """ Get radiant fraction :return: None or float """ - return self._radiant_fraction + return self._radiative_fraction - @radiant_fraction.setter - def radiant_fraction(self, value): + @radiative_fraction.setter + def radiative_fraction(self, value): """ Set radiant fraction :param value: float """ if value is not None: - self._radiant_fraction = float(value) + self._radiative_fraction = float(value) @property def latent_fraction(self) -> Union[None, float]: @@ -87,17 +87,17 @@ class Lighting: self._latent_fraction = float(value) @property - def schedule(self) -> Union[None, Schedule]: + def schedules(self) -> Union[None, List[Schedule]]: """ - Get schedule - :return: None or Schedule + Get schedules + :return: None or [Schedule] """ - return self._schedule + return self._schedules - @schedule.setter - def schedule(self, value): + @schedules.setter + def schedules(self, value): """ - Set schedule - :param value: Schedule + Set schedules + :param value: [Schedule] """ - self._schedule = value + self._schedules = value diff --git a/city_model_structure/building_demand/occupancy.py b/city_model_structure/building_demand/occupancy.py index 443030ca..ec9ebf1e 100644 --- a/city_model_structure/building_demand/occupancy.py +++ b/city_model_structure/building_demand/occupancy.py @@ -15,9 +15,9 @@ class Occupancy: def __init__(self): self._occupancy_density = None self._sensible_convective_internal_gain = None - self._sensible_radiant_internal_gain = None + self._sensible_radiative_internal_gain = None self._latent_internal_gain = None - self._occupancy_schedule = None + self._occupancy_schedules = None self._occupants = None @property @@ -55,21 +55,21 @@ class Occupancy: self._sensible_convective_internal_gain = float(value) @property - def sensible_radiant_internal_gain(self) -> Union[None, float]: + def sensible_radiative_internal_gain(self) -> Union[None, float]: """ Get sensible radiant internal gain in Watts per m2 :return: None or float """ - return self._sensible_radiant_internal_gain + return self._sensible_radiative_internal_gain - @sensible_radiant_internal_gain.setter - def sensible_radiant_internal_gain(self, value): + @sensible_radiative_internal_gain.setter + def sensible_radiative_internal_gain(self, value): """ Set sensible radiant internal gain in Watts per m2 :param value: float """ if value is not None: - self._sensible_radiant_internal_gain = float(value) + self._sensible_radiative_internal_gain = float(value) @property def latent_internal_gain(self) -> Union[None, float]: @@ -89,20 +89,20 @@ class Occupancy: self._latent_internal_gain = float(value) @property - def occupancy_schedule(self) -> Union[None, Schedule]: + def occupancy_schedules(self) -> Union[None, List[Schedule]]: """ - Get occupancy schedule - :return: None or Schedule + Get occupancy schedules + :return: None or [Schedule] """ - return self._occupancy_schedule + return self._occupancy_schedules - @occupancy_schedule.setter - def occupancy_schedule(self, value): + @occupancy_schedules.setter + def occupancy_schedules(self, value): """ - Set occupancy schedule - :param value: Schedule + Set occupancy schedules + :param value: [Schedule] """ - self._occupancy_schedule = value + self._occupancy_schedules = value @property def occupants(self) -> Union[None, List[Occupant]]: diff --git a/city_model_structure/building_demand/thermal_control.py b/city_model_structure/building_demand/thermal_control.py index 20888023..0c4c8a62 100644 --- a/city_model_structure/building_demand/thermal_control.py +++ b/city_model_structure/building_demand/thermal_control.py @@ -3,7 +3,7 @@ ThermalControl module SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2022 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ -from typing import Union +from typing import Union, List from city_model_structure.attributes.schedule import Schedule @@ -12,35 +12,59 @@ class ThermalControl: ThermalControl class """ def __init__(self): - self._heating_set_point = None - self._cooling_set_point = None - self._hvac_availability = None + self._mean_heating_set_point = None self._heating_set_back = None + self._mean_cooling_set_point = None + self._hvac_availability_schedules = None + self._heating_set_point_schedules = None + self._cooling_set_point_schedules = None + + @staticmethod + def _maximum_value(schedules): + maximum = -1000 + for schedule in schedules: + for value in schedule.values: + if value > maximum: + maximum = value + return maximum + + @staticmethod + def _minimum_value(schedules): + minimum = 1000 + for schedule in schedules: + for value in schedule.values: + if value < minimum: + minimum = value + return minimum @property - def heating_set_point(self) -> Union[None, Schedule]: + def mean_heating_set_point(self) -> Union[None, float]: """ Get heating set point defined for a thermal zone in Celsius - :return: None or Schedule + :return: None or float """ - return self._heating_set_point + if self._mean_heating_set_point is None: + if self.heating_set_point_schedules is not None: + self._mean_heating_set_point = self._maximum_value(self.heating_set_point_schedules) + return self._mean_heating_set_point - @heating_set_point.setter - def heating_set_point(self, value): + @mean_heating_set_point.setter + def mean_heating_set_point(self, value): """ Set heating set point defined for a thermal zone in Celsius - :param value: Schedule + :param value: float """ - self._heating_set_point = value + self._mean_heating_set_point = value @property def heating_set_back(self) -> Union[None, float]: """ Get heating set back defined for a thermal zone in Celsius - Heating set back is the only parameter which is not a schedule as it is either one value or it is implicit in the - set point schedule :return: None or float """ + if self._heating_set_back is None: + if self.heating_set_point_schedules is not None: + self._heating_set_back = self._minimum_value(self.heating_set_point_schedules) return self._heating_set_back @heating_set_back.setter @@ -53,34 +77,68 @@ class ThermalControl: self._heating_set_back = float(value) @property - def cooling_set_point(self) -> Union[None, Schedule]: + def mean_cooling_set_point(self) -> Union[None, float]: """ Get cooling set point defined for a thermal zone in Celsius - :return: None or Schedule + :return: None or float """ - return self._cooling_set_point + if self._mean_cooling_set_point is None: + if self.cooling_set_point_schedules is not None: + self._mean_cooling_set_point = self._minimum_value(self.cooling_set_point_schedules) + return self._mean_cooling_set_point - @cooling_set_point.setter - def cooling_set_point(self, value): + @mean_cooling_set_point.setter + def mean_cooling_set_point(self, value): """ Set cooling set point defined for a thermal zone in Celsius - :param value: Schedule + :param value: float """ - self._cooling_set_point = value + self._mean_cooling_set_point = value @property - def hvac_availability(self) -> Union[None, Schedule]: + def hvac_availability_schedules(self) -> Union[None, List[Schedule]]: """ Get the availability of the conditioning system defined for a thermal zone - :return: None or Schedule + :return: None or [Schedule] """ - return self._hvac_availability + return self._hvac_availability_schedules - @hvac_availability.setter - def hvac_availability(self, value): + @hvac_availability_schedules.setter + def hvac_availability_schedules(self, value): """ Set the availability of the conditioning system defined for a thermal zone - :param value: Schedule + :param value: [Schedule] """ - self._hvac_availability = value + self._hvac_availability_schedules = value + @property + def heating_set_point_schedules(self) -> Union[None, List[Schedule]]: + """ + Get heating set point schedule defined for a thermal zone in Celsius + :return: None or [Schedule] + """ + return self._heating_set_point_schedules + + @heating_set_point_schedules.setter + def heating_set_point_schedules(self, value): + """ + Set heating set point schedule defined for a thermal zone in Celsius + :param value: [Schedule] + """ + self._heating_set_point_schedules = value + + @property + def cooling_set_point_schedules(self) -> Union[None, List[Schedule]]: + """ + Get cooling set point schedule defined for a thermal zone in Celsius + :return: None or [Schedule] + """ + return self._cooling_set_point_schedules + + @cooling_set_point_schedules.setter + def cooling_set_point_schedules(self, value): + """ + Set cooling set point schedule defined for a thermal zone in Celsius + :param value: [Schedule] + """ + self._cooling_set_point_schedules = value diff --git a/city_model_structure/building_demand/thermal_zone.py b/city_model_structure/building_demand/thermal_zone.py index dd20bc28..10f8ac0b 100644 --- a/city_model_structure/building_demand/thermal_zone.py +++ b/city_model_structure/building_demand/thermal_zone.py @@ -5,11 +5,11 @@ Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@conc Contributors Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ import uuid -from typing import List, Union, Tuple, TypeVar +from typing import List, Union, TypeVar from city_model_structure.building_demand.usage_zone import UsageZone +from city_model_structure.attributes.schedule import Schedule from city_model_structure.building_demand.thermal_control import ThermalControl from city_model_structure.energy_systems.hvac_system import HvacSystem -from city_model_structure.attributes.schedule import Schedule ThermalBoundary = TypeVar('ThermalBoundary') @@ -27,11 +27,12 @@ class ThermalZone: self._indirectly_heated_area_ratio = None self._infiltration_rate_system_on = None self._infiltration_rate_system_off = None - self._usage_zones = None self._volume = volume self._ordinate_number = None + self._usage_zones = None self._thermal_control = None self._hvac_system = None + self._view_factors_matrix = None @property def id(self): @@ -142,22 +143,6 @@ class ThermalZone: """ self._infiltration_rate_system_off = value - @property - def usage_zones(self) -> Tuple[float, UsageZone]: - """ - Get list of usage zones and the percentage of thermal zone's volume affected by that usage - :return: [UsageZone] - """ - return self._usage_zones - - @usage_zones.setter - def usage_zones(self, values): - """ - Set list of usage zones and the percentage of thermal zone's volume affected by that usage - :param values: Tuple[float, UsageZone] - """ - self._usage_zones = values - @property def volume(self): """ @@ -183,10 +168,29 @@ class ThermalZone: if value is not None: self._ordinate_number = int(value) + @property + def usage_zones(self) -> [UsageZone]: + """ + Get list of usage zones and the percentage of thermal zone's volume affected by that usage + From internal_zone + :return: [UsageZone] + """ + return self._usage_zones + + @usage_zones.setter + def usage_zones(self, values): + """ + Set list of usage zones and the percentage of thermal zone's volume affected by that usage + From internal_zone + :param values: [UsageZone] + """ + self._usage_zones = values + @property def thermal_control(self) -> Union[None, ThermalControl]: """ Get thermal control of this thermal zone + From internal_zone :return: None or ThermalControl """ return self._thermal_control @@ -195,6 +199,7 @@ class ThermalZone: def thermal_control(self, value): """ Set thermal control for this thermal zone + From internal_zone :param value: ThermalControl """ self._thermal_control = value @@ -203,6 +208,7 @@ class ThermalZone: def hvac_system(self) -> Union[None, HvacSystem]: """ Get HVAC system installed for this thermal zone + From internal_zone :return: None or HvacSystem """ return self._hvac_system @@ -211,6 +217,23 @@ class ThermalZone: def hvac_system(self, value): """ Set HVAC system installed for this thermal zone + From internal_zone :param value: HvacSystem """ self._hvac_system = value + + @property + def view_factors_matrix(self): + """ + Get thermal zone view factors matrix + :return: [[float]] + """ + return self._view_factors_matrix + + @view_factors_matrix.setter + def view_factors_matrix(self, value): + """ + Set thermal zone view factors matrix + :param value: [[float]] + """ + self._view_factors_matrix = value diff --git a/city_model_structure/building_demand/usage_zone.py b/city_model_structure/building_demand/usage_zone.py index 590ce6d5..f8d77890 100644 --- a/city_model_structure/building_demand/usage_zone.py +++ b/city_model_structure/building_demand/usage_zone.py @@ -11,6 +11,7 @@ from city_model_structure.building_demand.internal_gains import InternalGains from city_model_structure.building_demand.occupancy import Occupancy from city_model_structure.building_demand.lighting import Lighting from city_model_structure.building_demand.appliances import Appliances +from city_model_structure.building_demand.thermal_control import ThermalControl class UsageZone: @@ -20,6 +21,7 @@ class UsageZone: def __init__(self): self._id = None self._usage = None + self._percentage = None self._not_detailed_source_mean_annual_internal_gains = None self._hours_day = None self._days_year = None @@ -29,6 +31,7 @@ class UsageZone: self._lighting = None self._appliances = None self._internal_gains = None + self._thermal_control = None @property def id(self): @@ -40,6 +43,40 @@ class UsageZone: self._id = uuid.uuid4() return self._id + @property + def usage(self) -> Union[None, str]: + """ + Get usage zone usage + :return: None or str + """ + return self._usage + + @usage.setter + def usage(self, value): + """ + Set usage zone usage + :param value: str + """ + if value is not None: + self._usage = str(value) + + @property + def percentage(self): + """ + Get usage zone percentage in range[0,1] + :return: float + """ + return self._percentage + + @percentage.setter + def percentage(self, value): + """ + Set usage zone percentage in range[0,1] + :param value: float + """ + if value is not None: + self._percentage = float(value) + @property def not_detailed_source_mean_annual_internal_gains(self) -> List[InternalGains]: """ @@ -107,23 +144,6 @@ class UsageZone: if value is not None: self._mechanical_air_change = float(value) - @property - def usage(self) -> Union[None, str]: - """ - Get usage zone usage - :return: None or str - """ - return self._usage - - @usage.setter - def usage(self, value): - """ - Set usage zone usage - :param value: str - """ - if value is not None: - self._usage = str(value) - @property def electrical_app_average_consumption_sqm_year(self) -> Union[None, float]: """ @@ -199,22 +219,22 @@ class UsageZone: _internal_gain = InternalGains() _internal_gain.type = cte.OCCUPANCY _total_heat_gain = (self.occupancy.sensible_convective_internal_gain - + self.occupancy.sensible_radiant_internal_gain + + self.occupancy.sensible_radiative_internal_gain + self.occupancy.latent_internal_gain) _internal_gain.average_internal_gain = _total_heat_gain _internal_gain.latent_fraction = self.occupancy.latent_internal_gain / _total_heat_gain - _internal_gain.radiative_fraction = self.occupancy.sensible_radiant_internal_gain / _total_heat_gain + _internal_gain.radiative_fraction = self.occupancy.sensible_radiative_internal_gain / _total_heat_gain _internal_gain.convective_fraction = self.occupancy.sensible_convective_internal_gain / _total_heat_gain - _internal_gain.schedule = self.occupancy.occupancy_schedule + _internal_gain.schedules = self.occupancy.occupancy_schedules self._internal_gains = [_internal_gain] if self.lighting is not None: _internal_gain = InternalGains() _internal_gain.type = cte.LIGHTING _internal_gain.average_internal_gain = self.lighting.lighting_density _internal_gain.latent_fraction = self.lighting.latent_fraction - _internal_gain.radiative_fraction = self.lighting.radiant_fraction + _internal_gain.radiative_fraction = self.lighting.radiative_fraction _internal_gain.convective_fraction = self.lighting.convective_fraction - _internal_gain.schedule = self.lighting.schedule + _internal_gain.schedules = self.lighting.schedules if self._internal_gains is not None: self._internal_gains.append(_internal_gain) else: @@ -222,13 +242,29 @@ class UsageZone: if self.appliances is not None: _internal_gain = InternalGains() _internal_gain.type = cte.APPLIANCES - _internal_gain.average_internal_gain = self.appliances.lighting_density + _internal_gain.average_internal_gain = self.appliances.appliances_density _internal_gain.latent_fraction = self.appliances.latent_fraction - _internal_gain.radiative_fraction = self.appliances.radiant_fraction + _internal_gain.radiative_fraction = self.appliances.radiative_fraction _internal_gain.convective_fraction = self.appliances.convective_fraction - _internal_gain.schedule = self.appliances.schedule + _internal_gain.schedules = self.appliances.schedules if self._internal_gains is not None: self._internal_gains.append(_internal_gain) else: self._internal_gains = [_internal_gain] return self._internal_gains + + @property + def thermal_control(self) -> Union[None, ThermalControl]: + """ + Get thermal control of this thermal zone + :return: None or ThermalControl + """ + return self._thermal_control + + @thermal_control.setter + def thermal_control(self, value): + """ + Set thermal control for this thermal zone + :param value: ThermalControl + """ + self._thermal_control = value diff --git a/data/usage/comnet_schedules_archetypes.xlsx b/data/usage/comnet_schedules_archetypes.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..8fce488c35d61f8850ef4ecbc7a22e9283c1bb83 GIT binary patch literal 348793 zcmeFYbx_=0wlxfdAi>>&y99Rv1ZW8E9^BpC-6crl+GyiWaEBnl-3jjQ_VUa;^UU1u z&Q#s|{rkRMMR)N#r|O)&&hE9>UI*l*-a%tRz(Bx3KtPZ}V9$0%AV5JtbfQ5(U_ih@ zeG{>@aWb}X(p7f1Gj`NwbhEZ10YO92$rDPy` zHZb^eQ$C6zKbi^ZcG^BJEG9O5?=CScW-NT~hcsXy8Q`Qv3EJdEBkW_xA_a3>#6=Wf zgWcXfz}Vm)7UvyS9P<3|pLIezn9z2l+x+#5AO|X%$@X}5B;DjY0*V40oFHEfvL|wH zI6hsRn7FO-<*>EJv%_rJ<3=Y)uENc;?QA5?3+_yCui;+W-Fh33G?*_@e|&tT=3CDC z#=m^{XnSo2_SsgI%H6gWm0A1Q?$XH*bUa8tZF9ad`G)fR{jOg@qaU8{4@5lcp0g8ET|!;0k5PZZG1~~WRq4*qI)t%&yXys z&w5;`-}9TFZ^eY1avO*|GAdoO+*7YjYVzFjP(5AHlSJVIGEAmgRJ)~~fBb+&OK=d2 zJ`(TtC$lO!EWKunrrXY)zZQrn`wa?WqGL)_hS%4BzEcF|S?bR#ucEbsMHZ z&@D?fzu~Cb7#I5JYR$tN2><;nWA^(4XACq1L@NRW1j^eCH!CJrTL()6TU*ONN^ZXD zH`_dJOdnnI*T6o$_ZTFQ9E2PcN6Hp_3QOj?WGcwqecw7Ymg>UAF4t~TmNRlSjrkOy zSZ-V^)_6iMnMz9lw#wrDhq=LHS_l)on>?kuA+xuk(?sgxbc(zATO5R%mUQJzIKD6T zFQa{j8JB9%P!>FO9}1GHH-DDpV8xkwNm$Nn*eRgBmpcFbvml2e`F%kN9pRX>ND*0C z11GwHeFS2=dR0`6`>b-EDgkLg62SkyMo+3KLDje@tTfEcI?1KKlrk)(HXiIuKP{|+ zt=l%O_1zubrqa$&6{?-|sR{wdwg|p~w}dVUp7CBd)8EY0&}qQ;h!>YQSZ^k}%RJue z>o`}T3z~-BpiElg#p^+meV~kNI_aw&|wY8BFRD#9Y&&Moq1hz)@xM z+<0-0!Bo)w_(4y6$5b9HNFxScwYy>$R%gwD@y03ZL*mqXbIEH6z-oBt5M0ksgewV> zeq(TPaX1LR66?(FaPHKh0+@;U5jW;!PY?+M^8>=&`}S_25liYYt-^Z?08d@K+l_X> zBb$VU;;+KPG7?bQhZnA2i5WmEMpGUIqe4qoTS+=o7*mPHFE6l`5``igUl=%VY+hdR zahSjL@|nm2znr~Vcp)t`QQLl}*8{YF+NKwWIpLp64LiS#9VF?>Yv*f#ZG&6#`F(vX z<@$RePlDY(uUPPA@{F3ByqkM7U!gAvM)PX?f^;{toSpJ;bqvk z@m7hij9`^K@ATpeZX2MKT6Ooy+w8r!FN99Ht0jSFm6xq+wtB^f9IdT~M^jx|PufnS zkBC7fHsUJ`BoNVlgGLaf44yJRIJh~s$Gyy`{{ChN5A*4$GWs+h>HWoi zsG++6uW~i@#W(||hvcCdmh%>g9LvlgXhc)+-lx=i_U4pQ3*TyVHHC*E0;WYf^F7D{ zuv_D%rD^OAzbo9n`zSVNWJ6XJe&+{0KP9Bc<0K*`jZi)KUfpRg(9;Z2dq~-&2GaKn z>``9FtcnbUB^`!oJfv?Y%+W6ydJG*E4+Ekw<@HQh@1OwNHE-#fN}{e50v$K@D4_hh0;$8OpVm1 zV86V4W~*s<%r1;+?`$60OAMD7AsZhu0x~Mz*n4?vR{wMdvuO|@(H?eIKcAfIKf=^s zZG5LMofRdGbfKkhv*pU*QxPU#>8;D@k+rcR_lC3X`UaM*fe?t^3|bogXypoI<29nK z4P}@Xbg?de^0i6W1qR^c2@s6Gh{3;D9}NN_1So(F5ZN{QgXcb`9Pkl%9iPNT!e+wo z=uDNlM_rxmTuCl*46niFIm}7DvkVHRh11cjqT5j)2155k9J8-TPdlJ6grcFT*j(h@>)f*`UAl*fo?jSOp#I)AwfR&|{X)mmBH0sF63}-k+ znZ{5Ocxn>+RYIf`CNMLLg|biVT!0d0gRK2^iKl<3tTOmO498>n7B zK7>bWg$YBVPZi+E`T3i|eOXobw+wu9tWH?CbK=?M2JzJr`)#Gm?tL;w6m zO1SWd)WoYQmF9;dDU+3-9x_uku#!JG1b>mq9hExs`F3m*|UuN|k z`OLOZ6#`JXzh$H*W7kB){&LexWmMb}xl9zmGd}x%1jK&wd@Q!NrU`bIp^ef8?=GTTeP%)7T@5S~v+K|-A(khnVoS_F6I=bK z8ek23&`i0q58@7e`4k67^1-@Ld#ZCOV4MAPfcPyJUtj3W97Ohy%_E6PQT1A>zzIJ-dh3MSx(+t0(=gAW<*UE=dquU9JVyNUY@`U zwllwP`3|mdu#U#EM}WRBqJdmDTq+s6)vhXmP5Jd^dy<6`qUU3yPZpynI$E-q$xj!r zFHgIIFOKK2f-jf5zmefqA)?oE_qH#KBsQVNPT;++@eFU&R1h62XMB50hf~y|Zea|6 zvkG{@w74K-pP$@RU*UPX@KHm4XgQsPFkUAz!^)}3wkFJ}LTG`Pp9ikM&>T&kKw-LA z-@Z-x5egBky^<$~S5@lyL4JOQAC~6mK<91Lx%Q6%OTX@llTTmt)wodJ2HXA^6!>GX zNGY4mDA`pq*5Gw}zrR*;*7akM_9ZR9kN0M_T)MuRx1?Oo5(XE!2i!{5JTQAwWR|P{ z`t!$@8zqmBhn=VF{M{K>vdDAgfqgq8+WG#wG_lV{9Sd`)p`IM}X|vo$9g7*9jQ5w`@>HwPmCS!4UWE+B}1H$vnv0b+=&{%u zN_U7_%>hkfPIF;IRDT7Q^}4E#)R#+ z4^IYhC@3Su*HgB^TXu_MwF}#i<_jxJ9j7nnW4698*}mz|OMA~>FFQ8dpCe)OHabsZ zKe~=bO_RUi>mk+NkG|A>rFKYLpCNU?K4NuP>{?Z;G#2|b>gT_3l))WW%*~=ut(W52 zg$UR^6(fv_sMev=XQp#WW@wR$KZ&nvF`5zkEh-vqHYM4M=vaz&%noW3a~jtQ%M){& z3>(Ft5G5bd40l8Q;nJaNyF%&iI@s2-HKDC|ufzb8#Ky8pa6iqPhx4v9N9|rE7Nz(n z{d10T@sK;&t2ptDF6Jy^Wc7xw?gLd$JNs+3|FM&T>qW*wUSUn7h0l2NcQ)|#M`)hv z{*hOwFpl?2(HuI%QJN#>Ga|ic1XiQr7~veYRI`|fjx4{XH=kCtUi8YsuNC0~s|#C< z7PRA?lJi$6OF83-xWelt>lMHabThIAi>1o2z=LANOy!bVNoNIc%?Z~=eaqIYcD!ry za*Gtv3MEjk&tUY2gR=m76>_*?3|BAt)DgB*i6A^oX7Z}r)ydQ44$7fUmO^OSQ1K=- zX06sZX%429tB`rG!Kzv|>sEpfR$nU3wxbV#7JCB9(dU%vHzp#6y-sA9;cpcz`_HdW zVb03t`@#ONZU{gNup%X$%A#c7&ZR2;L^CjKB`{qT#3cYIqAW+sfx7c$j~kdBEK z&58xG^5Gxcu4ZhY>XO!0@Sg9UN9FonJ)VcR>c4tV6L5OmCiU!{vf7{UM7fJOP;aFc zv>~(Ds!m4Q8|0Y^;Flj3H53DxiT47ir<(C_ND=y zux;+VGPZlWWjf%v`t)UbnG;+tUD?DVLkJ`V5MLZ@&+YQ!KjoR~WLg+~PTDg9%Qj37 z&hjpZQ7;`B@nu?7CJ;K+8i za6jdJ?DXQ5J+om*tp4Gz*5fvCON(I$v{5s|$TFhR@UpC(j_p>N1HlR-|(e*(_EDnQg7KK34@gl1ufd zIAOF4@54qsn>o;T6Lq#0rW$u02}(u0uTntkJnDak zvA=t|^N9k#bC04MvVb_-c6vW)B7y0h$(38UVltgGeSuB+v~^;0xniTz!HwjK5SWk7 znY!SlY+5_9SsL6z=gd{WsCIB7xgr84-;E5ZF78TU7K@qXsQm7atelX<^#ni{YE`g_>Uph}Lo@rr&6Ws{|RL zzD+2wu(IX5zxGLoC&fia!wG=2;~Ey3)@_o$nE|AtX(qxHqs?X{uO>Gy$t(+O@}{kU z&6$dgY6r-YD+*vfTIaumC%K{mZlQHryjsrmJC z$Y>sey~S4u2E=8n$!8_6CMRBV{Bza-)<2`CdsXlu;SPGs?X3S6N}ZgrZitP6(~7H9 zbR!;Llm&^+fUT=?74L}amz5G*Rdg%u*8@82`G_<;zhWL7P zT#4o}K?Tpl-M4yo;^AV67JyP!ki4Fy<0r2bG56O0gtCRrLAqF?CBQX$FBdJYRy0s! zUR;{FG;1oRD~jl3voj-8&5 zV{R5+P{)I8-(aJNMpnT^IVcxX`IoXN^?E|@oRjQ85oTcU!er8 zoE=mzRyPsWfIlHizGw-6jovFp%VY=DiaAY$u}1Hepslcjw8)z+0VvU7TJ-pu-J=G= z2s2)LoU&xQBDujR6(e?q_#!z%lbvG>Wh&7;{1ynkKm#WG&>zA}buzu4qx;pACzYZ| z+Dy;iGj~Vg?M(D%lr+3JsvLjlVwTeI(HX?v3r+UjUcTHpzOL!Mv@sI&iFp${>2-{; z!UZ=*GqHmT#1hQ_V6i$2$_obugAVb_19v>^4-=}69(>Vem1uQW9 z=)DRwes&bng=Bc)4LkHw1N~SmY2hMcI^UWYq8^8bG7ZmqHy1--!k+jKr+78G!4hF` zK!2UgOy`~S%HB-rnyL1L1oB70P-Pn%(Q3~hBI2c-1nC>|j)kL^Y1d$QYFO2LvmbPH z+KGf%8$(Y{#y>o;x>d@?Chdk3ZY)@vGE&g?$4h=?HzWZGM8?X!~!PFgh_lxw}NRWA=tqEi7W7SPR<03&dU zzm+Nz04+-YERgcx7`hpW0^U+(93Y(%xPop*reH{=q($=Dtl;9JpN@^~1%LGhorM-7 zzT^Q}B$51t&`P#Mfw^_OjIa@ctxIg+gQtT(POY8jZ(;qRov6aO*eR#`^EMtG^SPT+ zf`qwM%gePOIeN`Yj!X_=4ecS}igh*V1Lu@V`h6Rd%r*8}8Rpv2cmw}Z8m+eo?k5b` zY&&SS6bhpr{Y&Yz(Aue^evIJVzDpLXkIHi8i6h`q{WFvy6fNrt7&U9ysXWX7pg>gJFEQvl{3O{@e*&GWnG{ zAuCz#EA@UzZb|JI!VZ5WA>olC`hA8y0GPg(Z>F~DMFuw=a=c$RKPtl&RxOVU{h>;OEdP~=ypW34qC z562CCtM!V6fwBbwd6c$M*O0HQ1Wun90*q{a=CY#tpb09;HcoLDF`kW!*BhqmbR>Bv z(Mur3bA^B9?^5PF0Ecns9@hqf7y*N{-HP?zv$MJ0+q5nd?L+cHhN=~s#mZ7ZIt8#I z&|*fCLx0Y$T+v#&r2NkYL#TkaM41|RP^PG@Tq0f2T%ybcq*DW*(9PrlBU+GBHLfEF z=bQ205+I3=E%;FKNk40}{KF>(_o6#U6;6h>Aq*#fI+Pm_M=b%qxvQ*j=7Q)9t(^)% zcsjhZF^X(=&j@E%5HFc%prXQ5?iQvHc=5r)a`-_g{D#XDvI0@EPIecT@(%!Qoyq_O zk}4%C^g!aoMJ=R?#p0NH6|f84j7|Y>vGP~oLB3+9N=c+75Ul)%25sJT{3nL;5h;oA zlFB5olzh+u=wMzL%KVbR$sb$?TuISTrD8^uV^Hu@ixBVNQDv{+yEj)X#vKNcRGOH$E<%wOed~L#lobyU|?AD0LF@7r@lOwAIKB%()_uLb?mO_Ski#u#@6_S+X!UL(9~yIrroALz$hrU z*=qN#)}^$Zk%?S7d#F6vyQw^&c~x_EvV9Dz<-5(T!(^RP->2OC5r-CvXQDb5f$9gl z$`w*x;n-WS3FM9Vq2g49<~mLgn_gLD`Q%F%d3vSw-JyoTUmF6%Hm?SjS%IndsXX;ToLsvW-90*b0C5Ty zV-Z8{R}GZr*Vgr`z3hKSH6L*#{sg}xfNqXC==~8}lwDF5?Jfdo1fvD1Y^-Qi0OSqK zBU&Wb8^q>aP^6Y}s!^u?wr^yaX_c2??Oc;7+b}nX%)6jK&9`r4nt23DFy>uQp>EkX zYLa!C8zkbz(^%e>Yey13OdH*EW2T&RY~5cT8ehz2s)hMhE+^3VDM4ZmGK%1GTGOij z*KzV6CYiZKbd~s4<>5lR0Br22ld%F-n-pSc!xt_``5~fZcJU?XDUxu^Xt}RMd`oujz&cB}1yqJugzLDu6+gkEZ(6(0ZPMN$Oer-?zuOsj%~a_5?8*@l@x7v2RO zYQ8?iIp*)TA&i?LzK1orwOn}E*Ia!z#?-sOy? zEM?O8ebgnX6Vh_Ij?CwS(uBH9*;h*+EX;1y$;$eIw7zqnrdn z=b8jrm)Su^UX8*8ZReVF*`z}w*UY2bgt)#wkivAJG&=|iM>`K~uvsjxu#82sL>ga| zoGx<%KyHBmATP=Y83gxc7vay=v*GtJa?0GlkxV@0CwM9;O~q>L>p7ek6}m7+lI9>Y zs0mCEeOLu)v;Q78h%fAf)WdHN4MZK5Me5;i1~Wkqutohv>Jhs44%Dx`2M@nm(+?;f z`cf2&s)44-cqSl>i3NPrEhH9Es@ySDK4Si)(>CI%l?qg?OY6whA-`=`G-^RI$_4Tx z51cd9HHIgP+Yr|qfS-BdFz&zfj??y%q+=c5S$hrDA9c~f{vi}jN8-uE0I(lG2*ujM z?EYpSCa?f=s4HKb5GDu#x2XJIuHYsx0Ar|aU)Tfpus~d4JE(0USC9&qsCns=xM;_p zPE7z52POb4;sKnZdViUohi#^f_C}4O>vkt@BUiXPL{#1$AikRB6BWsJ3hS`^ z&TuxOe=7evKX`m>~9-*2Z(|( z$0(K|Z4TJO2N8tXkTwVI5rJUCh)6vG_V7SBVJ4_;!dEa8A3?5R0wfkuFe~c)W^gAa z>~)t&K)#!3nD9MEFdLHQ;5}rJXBZKQN6;QT$TUocBu(h*!vq1qF>ICe!LOJF!AS|f zmgGme#`sOPjfp>?lfgoxhoSgWRs5!vV0?M*s^zqssJ_I!?k^m<)4Kn+z}#Od9{Cx9#JipUkj1UWzsm0#ovY620Y7Pd-~CUS*5 z0Sk!s&Pkt~{~Fmw{E}0+>V<}z4S?kkhdv5?%U=9i^DV>a(?396ESpYYUDc-2m1^d@ zKX~hCF|@$d`Da`7036i+O}f=j95NW*Qg=y*}>>wm)l@-}FbpMEn-J zOyplvvig<|#83D>Kgd8MS6f~7?Pi_vaFObVqM-y@ZO*%3HH#dV|5c0{Iq43( za9wy-5E0DUpN?%R!lGE4ZD#3K!QZ6iwL$#FzRo7XHNva)J_o z5mtz*E#!nWfdptoT@i9ZnRo|?MdcT|LYzPVu!ZT6dIXvwPw)YVQ8{jemf!;#%c@ZV z_#<)~hck90nDJ@~#W(d+c}JCXIeAw9-zo#! z((mL~dA3*BJvljivT%};xv5nJS^HE7)8ti)%{MF-fa=XOd>g%@8U?e{q8gMxfRQyF zX2&3_L5EF{)lrABTA;VN-hiG9#tGgCJxi3;nE}uO{hRfZ-Le9O?Ean4%&1i4>+d+U5L8VTDNt5j>DLwebCx>0TYirGRZGrl5wdHPGq&_yLi z_o!khnW!VU@TThm=cpuRm~qE>U?(AU$!XVBs{>6Xf;VjJ^MB z@Oqoa$+R^$OP3X6rBhaHE|%oxw;3YE7a?lf%ZJPLYwi^Rt%@n-P)`JIq{`Ll7tvo# zTkJP!CevHINZA{;&0$mAQlF2otp8;{w<^GTz`S@+`JecU2ju~{C4y=JKg5Hi$+R|@ zod05rm_Qq?O>Qi!tc~&2ibea@#^xq0m<}`4#*e&O>Y_=PlZNl?FBz2&@rjh`n}|fL zA!F@l@%Y+)jh9F|l7(lk&sC3|*ARzY+E7s*;?erjh?kPi(ip4hkti~B6uu^_WEUyJ;wRb_p(3ah|a!S_CTKr4H9cP59id*o4+UZ+@4PD+lTn&I7H0g{>&Vzv z?lpZQTeWLPT>N${m4N=7!ZAHoR^mxW-K1KSYD=G)@KBwqh%U1>&55ojlTC}RK9*0u z-^KdGe-~1sVmqZ2mwuuYCPPyUulfybD;D{ThPYT>I-m z!b%yg1|{2)b7n)>WV5Wm5zW0|H2}3{B-%*kb0s_>ff}#QQl0lh( zd8wd40JmgN0)Sg8C=&3KqFaS*nL?Nm?F4;;6fJ5S&b7QcBO(`fI1V*-TewnE-b@!b zFU|GJorSkDv%)Uss*--lGmVYA!n2Q?DdwsyR*)LYVsgo+I{+Ck*j_5$tw?+gsFi-X zbv({B2nq(MO9lbR@-x<>Pv?m@+Gtsp3D#ILs4>qr*}SONJs}0*?i4!ZpnR~;BK=H1 z-iA2eJ*@u7(tgd@T&;?^9;-zNdhBrn+9eZImf%iGH{Ql^l^(u$0ALXjvh$j&Yw32q znU;wJ*-Pjn@S}wk^17DXmMw3_k|mGx4%2j;f9K+SqUI}0*jI3wzoa}poVQFo z_Hl5jSM*wx-7;ib=o^e^MCe^9>2@Lnj(NXDG6#I}4{dDZ=Y5XUT&LVnN*A@HKn?1|D z0$Bz{_uq2@tpa_63e6$hE*TCbTrW!E2W>Qq3Su8zT+qtbu(#!c)Lp0S3QK-DQ&}Kz zC-5eCNo!O@8@I5$KG^te#-fz^1YHZ3z+$YL0zz}m9d6KT{Pxg6@Bk$o?|r2Cr5;JL zGRkf-GAqh%S+XO_ZXvRdl->WJRrC!yG#hl%WH{IGv+r>IWf=r39R*Puxtj7W+hfBL z)WE?naFp!R!#+>QoaBuMc4(wLGoF=qDKysK!K^fg1B~nx43ffa0rB#A$!|B!lLDm` z^(wnKxUlUOm&>LF7njtn&0q7Y_Pzd+B#ih~6ya4#a0KC1Ksdhes$@9WaJxh}>TtUx zIGphB5<#VaA6;v_p?icNlrY&M+y)f5tip@WVZ6sDLh5E~Ng@1Wfmz+Vk7)HgR0c~= zw|Q#T4Yy^=4j3DyE`W$sIAajZng_Og%O9=Mw5SNx?~D{Vbt(S;jQv?rs}TF&m&1n@ zx;>c|Aawh=%_lwsmLS`^!xk~cK+}Z9bGJQHdQ8MK?h&RjZfVjFib3^2Q7E$SnB`d- zoBqj?{_Yu{f+af@4W6kR&0L75nETr+Ba9~#KF{Kqcx3}@ue zHqv!m#@wqYpyfff;NXC-{Gz7q*!+gcjEY?`{g5z`pYSY(Y(^o0pZ^mVB-?561r>p( zp=ob-q$VK39hTBpto2>4)V$*#XTg7J7jx(iE6i23`){@V9l^?9;@&l9Y1HlO`}D02 z*;9u6Zp*DzJ_PW3du_W~VsDl~9z~+Jmey&G(Vm~HSwe16rAm!qt;JAEG6}rB+k5oi z>&Db%z?i2woGh#t|Bd${$+)u28O_YuKDzpD(Hlr1jkv4&Zm}EhLSk_R^zWiJ5JK1x zby>W6>|!?fLWprW3zhgeov1xaIoP_-2_iev%NCApTdz7+P7Vlv32^U}ijQm-EB`#4 zkMg4Gdyr9}>*e0KdazGp6l~AN3A;D6a!we`zrEn}dLg*mq#r_YU3H|+M5j zv@eDLo)Iy#l1M(wt5nlKjIaE+PQi$}FEfl&b9A|=wohb=(}E=epclvu%9GDsb`cv` zA#=E^dTx;$gdw-M0(y608yF#Dxb1q}J!e=wT!_24?S^;$`kTaOI_xhB8*WCJZeL09 zKH|IApm^3f7|eRs)2c;0llp5gf%13^$t4k8o&px3d~#*6eahJa2@XY{5-5b?xp+%F z&}Sp*;_p#ys&%doKH&E z_E@yWMQ&afSMr?1Y1{n)x|<`o*?vun25sJ+?2S3&czN!C@qq+<;HN@74){`$L_b_d zJUFI3RE1>!1L)E>^tJo5BHA*y z_MIX3cp?%rd-a~d_n0E;GH2-DMQjj+I3ljGJawKq(XHRucjnQY78uE{q(!fi(DwT# zSF>_QTo~Q>vtA=**D_86YrBn>pejI&Q=fAU>Y^pS`2^^``)F&3VSn}kBMyYsE+Y`Fs#^Zc6_4>Pgx8KZGs|Pgaw)>?IzRR{_Y-h!icC#)K zQCDdU6RSxQ=@Au(*oD>I*HbW%d+O&$KF1WvsIKj$<4GGtu7 z>yM`kQFrPobv6lBoIuLDup$;?E6!(v^%l%diJ#ihDyO{m+(}RP<*UQndAW`x1;X6l zabf-)%pj4;$q#nS&mQ)yY~2zIW8WF}1a!asfM}})i2c0b?u0v&p|arI=+@~WbrJ5w zIvb(dG0xijh@qeU*@r-Oe7KY8bcF0cxD)C8<*3UqS52_2j)``5!}6k+LTX&l!*;Ry z&RERTsntR9CH0aU4~vSUL}rLD$9p=XK;U2`lQELYNNyT_p&(wSTPfzoQASUUp>_Ie z{*O)N=l-+c9NiCYWb0tRN@%Z7>jaR0HA824e%a7Z4C^?MW6)1@>s*jy@1GdgQ6RZc zSla!gH&JtIH`ym!@_6D&2XZ&<2WV@`sGXDQnd)NG4!Qm7EFJ=(zR6K9)*YCGjbcQA z&gn5CgLC~CS%Y(}FEGwnt{)}I%HvU5535kGq#)&>KVzK%{VLykk*|M)T!X)RlkQMH zm}d!o+3%mo)>Hj--#<~UH~Fo-ewJ)F2zNrAu~0tfXSII1(4R5RO8wUUY<{7aKkq2Z zEyzY(;AN#!!by3)jG#I*NUJ((KLh&OwfIw03Vu9z&WEV@G&P%X_)76-uAK5rEu5|< zc|Mv3V?RcaORsK5dM7kC>Q}OSd+AR`KQ=sOyb+~Wl&h22sa2$9yiqtY-=coWku@M{ zL_I6;qlU`*a3|S02i1;tR_=ES?ZvbX3n}>FPNH)R$_MRCy0a3>2jfhu^Azd@)$Ve6 zlVG`34K24ebVg&TsyFpiUtTRH^5uc`J3rlbc3IzuLxx256E*1!vxI)r0>zRP@%+0b zwCy~x3{%p*|7A)>*RB)Hb4!VS#~yuZB%g#oG-qU~u4ZZR**P*D6)>mX*`UfXJp zz$+(Z%wssM&aluFTc9!6_&5u2jxi(cxCbE#U8ijnAP8`rL-v9~RWNeet;@2XBj?kb4@ zDnS%=Wt;vcM62s#6mO-JQ|}kv&U9Y6Hs$82^*^kFv+nd6QN&2}-+_Bj|8Sb$%#q4G zs&#^xTF7hFPFJXQoU=eb;`dJ!>!y$$aBeV_FaKhxZ>sL8a@dH9f`f(dvcIK%)Bf5B z$_rN6QhnYjlV$1Zad0lYfpkn-+zZl*%`KVs$Dww9f6&+rRqt*7_-2h3!=F~YSpx&( z{$xaf|2uDelj_Y|w^8is{t`8#mGKRWDCAr+T3uA9dq@lSFo(^mB2^*Xk}y%3unO7P zf3J~_eo}+YaP@|x^ZcupKfucNR}^net*}?`HdS8pKZAd>*+qWQ7kMrc$huv_XfeEZ z?TvqQT_v3la~p$i4Sh!)MSTLplN9fL#cRaXE5TdvW>&a`>g|4apNkfKq^D+n>OU7C z-^%FhIP!UGt=ueeb%k$h&nW|f8$bH=Nbjo7pL4)s=TKR)>bkiIk{(;%RKcPk4M`{6 z3@qVV1rx}OjNELcHXWD8D4bRtDy+J6Uf1y~vN`%0^uWG2;l>%*h+g}f!=$%h2`5+W z{%KvaO=He%+H7aJRK~ixTDym?W^tW{jY!_;pp1M$nIw%_B~9_q;@$|(?0G9lg8`9W z^{7+bq1!WYUKszn8!=Sayn2_aJqDq!B%zykBel;qIymXxR< zDdjAi4qz64I`hdL_4X;n$RwcGIQsQ!8MYN$3Uxp6@*@HnC$;WxzWqO1t@~XLlID;- z7|^${6O!i8JxI{kuq+afkiGXHJU}IC+h0wVr@f6N(S#|q>yr~$#o6n)ShjCuImE&J z2%1^ft`TLFgdLsLe3A--MF9PSH+q!p>H@sU(o?rYNa0hN9M}j!F_C*^K3RxCUns%} zZS9Xei1oUD=k#BEYcbaBJ#LcnP@DbvZU4jNh;P@QwhL} zhCGDk(i*r3bxOF{s_*E1(A{+1wL5Kcvp#1{JUy!=q04P8_;eoo`f`zQpMCnm?D*rr zsYmhteri5Lwe7l2a0|#A#zDu_$JB>r>NB-$`*4g=!V!`~rRVv)Hq(tCU zacNe)uD+vE2?gl(>;3G@*}Y!#Z4K}6(Mn~=kdj~r4EL&&NHH;IGCy_9eWJ(SyRCmE z)$7S|?olnh9(x^SCwep&^gj%|j82o4SVh=>H2(Hbkzdp{Tz|m3F|NBp`r1A9(VGw9 zGV~aVRSB{vcw>yM3(A^4CzJVL6Zf$tyjwDVX>&IjVeGwa-rAFjUHhW+sD3|%sxoGb zb7?xZv*Mb5@|5K+8Nt6XUorT5MBsr*^zFZcx(o9~o2=tAtL`CiIuvg-kY)T5Gus18 ziUTL?)?Lk!Iyr`S;eK*RSvM~qWuk|S-@b%lur-y0GB^7z{&}IoGR?(vTgKDA;c;_j z^ZrkZYuEQLv4A(w(*^C>5%9ks_B-FxPXP^?v16hWO6Ey z;&dVvGF-s|B+Y+XJz*8Ni<>B2|J0W{|J7rH@eTXet6{5>0dg-nf?EG;fMxCZ>hO*# zZq|8NGZGgAEF_QS=Qm zG*($RWAZ=j9*H0=vSrHdKn$1{Ke-?K(WYJOKNf4BY|}LL-L!vtB;BUS96MsW*Viyf zc((_~w^=R}v2Y8s%}5Ha>TXI+_q;sP)C5jCb2{&6#d!~BSoaFH$bMWGSE25VgYH)3 z1^j8ywmVR~6;jW4Y&TpNpP&PL-ZaeVQ!AL-1to#OgW|Ec4@*rg+n33eU$e4vrm zGkgx*%D9Ra0wL0-iJaXNy7Of_{jrt|QCEIIrrE8|_6c_&OQrdZQEr!dK`m2-e=@Ou zRZb7U^Y8vBx;`b^EV@29S_8U1HQEjO1_@dW`d>(d+X3OY!oN!ewE*12p$)mat*sG5 zEj+Lh1bh|@*iA|!{X>HGo?$;6N`U+yk4c$S`m@}c#CCaO5(G>O^Z&es^5hwP$bQ+| z)%AJ0_T623dD>oD`!!jW`bgK>p!Jm5t^}oEc;VK~CH;1GZVYc{M^wtae*Be$VNV+L zXGbM_)^B+9u`QW6#B-A+aE;ElrgKNLCl2};lW`>mk#X_3GA%3bjO z`YCqw1Ox$1?)6CSSoLKUkDe&f(5f%~u&+kId&BHi#PyOn?A z>_J_^`k0}RGRN*&nLbWaM&_@O@2A#?w*z3p_k495 z)I2?7*!k{CZ#CAZ!?hN-)05!EhnNnGi5K^ILXpWM?$#upEVYRBr(P9nYcV1deNXAo zSu<-Z7}1u#r)B7@oz<%tk(j=xXy~ko)eDRWQS^fba#S((@_|O0K2Q_0kg!AG!(k8L zxAblL){5o#sb-t(NJ7Jtlb?xD)p7eQVg{yDHGc3??=FgVh%YKTqPK~)Uk10cwO{(S zbpyS8agtX%fH7-Q1H=WJqWf+dygI;`xq?lZeYbDCtH7Asf=!YAJ9SWE%f6m!%#iC($iJEElHNDHem-(@a~U*VEK4tZ zlv%vt=u$guE>QP?ap63!Dcbfqtr;+yH)K6x!MjO8^yIww&L8~l1`~N+iCa%@!|y-* z5}V1{X~;Uwg141|Xe;f5Y^xl~CDW0{CDlmamd8IfoRdhj;>s>~YOz1txLmPuR`)dM zT*hIx$?TjB_+~nX&aiF#E1qW&9W!vGrr&7XfYpozuPy~qQ`$tvkX4=q50ru^D;+{f z^$tBDH*Aok*XT~4^^6(MAqA0B8is%>5GkQ6WKgeX3y+}W_P*-UWyj&xtxDRwr=c>g z$Z4C>En+3C;#!#eJA+9n^$WrlJn%c)$BQoLTBXEqXC5Q`gn70e~p*R ze91M~HdoH0Il0a$D`EXHeznkE()E~G*qJ1kH&LEYx)_;S85Xmownm;%xfq#V85X-l z1Yzd?aP}5ZZ7}Pib}21Vyto&43+}}oibHX?;_gtaxVyW%6(_h$aCdiideeS;?{m(* z=lj>*|5{`cCXkuDBd^TNGr)kub3sR+Y_6U!_=ZJ@%u3KWB|-=@z`#QrA;-?_M`=X% zYuB2q_vd6@*mSImozoRjc&*cs@jak3hej4CJ`;yLFV6xQ&ONKev2Xz4&m@@|OO4WB`qPD75W1ZX@) zY{Vq^|0`b?@ISJ-4SxUe?@ZTSpFd*v1&bZe^yLF0-Wfr`3|+_zH(Fyshn?@ui|Lju8F8pnPuMI7q*d z`kme$ri^{6)6m#|P#Q5dnSw}$qCGMOzM9eqkXI6y@3(+{OB?~zq`@0$UH`#Q5GTY) z6&lVc0tb!SV8iI?N0LxjHU^8jB!^SGj3DjG&iKoZXs)P?t|~jcMx*Fsw`TFj7p40P z-?~f1{2|3B`&WK`a=e+6c)__V?u{wApjF$$zjrE)+d!BMxeP|aX z2Hc%=pgVatZ|ugZimVYe4J1tvqo4ShK$l1Tump>PyViyeGo+{hjYnuH;e+STS!$4lu1lLz<7xSw% zi~+!(3A-x^Ua!HfLSCIqSDFc`T5`h&5CA!XhRC7F@kVCDha~`V8VylRk;6x3N4G1G#5;qUuG1|D|aD!zXIIDoPIhm)N1s(P&1& zG9Pf0zmqib5!OT1d-r)8iUSS48eWq1E!w5b8zT13PwYNfx%s_d z;@LrxUcwn0%iB(lHhQJxf4$Gsr)IFy?XRe;x9 zUK^o+V@argeuMgj%o7+_^hr|boRr%1YpH86DYf`VbiCjXA;Ry4a=w3(2SyfUi78c) zP!FJ>FaP*pF8rQ3=R1}>FtjMksJw3+)fYWhKr+@dJXVK<&$;E+>8hsQwvkyHs?jIG ztJ}w-f|FB5!Qpb_pj*2*)@-byiD}t9m`miRBU?D%50>->K7K4*NTJ8K@bVCWMQ?N_W_Omw#w#rN9q zdimKC1Kt4&OH5Oj7nl2(aFhb^cp?ugnDC^HIrRW8jM#ih@Smguk;zdHli*3Dp+1re z!Nvmp2W`3xKkEVV7_pU-;Ay0xFvx}AVu63Ot&bj{htX9u@X(VF%~on4ZNt|Rw*ls> zU01jhH@|7f8pc;ky7!bpOZK{QT0*B%)XD(Hoqp-uXa%2|r0rX)oqO97JpTBIOsRf` z1r*{M9ltFmaw4AB^SjQMHBgo!$8-lpf2>X8P2@2Q_bo9uXsXZ!hEP!YkviPgqxnT& zrx_M6h$m3v$@)dV|AM3*^suyV9de`X!2U)QUDt*jpiCidd=~x|$n4wgHfRB4WnNeHV^lp$`pu zZor@W17UCxLRgS}y-a|uO)Lm4JWeQZvIF(4wgwzCd1*B?6UFp4kgkw^53=VmvImC;9(Kkx zS@A4Y4j=7}H*UE7s-ME+wV5d4TDl;vKS2NW)IWR-Q%abH*$;zQgo&>Q-a4TVU!;0W zHBE!3TVeCLz6+w~1M~T5zUCt7R1x@o(Pue99QT)8aM=0Xpb7+rPSTq(_6*~HmgZP+$*^JzEOa&i(ar^PZ^ z!?SA;?BO_b8eCS%XooXwm%!(hu}1;0#gpOlq@fhZA&Zk5ad)wzLJ%X<)bx8bH}F*Z zh#>iNRq#lvP>sq}IPb?37ZICR{DhAGV4R#zf@}ov0AO<@ z!zW8;4CZken!90N_4<4S(D7E1#X22@7H;V_O}p>zfYR(-x9p0IEUbiDryE-1JlsrV zgj&f+9AU>lno2{lkVE3cLWB?c^cj9O0Aw*?4<*4XN|PHiW2Yp+OG*clkVB%x68#ul z?>3y*1Avd)K?Du@bQ^Z+0oEB^p@P6eTJwp=2K#n#&v(`G&UHOsL#z+X={W0neLTDK ze!UyF{sm^){S$5ywp=)fun~zeu!ws4o+@NT=hvgH+#ICmwCJHC zC3PyBkTYN1jO)~tA4;0#AG)~bk3?vI;Q%)bqDt_XRBf7-R2Q}xmZd@2=mP|Gx@yPu zD4s}`Sn*GX1%@0S7(t@cBqzRlbMnB}>>W;NJrQBRS<*o3xE?27y9Uy*SAGsS_!+~g z_KHT6qz}YWDa(1^v`>ktHM)_bSaBW}N?(o{p03T3-Yrs~ImxLC@Ad$9xP#iiGhVHl zB-@nMMd2==Sw&nRWf%T>xpMNW#xqH}Y15wmT=nB~kkr-JWjn>;3bFDc+V1VQj8V~! z_8-bX^n^lQxn;^N2GcOwAGUISfc%B~8KhOQlqDB3L$ouRMkBirag;&Vw**}+dX3oJ zS?Y@vj&$vmjNeu1xrSG~{WcPujXHL|>P~;&&=Sa}t25;Iv|ga9SGFt*s2X)w+-9PT3tI%F$`(hhjIYeonfr;y1Z=PX3aP91oY{)ZrJQqSs?p&o#l?c6@ItabtMTpsVzuO zQ&z%6euM&uY{-M3ueFREuxRcZ4ZhGy*K86T_-d077#%1H2#gkqDV8qLxodiN*g)(h zA&w8I?k+bEei-(NvF`Or?1sGrNv`In@v3%LlxG`IhUo7@T^UEHIo@@ptW&A%?&FMC z@OyX3o0~LAtZ^!=4pY#ItQ&KDL@G!P}G*WSdiDMZ(C!1w#*dq63jIXkoYPA26?ti-Nin zgB->^6MKrrpP`o&I_nT)@L_$q8wb?^aPGJ(Ex)at zqptiXZMpA~gEyEH_m8=w&Qm@1h%M$u=fc^5;910Bm{=qvrn?=5y14GF<$Bjjm!1+Fkl-qV?-5_lwTiF#9i5GX9t6m$g^t>tSpDUzbSK-x^~WbiD65 z_u%b7kvbrTwJ#6WN{{_>w@7O*7dJs?UThfMAmZ1{^!(O%vdLP*uL-2CgU-{bVu|{* z`JYv}4sscq950v&JcHGABty-0X!j;t%+xpU@~s$!2u467K297+wyS zANv^`Ix;$%rwlq;>RfM)wJ;`(yra12jn0i_?s>D_Pk6{5$O?5Xue@JwpZI?nJdb<7 z^vgZiqk4R!zN4<(>1R;ShYViwWnUvU>L{tpqPV^wX|Cuks@qqk1-6VMt@V*OW@AB0s#wMJVxK4v4vlt&T}hdbC!9|#qYl7#I)UEB4=MUJ&c|EXzU5g& zEO+o$Q1yQBrwA+5vf{IYoU3DAnC#uWj3?ZJ4HG+?^2u@45*L82t&7B@ylCp=+Fd@T zXQhtNnfuJ!sBvS3D6ia$`=DRFW$nnwmOaF?Yo3U-oHTrk<#cC;LLJAk4TC%^%2=M_ zsMnU^um^UMrz;MH%a&UPWYw_}I^{Kc1IrvVVRn;4nTruqv~+gkF0ozD=nQj$p_NG~ zwI$0f@pR(UF;4&dJ-OeukkIHFu6ZRd^CM9Ch>ZQLpGs?}ZtkrATEngE7M)pc@x?!^ z$h?^c7G-5|ZiL5Dtc~SJ?8It%I7YUIt9D~I-6^f^pi*cKyft;iV|a9{ES3DkDwCe- z1?JZ6l*gjE_@oH8|C;uCGwh%%Bh}^2WjjQM8bz5H^OMB2d1ocKNsmI*bXoJjZ7a5jY zV<-It>!)5K57G@)cMo|X)00*Oi6`w7KnCkhyL}_v4x#%*S&57%AYPe;_@*o9ma*DW=dVRa;1M{-;Qe$t+TryOZ~USA z^v+K&CX)JBdI7Rddj-^-wU`py{_8Ko_NV$J&N>ATYiQ9q{hXkbfl}NBa$-}|O~SYUDArmM^rgv#PbmBhxdU$mtf5H{}f4(1fCof+br_f&ch9m=`A z(B@)>8Pc5^dS)^jbR;>{<4%>%M;Lr8(>~^A4~pVu2R#rp-N>f&ZKG&WsoNZRap$)8 zKlo(Jn2&j;n#gYXYd1~>lQcufXRcJBGe_pKSj3JrFh2rqx%DM$c6YT7MWmg}Wb~7` z;Tq3ytul<#NrzZN@Kh3cqAc2Xc0KKG%o5Gd>LsF^HqUsjtQuoQ+UOs4^3EP&d6_Br zr5cN*8bk3c1)Y63*~ingkHgX=aYUNL3=ua6Msxj7y_XrL$;mivEcjjP#pDH@nI#Xj zqy3qC`Q7b&Jo`Xr(WE$hW$e0$XoYVav8VgO*2ySV!s})z@WfrI@U#4js zjEaw$?v#o|A~h32=4!NC3|den1is!=hxT_CssBvTkKc=|ln7m=#d+JmqRmYsT70^K zixr$JnPH)`BXV$;A!2Wzp0<-;h{MtTT}7V{q`E!R)ZRsdo$~Q;+kh>Rkrf*|6{MQ? zU=&y~p9*&Gp2YE?@}_VP4bXcxQ)sJutDHHAN=M zkjV@1296lm36M^beq8awnWT&x4RATRe%q*sgSFGql6j+cLNngFR7iyYOW8If4>Cg3 z9p#=XkPzn2DP1U(AfFk3e$QS=lQB7(WAdFxHKkhBdw6uv7o_Gx9%q*3oc=P6&tB+j z_fc$H7PUEs#oV5|c5ipZ<27ykCB`ZfEs}Qxdc1Wi7X=E|isA6)5x>2lchucM$ILBu z!o@EQ>uhAOH2pDpvM%OboH3EsZtK`G4=`_iT6Q3ByU1^VZ5`(n=$ivjox1~E^D)~>8+1}ed6;(5^ZUH&k4GcAorDr@ zWT557YlX`~*vwI+GJu}9Kq=wA7krqLfOm9VPhx5T+K!VyyYx_fWd(Mu z2j)oTe#9e7;V9ic$f?}}|K@;|acYUp9cJx_n!3sO3sBtv-6BU{M!8I#1Os|z+C2L-KVBK64A`VDih>Ml@!iaa&z+p0~B3yhVe2pcqW!IBB zKGfrTq2-qtD4{8}_N5K+9A@6lR5_CF!$37-u4yaV7hJUkcu*L7=ptQ;gzD?P|5%CX zZY40oTWTLr zpTp|%gXT8C!-=(K#bX9rFykW7mgY$H*Zt#&?sM4=XA`qgTU`yF>5yjSjRStg+$KuE z&hbbnng!NprV#_-kSwmMc@xXSR+TvS@>T*n9tS_shYFMOIl#&Le!*T5Sa&*fONFJY zno`zS;9qs+S`-x1I5effw=B=Kbf_zjXqcM3 z{>inbOry2k66U10wlg{)ns8Th1vmi~Ylgn1D)HtyH+=rqw%IzABDJ^EOh%ymy0;Aa z6e+pw>q(V*(iRZbnlEOdv@&)n_*5p}o{6sJ8Me9Hy`x2pB$ntHXYGxJ)K9{fr2b2F!l;@DtU~YCQZ-z3i>0t$HdUvRhFj`cfnKf>wL)Xs zc8p@T9D3?m<=*CEHD~l;Q+3;qR8@8JqlM%Tl*@3Ub>$lf9M&SBiS>xowG?FqtY{Rn z@_`G3Rj5=;9%Rg<;YatlX$DCUIdd;i`S1>dOy`uV2uC1cpFT!bhQnn=MqD)^) zsjM`_QL*}zIsv7Qiy}##VgP?g=u0)~c80zo7Zr!VYD&D~{yTTNkd*RgBkga`wHHH% zV|a!6-UPVjXjRI!>V4zmuj*??vRx{kiK;AN(kSpEb)t^#G=rXroWufbxqRVrhaQP$ z4Err1Z^no}#Xr#_SST!=Xl5KPrj)2Lqgzzy(fS8XDNKMELm2HcjKr(R9TD9DWfqneyTbDZ zG%MV{_T3`N?6m(fWPpD{Ca+EAD-sn^&fhMN zZw-%WRK9eQT-6@rrgRk}JImWnXGOXZ5o$IUXGb`wAi_2(wRd6_6kM(Fkkh$#+THP5 zSR%@}aeCd{^ixjGPi`eU5e(6Sj?eV+;wTaW@mRjw`a~_K$c;Dz2A9$F3tT9U&K8Xc z7&Z9~9gU^KTT;gO5ffrMeJOy`Yt$3PTP2kiNSSt8yasH4D8vCt3#C|y!y%Xz6P2^% z)d$v1Mm6AoWQ7(j#Gw%miipx#@@fNrPt-Ud8KGtianXA7bUC$%0q}mW>zpel;=nYd z%*@HKqBMmpR}B6}Ifz>%mENnQZ5*QUSi;;_@o|F)qu5G!dgL1X)<&MCO3c9lyzvQ~ zjZeKb1;iT`l;YKi?uEJvdlyo?L ziM6V?gGDaqStsjKe1lQ@nV+Rt6+16tuN@=`XpyPJnX8e~(a>+IKYf19_i4;R`GpwF z`aSylY!Ue;@7R?s8rwq;mE3?lDH&ZuvCFqXcT24RdqCkLr$w2*j8a)~2&+ZUCbq=h zs2c6vNe*`CNh-Z^S^PhyT9w|#Vl{X4qbz-V<+9iiN0sV-YKuEjT87thG-lA;dJ~?) zPkr@?Tm(xC%Iy_{aCjQnN5Wdg-GGw0Bh!a$jIOJIb{=YCOry&jzpE!NM%P~ni!|Gd zUg4xi4VPy(Jg@hOUQj9?#Wf+7?uNTC#$pcqZw|H(@2?=RY?DiKaeOu~5iC){kiqkC z2HkkK{QNd16aNpq9oT9Mp40=C`)sDYCy((0JsI;St>u%Hh63u@GL+%FZ!%u0yRHqQ zwBxmAYNf^;Re5+)K!Q`HaQVKCxszf6qA1k-aC%-x}krOKhf&Rk_-jdRDXdYmu51x<$Tz zxpG-}NWF5k6ZNciFISP882VA3e!6m5Xo#b7wH5WORxdzbO+kFGL!nKz&@56!{pFoMbFjYD@IPaQ5W zzcJR(*3InH-$bbQSv$6DE>lNQbSs^_Go1Eh4tx8|@5Eom!zweJ7RlT#f+`!az?NuAB2LDvv7mJwtH;9w6w#${&PH)ETRRI#G>L(TPT2RWJ{1 zMnmnh-l3uo3Ar@;fKN=Yb3x%qcvV9O6|0Hd!c(#5?WZu9O6jUdW<49%KWnfT?xH^Y z@>Y)9{TIz}QoZTC?~igE2HZ<79&8Uqf+yfn0nhBXjF5JharRPRYuynk)@VloH~p#06D^AGr&vREPhhy>W~76w>sBtCZZRV4_d0~uA8yN zy^FzFtc#8DB)^ME__QxJ+wlO-$G#IKzLjh=j}VyfaR1r9vOCJZA0yfN@iWMJ&JRI zvp=S};2RLrsC20!x%zc*3EVnnQ!0Z?tUFB6%QQ>el$FEsgWOcECS+$h+gkChOJZ$N z&PLE-Y_o1{Uk6DlO%z;V)(UR<4$NG?&u!@-=S%2US4j<*ZX1Qq(e$pkwH2W|rrBbS zqjP%A%3rt{wLw%SdMEFYpz5OEp!?Fs-+X23@_@t6=P{qc0p>;aQ5FX;9&f`+d`Mkp z-Zg1scK<}bS?YdP?}&aSXded34R@>yj$oUaH0D~=6w}x@mBP2o&nGt*y_h3dX^cjB28t>NPQ zJb0M9zVT`%aG+}-?D{cZVdhx$>2uFw5LeSBhbya3t!Wp+dYuTP&lJ`SSJm^iyIw3e zBMwH`W$lfTr01G%{7Q-IhY=|2PTykF*u@OYaZN1?MSRPG+)>B6fPd4Zc4Q&3tw$`A zl^*boU%aV-WUf=iSBqD%)y)?tfmZpb1+9|sUf*=z&zkV4V#t0wqUACqP^45rvyiF8 zQPmYHriaN| z(S+NQ7C?DC5mjW^*K~!v=oY}Bcu7C9G@;<>kc6l{BD}|g=u2k zGwzvh8c6GmHartc5)46)j+9H|?lGCtL;ZeneT6^DhouhKdDt!=*nW7HUqTo*A=Io` zixzHNEJ3tIDj86eN9G?HX}7j|&MfaYr7(PVNMpeu%rf3>bs&S!GVOvb!d)cB;x!33 zj~ik^6Yh`V_T5LzVg4~j^yeVJ-+|%&aMxCeB&|8!C?$$V20iLJ4r$_v+6t6@2oEWx z^`7%*n{cbiked&gGS>t6iXRdnJ*hT?_}R6m_xd&;MT9io`Gu6pho>Y7m9d(CpMWiM z{V<6wy!Y1;kH6!+{h@=MA(OERMB*0*OXQxXq+o$8%)g6&mLaG!F%3L$pk(RhTz!Ec ziSAcF4Y8`u8f5<}ar4pq;fmV8Ux+zOKBJY1u~Kk<{+4(k{jF(CXm9?{ z3c>0M1NNa53?Z2qNdVlT^Yc$mv3Vs9t|dNBe!gqfIDo`W6UVZkT)F@XxCiHLrIilNqEk|a{VWrm(N6G~qdghsxp44)Bw(!iC7`0uG3OKv#c@Vf%e9?ix`ZglYeQ zdqti=KP@oU(;cl^Y&>?ryOYssY-!I}h1~JC|+pGIs zUWt4TfQ$LpZFUC(U*Ph8RICE=^_Q5B*!Qet0KEZ*a^%zm?Ue^F5H(49&5P)hj7Zp} z_v>}!nm9yz_RRdtDu7o_(u z3Q4cWho_F0v;D+1uh$*I>*hucv_}&KOC&gZF5-)O;`#MaH1yFX<*T2a9lxH|UT=r_ z#hcnaAT_W_ck&`$pQ2JJiwBdV&s{Qh%ahjM6{6p&`|8PiPVR`QUTSub|N8ml?n?W4 zD&t#>U1ZehaxF^6$c>&*4NH@{k<+Y&@SQdNP9q6{XVgzed#Bo?2q_S$MU7#?L5v;Z?&tGulxup6-2mbTPs! z-cq@p$qsDl%kw>Uchx7cal$D5QjMXmU6*6k@I1S*u_{245j>6q++nv+lC^lij3(j= z-2cafU}74|D-Rb@YhpHN`}#gS-moeGo-GELfO{V4B94Ac=I3W<69+H5?9*mxvcaeM z*Qxyu`_>cB1;IKOW()`ES0-b;6o9v177u~cJ$M0=fiQ2o-@weS4M(#Be<$Cs&N8yU zLF2g^CrW}$&eTz=rtD{tz99{{z(aFN`Af)3bnm)WQM}F9siTys);QNfp3&jvCF@=* zF9imHVoP#lOZmE}zMIlO+n5H8jLsAss?{!3H52}8TeLfacMZ!D#eH!F+ThR~5M9VZ zi6o<7_7m5?M$Fh39iA0!dyl~fX*BTNni%KdKGp%R|8>+ocuO;GEC%>~wc{TXZ4Vr8 zl^;7~i-K7LwV%aX((v8~%@hFhC{A*77Z5wogKS)ps}5FbNF@VmSLf5!B^nQ|cLi!v zP3NmQj`-1yo*yrNuj*u4YBUG~c<%(Bq(sIN&~bOe|25w;Pe1LB3j)+6p=btm>9D_% z`js12#lqjl00jUXl28+omO@Bc6{!4t9BM>iXZSm_EpfPN9GsnOte6IMyq)c>Z8LsmH=n<)(bP^9f*-5-BHLyr5R7>4CIp2)b zIo@E(;2@ki>iPBbw=&tcmo9IW2H&3KNmv;5sHotluybh)64Hv1=SGl-@w_E-1fBW^ zs(=6QwMc}=hyf}9)FhxR26aiWzmfT+8&)O4BgO#b0UQ!gCWGt5*qUU1sfIR*@Zxd) z{VBo+dh#gwHPTELEW$h%@Cc2cCU=wcjxH^|f8G=oL5i+ULRk3U(Wkfa6ZxXT_cdVGn(nUT1sgKP!y7Mmri#_@xpmV8x?hP0UVVXE z+1(9qu71L8JbdCrCD4zl1$Aj9F%0K1CES7OVu@9Z zgbK`4x-K@xGi581=?f%CS5?-AltDHrdtH(O^kqL23uIqB-kGe;PDJFQvflw~Oo@-E zIja>a!J6wlB6nj3cDAKFe@vGaTbk6b*pM+6es{TZiszQ5jf%V*{OFr&_3T@Nxt~4i zVDH`L?@ZPI00mtfoNH(kbK0d2_$dyhI=D`by-Wrf!@m(s@jyeT`9L$QovDwrdZZ{V z{*ABKLrD>PB;F{SssL6037E4}xSBK1moYGo!I`roBb?47nr)k+%h)hF9XaKXQ=ovr zJg7^LO-t&RZ&(!rKN|xS1yyutW8NRna*XpYSS`r;n*3=$E1F3hBi^~5ivjs07tim ztM5Pknr6R@72pzT#lEZrQ1P>lGdvA{`!5ID1_jmQ1_OEhB{T96?%85ewU%mDBqmNf;*r8rZj{7Fe5=EGO8((zE*%aK;W8zT5-@#IXT6DNKCsd z;lzpc`*hsO_m^Uw*kCsw@mr@OKf`CwFVmxla0NV}G7~>CDeFXSAjfdxFCcjAe&Kf;z{@f6^q!9d zJQQ@}{lRFpcAevN9cj2uPNw8ZzPdemDtR1@E9w%N@J*(jPsvK|6KRex-~}SVH8z&EUUFHa}5iJ`c=PB z0u+S>!DzdoETXKUvT03%r*_}(z~xNJeLydK)p!ZhF#CGb#am&c@-D5HWh#4u^!7@yKPWlaKfpbBc0b!~BsX>y zv;X@@Hn-$b*MFJ7b6LPrmc$ax|E3*HI-#UA26gwatDNl_W^AIkT*2N@4Dg?Z%Izhj zU1CXIqPEMqL>&PJ`Xu#mPb;1F5U*nL`2y_O%`mj#PE#B4MkorMWR>hoV-9+KPDE;X zdVd(-kxO4#O3gYv2NK0gS$?C~^TQP+s#ZoF85~x=M+*oCcp__ap<;iqY8`MKgFX^4s`QNRNF2pUOg0H8(%5{$-8vX4O$j`$-s^W8 z7YRE=QXPCkBDJJ*)!W$y+J3h;_F@`F6oR?sa$%0+(lt9nv-y9<#t{f>#v&&8y1Xgh zz?UE8yx3>_od^9o9E8sQ7eDMb+6)YLyO!L7PGUJg`K67m&KhieS8rr^=%y7*&2+CU zGt@m-D|X+!mgV`PDML*IX9n8qEky#vvNV@SfL*$;8c#SxU9uziYA=U29)=?u7wIU0uriE!bmI57@t%SCOj6tQZvmPuXvD> zg6riCi!7kWoTuc#!{+;)E77UnNYAwjvx4J74oG8QU~Lor-LU(E+BX>|-%y#>wf7#h z>!=W!AMT|)_ujVu=BQQvE#$7_H)ikbt?XHwE1Wrj&Bux$0V77`gtlir7{$bEJ(%`p zqcuRLNd%lRwj~{3yQ>`M6=LB`V{U7l*P6ivuQ_MvnI}W-_Y-Z?D}Xi8(VVrmF?d_3 zvicJEav%98>ROhJaU%+feC1nLqL=O*D_j1nXyxYRifezQErTWTQG3?^u&qep?;*%H z+_9AW$9tOLGE43f_XwFT8Ot*mJfh~92ul+K=#X=)y$D9BGHG_?FOPj`#4<7K`A4Mh z8CFeMMC-R0!y+w7>h1g=6n**s{dS7Q+klSR-q-sQom(TlNPoREaPD8pb8q*~b#!N6 zUdE`Sigvg#7M-`mACALq`l*$6&o9TyX|sBGYRo^Z6}y1-yT*^%0gr^f6%EgzeXJbR zAF@PTf1ItKxJ1pMLb%Czg7U)`BCNQc9-AyG)$@4xI>Z7sO8K?AfsL@0WNr|3e%C9 zjJn&hv{)2u(}jX9Z%IS*a(QBZS>4h9AmSU0=yojbKmc($UYt#ZS`bJZV{fSLqCcP* zuAdN%m%w1e=v9+Z`}s)-ckC@clfB!x@E+Gw7_I0vo#FQQTS(vQRleNv!7{lE^((lb^B2aIeBLjGq%2~o-Am5xY_zaf0ouysG9%u^+r$_!V9zPnma>VVqg9 z_hUQi1~Tj7o`{Y6I9_gJpWCmWw`=~Z?JBZ^k{nJTRQt?~l3k>u2?2!3L;h+Xtc#3O z;7YS6S!3`S1`leRhD=XTsCCq9D!*vGJhbjV=<)H7c09v3BA3gDza~+eUqk=HF#mU# zg516naD!r@H78?hj%Qpm_1ZYj{K^(x*Zh>_V`{=qUX|M$P9+B^pxW*TP)_3^{l6Js zVgN4pJPXKMpG?BcF>;Z)8rop~>99Swu>X5&LGpG4l6+sP&H`KBYAa|zL)Dn=&!9w@ z!A07*nEH7pZ%#;uZ*EaF*Lf9SJg#OQcyu={P(Y2`4XLdR0UlC+s~8ugaV2FEl|PFy zJs*BA#@Nftm8GRTh&6w5hW)$ChdQ}ke&5Ukj_RvrLx+lf@m+TUTYYNDQTLdM$)AR0 z`&&OdF7=w1C@LmI!g~L@7`EOK8>7*ob`txNiK)#;xp}^zqJ`~!_-!VcxK$Y}fOpW~ z)sk==pujTK_L$~0Dqf+noDFibKWpXxx?pm*|Mt(_OyC8YchJ0Qq#V9kc8a%~iHmec zy;K?tUNRY2G#oq>37*Mmc!jr#uY@KhAQDb1;f)SJQmm&9SA4wkjM+Sb(p>x5k>$FX zfk(A3cK2|{UF^>vVOgs*Mud-@_u~1IVPa6|d56x9u}rC>pMLF}@&9^$A)E*%^uD)l zd_Ac2eths2Tl0Q8y7T_rmf)iC4kA=1^z|dN4!dWwJ0*9B(R@PsaD;(+a;FW5k~`Gs zi{x~D`B>xLZP|D9_vV@)w5B0dH%xs$OKNGt(*-9Sd>2W&ZDc#ND+SrCxag5HwYa&G# zfC8oG8uYYCvcUoK$UL{#N-AMD#WZoJNzH;gIS<6$eJjnSOLf3;=bNNWrFQJbL7g== zgn{xX;UbMLVC~d#YW-`ju;YvK+OL-xuaAMLaz7v9QpbNjq@^;Hx$2ofA~IIqSK#SJ zp0p+Ogp*p4+3vwyacyd|U3>}_li-oqTtG3VbeA9BR)(<#1FUMQP%W&adZP*I$JlGr*0YCd#1;1$iydQPh0fkX0c^y z2)lTCeC})frX!wDc)VHgv%c|{QGa*y`MIAjg1h@D(kiXW4T|IGi*mE9fv3sOoRu$n+cx2|1>;|&xOCDVo znRZv$?3wmg0j%yTEFCLvRV&_;K+b*mDc@QC_H;_a=q zaEAC(B(pmCqD`IFA^^a~etCt1Ruj^^!vXiwaA6cwT$XFS!aU=3GB!%F>@hTIvg|Q7 ziaC2M`>i9cC!Vx|%$6kh|B&{U0d*|dqA&yq!QI^n1l_p1y9Rd;?ry=|-Q9ybB*=yY zch`-(L(p%Zb7$V2d*|Kn&YAb4Yxk<^-Ceb;SFP2xN=JHCdRreICD9{!Ac-=U>avey zg`Tt5@tT*@-vKa7`zXC|a;j8hTDSM*WO73}W|i`iwuPfU9q>_dH=#$D*YAyc z{6i5T0}eGJuT;-x*mP+ee8|!<&gU+OLADCI?@a^_oJhm4>5wXr=Ec);!`zRKU7rQR z<$St?(aRFKk_Or-PPQB0F@m=OX`_c-to{PCO1r56=?>qf`91g@G1f(KfCX$i?wR;Y zg#p&Z9t2>}AIu9!C`zB^ev@fas6c0avUkK}aK91yA}3dnjbMx|4!|k;;yNOplW;1a zl$2>T7|6x+;L#}QcILMv{>vTBa`_v7y8}IS(_vg*T7UYkI`C`lfkP5&`LAX+5)6q* zsT^{J=@jY}u9Sfc%5f^ktq-jo=JtbrnZV}FLa~*gwZKB{Hudj0y$G#Mdv^V(C=D@x z@~f6W0*-W-%(guGdE!Gnw5g)2^YD5&W&;lV%1_8oPSLl^hmcm4^NIZ0EU1y*2_W1MaT?sEO9gMcnv?FO@CeS*O2`ix+obb z9Y03UOY6#ROQ5$Vwj~X8Qxby-6G0=*C%Y|RG|)*YKnXdel0#IO5>NLXG2|2) z3f~YIc}fUXs@-IqS)C#7-8qIzW?@(Q`AWum`UCd}o6JCkppSlwG4W?Cr{%SttoJ*j zsAtU69p&kjoZz@@EJc&S;#VYF4gbt?-(&1Dgx+X+bm~Z3lJTO{zM7XsB^C- z10U_CGNdBBhg(1thj^Tq`oy+8v+&nMv+GT5 zQSY^>kPp@Fg^4p|59q58e^od7080Y(mq2q${TWQYise#hANdY0x&b1b@c zQk)K^DA!Bny)huQw^1&(zgtEe@rd@6Sq40hV(v6q!ETkbf!pGF#}B_v1+laUnW+*C zTFC63W4R`W$^ZaNHvOguWEy;%%J=4Xc9;%T0lYB(pab6NV_sASaKd!aZ;C=rQcu+s z$VZeH4trSJFFYS1N`s@_@AswC7pS$F6#DLP^jwVHrH$CXFGM@;Td8;ZhQ%T@)9|94X&d`u2HIckyr>ZE8Dn0=78OHY2CAqS^3oRZ@g+}`9qWYl5S zE^AndyGH~e?S9$nZZ<3)3tM?*$ZC=#f|Hej$zvThsm%K-HH%zO`^*iGV^Ez|{C#9g<567UMp9$y6&}l&A(}9KYN9A^_A@Bkv z=*VR_Lbnf?%_jH~r}5O*VT{VWViC&);C^x!x>=$%i-wmI(TSbLlUj$pD)A14 zEt3h%B3~A`g`+f!`jmI$kRS{BpesqlR3!KMd>O44G2v^S(}*GndA$OOH77G}IUQfu zed=|{wEw`lXCYzc_ab*_Maa-)AD4cVie$aemDzXlk!o|AAsO%Z!dC2y?YAVM=QR~$ zCxQSzM~KG($PbvX`AY2H?Cr48%^;F65Mx*a@Qg@R2m@psAy@|?4=`a#)d&Mr93fN& zAuX|BNi_%qv>hRwz%5|lGhu+SBSfM)6k}7MIo!KKc61P{58c|*w`N9n{(<0v=7_q% zp=QQbiIg?_GQsK{dou;URhWp-ut~<#6|tK%!41B}N1d0Kytjwe_0IcyZ13IP7iH=y zhRuE>m|k0M-(QFK;x7BY@)c_keeNrnO=cdGo#JV3BcLW}kcINNAbcwWLj~}@A16U} z3H&*n0}R-FMZ$o821u}P{_|hk5MbaXTLyKqEP?j+WAgYynUZ~vz~C{rFFuDEN?5Xx z`yI~H2ovw`4eM=_LSK*D{+5oU>D4ICfC7G;mXaI2f#lx~5zQIDh3hdmxB=S`<`RSf zR}K)r`XOmiV444;;`?YgloBzrkS73;zc9X`k7lnfCQhQQIKjZ;2Y(6hxz?lO?hY3U-@B51mRVbvjEeMw| zs7WGZA$Kl_e+C3U{lmZhyv5^EB56J^h4PUCZB89;!Me{{!~&?JQU3&s>2x%6a=sn3 zbyCNJ*uO!w8>v}0t8!KqK8DUw!9f_X>;Pfi4_S@`o6kY`{ZklJ_!w7!7PwFUt?9p9 z_uqkhO#;5{jYsZyA>K*jkpGlo&pXN_9#aHZo8^q&khLUtq2+Sm78bA_D(nTjt%5L+ty!Od@ zY`+)@%rs*fjwP1li%wdU-44j()4M2;u%)=JLfAbW^)$w8&^u|4=4Q5K6ywKX*Ewzp#1e2s5CojH9!WmdJ48!Zm=mUlaX; z`!G)0UcM>F{)_|YXo0Z$-4!XkMV_+c5IKJ}tYw}~X@%md{_#z8^;DS(R4iN0u zq`>7Bq34~m7PnrmsRxSsC|3y%g7a-|VH%9yg{k_4T|qtPtwXntU)K1kgY=xwtNi<> zCGCjLzlSCH_#WSVGMHmCwdWe+WM2#8?6>Nvweroi@6n(~^_+i*Sgy(Jn5Ozcbg3wsntq0v-tF@;Ry?T z#hU%Nf`-FVU1Y)|DPB=LfT7CccjfC;U! zIimPzbS`l8z@@pJ1=A5?Z3^PnF}{V z6Jf*c_vp`IabEFtkFNfB&fI3c^}F(^r5bAncU`yKpx{(q#!ji4-Pn;>C7X7TlY3qB zs$;wAg7z7gWjp#BE!jB#zu)^v`?(b6wZ}_Zto6vjc zr7+RRgaO{T-*fv)wF5vGvWBLc5W102xHyQ0ibksb)*i(E*tqzJh7->k5TUT^Mvssp zE*-%q*5=XSw_-k<<3?4CT>N5evAw(OGqiywy)UV?E=glBg|Fr3wM7=b=WIQ{ll@|~ zNa6icy^>uR)z;E3grA_CkPWL*vX1h zy$4O4AfYfE=ulYU?|9IGkRp6&T#=-k96{f4$v(6tlWrOY9fZM!7^Z&+$D=mx{yic! z2xEbaQN7oeSR5HDz#qb>xP<@sbI=teq=^k}4hRzB#DSLhAp9K$I@IQOZL&HDYN6nRPM*@aEXaK^{uaq9Qb4OUn+3XhbM68JL z+8RzQTunFIPGn)yy32?ub=OYFZb`S=PGJ5hS7(le`yAUj7lOZnoZ@O=;lLco?O+~% z(3sasNNN1&*8IAIQOI{YNc3}Rknl_WD`xr6*2&Rmw67T~KFk0hvN`bkk5OE9x<(Y* zbUY9YpePssc;o??q#K%hKmbev49y>404A1T2mxTC1cp!nCR|_$8(=~ThIGvCyc^nx z9!ZV}UgBb=QGevYtLfW}SA}k4QyZ|moJFO9GjPaCy}-Td4aHt;=sfjfgNT}tbxs}; z4;XMbM6Q~&7eCAv?=b_|!ZHfD!SZZmZix&Kb*5`vAT$sC=lXQK%>L2Ie`q>M5OV&9 zN@u~-VmC{LZvrJ9#t*`5omySF%R0Bb zbS+VLp_LEgQq6rkcidu;KMNN7pt&)jca%^c2tK@XvV>y_!@A_=W{YvVJKi{J$Vn5O z0sd-&YvKo`tEn**UR_Ac~tYa`6g#X)?D`mLd5$T zf%@Y$?;vw@U?QzM^F3pg)MOVy#|7o^framjb9qX=M3gn}Fr?!rwI^bB)f0o9_iuBOKUOKvB9+aO<;olz~|& znt7C@aX|rm@b9iZqvfIYA}rCGM{PHxGr(_aqsL250c&VRli{zx9f!V|LI-*l#1$eP z6g`woN(u|1!a%-YK<_A^Dsc)6jqbVOmS6Ay)`+)=?dYg`yI5Zm1X&%}#w-*xQTwoiz=!|!s`Z?h&-4I4Cvx<{lRBsshk(6%tOlJan$z#(|Gv+=;U!!SwkmnZ>fpd&- z9t%%v#_s-Ip)uu+2Aojq0wdld0i4v~nqGTQ>t33oMB+s5^yZka}7*$ShEQ-cSfas^O^MG8f zz8l1Y460G45b$Z;NxcO$yNS6J_&!7Fc;vv*?r`uF_tRob_@E5kkqZey9V4gYK%{{* zwOMD3)#q9*g?s1jvtLRvT?|%JO3tr-YwP+$3 zy#u51l;)w7EC5QdIUzZUcxz-nTAhy9(LahxWFETB4A=?HC}1B&ECA~hM1aXJ@b$Rn zp##hS%FqllmeEyN#AXW;7IX~yF&`RjeVf=GOhHF4(RObd9{n23v`Gyt(9313L6862 z!j$6M)q@Et%^q({0gsiy^q}>;Bb#Hs0y3CAExBSsk4L)=&3P}qDm9bjTmH9Ptt@ddC*_J zyugXoZxl3@Uot&Sh{y~v>w>1fqY@%GC$oSVg-VZIpPJ3i;!;nJ6NT=;BBRNc01a3$htu42T5{!M zz@dVq-?kKI^q(IwC{83c|pkAm>v)AK3rf_Yq?xX4EAjP>2ese(TLTlCK zY#>plvF>MXhd?Ygo5_7w^gc_G^sPEt8Jw;WULVf3D0~_zBGw?iFHei!?3K>T)0{>S z9)MJy>cw`Cx-Q?j-wu^o;;FK*o%N2w!&}V0&37F4%)=mtiUeAM0aE}|t;0AT5%#O$ z4RupONm8KIdgnBg3H{fff`3xrzo%=g^S-v%7lwckqxm1xHFEr4(lzE$#)m5vsH<05 zVQ8AMUDbK7}L~~y)t%zgSgb#mi zR+u^->9>shRN|PjrJ}|r4qG|7_6!PJQW%b*qn*P>rAsLkIp%RB6o>y+-ykke?~#b` zvp!U|-A#?*<9McYmy6Fr3}LKRh_(nhlTkRI#n*L&hTW!V2C->$o5?zOjjCMWPb#k1 zU2~nd@J)77jcU`!Nuh*6>8qyNuLD)+N9*1ua@E~m^HARM4NBYnThJ}U;HjK!p_Z)J z^U_Fo>w|v^@zlMMM{~MS`kI(S#nHZTNb7Tbd#3o*hK7yAY?3C|@=12ezLi&E(ra%g zc6BmaAy&{*z{aT|`C2Px;U?o~!Rup9_90IFSAux;ol%Q%W;zqp4<61vxRfb8&NGoc0DhRem!UpwT~$SVSz2fNXaHBwl9VUZZS!?15j}|$LG)Te za0*?5iI~4!<>iYeCr&UQpgqm2a=XBjGrux=&qL?P-uPZ>sbf-UbrJao0HcGFcU4uK z^z>U75gQDevdBk=7A`;?LowEQmfH}kUh#0)sp_Mlb#RzzRnw;6vSf^XzZ&j>Tu4jS z(eJx~xV{h7orsWToF6QC#lCnaab+qnQbl{CB|)O;f9wl8SJV06!h;90!E-c#^O$ET z1iVlF9h)a?Uw-Hk9dRn@h?~xQ7yAm!`q}G-t<6o-OBL$CMdaNC@mm$>#X)GXqb6iL zru4n?M1TTG{ecWod=$rK?d3&BGWXcX70&-j#F(9A+nt-qQ>x*Rl%pZ2G%^S9Esd$;@kx6xd`-?wEO zulLtaB;eo)4c5B9XUEgsb)DX57s8|0z0W;V%A0VBzhCFu?>c|Kr{8tG4aO|A#AmOS zB8de%74*O9^`5+-P=H8WjL#1~-A-j^_K#k>8IHTcEIn6B#sqG13BLV68OS==MUP?=rZb??LiW-*s?W zqw6p-rN(0c%SML<@Cu0@HsEol61F4UI|(FXd0lEjj=1bUBSZ(Kje(1@F(PzO>l!a5 zFiwYy5?JT`jS2-=r^5q2)#3i50Jqew{Mq6n6>V_86OY?#cMLAhmdIHA)}lm>pF6JG zYdqQf*10(cmtMwVa6}j7oKZo4#NDzI{MWw#ALjN~-`!@)0k$Q|7fhe8ob5~jthFn< z(Hnl_ww4n2{upb1zBLwPigVe>_n&eyqwT*8!~uJk!emXJix@rwT7Z%ugH!O=fU`9( zHE+DHuOsIh{+&--_vijkUiZJRW_;E^z6$4@t+h_LKf3?UDvHxnm}7-j5wvj~sk(@F z^^ig*P7DqE(kI%oMb>!{|A~(h1!XBL40DSt>>{3(uNWSsqF*%gBHqMf_#e*jTOBqHP=R~y8;<6?M^+HBlk7u^BqD*A@4fAE3CN(TF>4YrF?1= z^ShZE2OOm%mw+|Pe3_bx))-RBkO?QE^k4i&Pv$0lw-Mv?9loxX^d_Za5jGsPiJ z3<=xWB73@sC+FKiB~}RyQ`;f~;{}7*2@!=mB&=zRZ2ux&)??UERCP^9(Al_Kcp?Q- zdrikew$bjZ+0Zw#Wg-QfnTexCmXWP<$05Fsy2cLRYUx^Ac^oeFd2*vAKmp?gR(>4% zh9pP(T(CM3CT-GIZ^&K|D0PvU@3+G%kINvl*><+bk3iA<&+z_7Pf`thjZ}qA?l2=}+a$ zbhZf=%K|S4ng2Xpqt0DAzg}^EfH~{l<>Cu_%O|>VIhby#t1t)7sMj5a*Axk-Jp@g~ zB5Wl$DS>J#M`-`AckYA8nLG$rE&}*2)t0O?gXuwg?W)b(t*Ab{xxAmn)ZZ-{KkD;+ z*~ui{V0T1&HK(TAgSBE*0t%BwT7~4^t~LPePz{VdFbdOB4vsSITC3b zgCoM1#0N~#SUkl>1;|Uo!@0!ETw^0U#Kh}u{!0t2O6!ujOy3u+qgy%>=wBCLcO$~Yfx*a^^=lJfAVUdvPlPM$pGeT zMmzM2IzMp>%Dema%;Z~EE^H?A?9=zHnAV|TH0O4mZV9Xv4^^B z+>Qb-aYL(J7fc7}{=(DqG|i!6vQV4>nA_Lw)h4t%eXsb!>ig@HCFf|@3afGT?TR>p zdU4I24!}ltPR-(G(N!X+RQB5eMxm9~@EU>q+(5zE87;YG%`5Y9?vIqj^n5mIOAL&r z7`Wv@XfqaJPq|3}>hf?no&o3-7GZTcB_fR30cawVAO#j-Te(SY>H~C)fl~H4$%Lf# zlW?HOiKI~u=D9+XQ?VqTxf@ML&}?DsnzUY#1c;Qe=&ikWjIV1=~y++>YGRs?K98+Ki{`oCbtdex!}Sl{aprN&E*f=o*^y4EyO@)c=qj zYHCXYjHW2KU&cXK>G{5LN@y4^;c#>&L6zzGJk;e8a6CZh9OIzn^n7QzNh<1ad7FkZ z#}%|&HgNy=kehv$d(9 zV5BZ+}8lRIAE!|XL^>&|4UeS#usHHkHWp?`e^rr-GMIl7{jd2mDhF1 z;%YM(p@Ra!GC+(!_&~+2L2hi)rXqrvUTqF{PsN z(JzGaR2yKACTw$?*3L_D|8H_*E&Ebkn|#nC)_MvF$DakzxAatbUAm~Ny58xj`_J656!^4apd|BJ5@93{K$oxx zXQk(Z<(H9~7A!<2L3b>|;&PK@)aB7|mBv9w>G{;u2e=q6k#N=~LE|jKY3Zabbw6u= zoql4|HwGiLl8d&wYFqVv_92q$(B8$V@*4*}qoX(|xotlpxbmrAVwYcmX<rEQDHBA-q~xXNgE92)iA@i+H(@K$CEOe) z)-0%;0P)C?EBtjRVU~Fgv>If`A4}J$3bI7RVl6z}HMh?G{Smrsn6FMg!q7S!Jw}z{ zu`B2jHYAmNPNrBpN-aVvB~pzb!J1y>RIF02_?LKPx0HIg8f-!(W(8eBC3XdV!V#@X zj##BeF;A2lPC{gt!(!d*hb;UIl95N>R-#_!p_u+@se<+o9kYBS|KNT(3b88iV z2c2pvHKWfpV^^Rj&|y_@kk4rpFGr|lNTo!nc_&n&Rv?qBe^gN?S7%URC!bR;=7~_N zk#fbZU?j)lPAW!A03fku26ga^m(d-U)r2N2O)IeMQ`hvl(;+0WS;4e5muf!j7mPW&wRtu6YQG`}Y4vR9(@!bHNcE*CJ(H`{`-juo1%A znUZ?g9c_pCr-$?p*ZJLXuwlbI)FKfV+PDjBuIL9fi#CGI@ zqxEeiikKM*+UHabCCn;e(D7@goxk|wwxqg0qqfA9rV%twcY%Y0UlB_BY>Wu^9f=~W zOBfFviKO)+%4^k%m&4U`60B)d%*fRlRZhh#MT&XC)k>t)Bh;Fu=EBt^6OJfVn#t$1 ziaWy9c6btD^K>oZ4{Q6uJiG{g+H}?Qu3)MW7vdzuM*;9E5I<)hpG`KjW!x)GOn4V=}~?A9;$tU)i3G^8B19 zd-J6>%sNHyLB zI*baZ1ZyT0Eb=+wVmk3knc_^`_HW&=YvxP%uKyQ}bDcG0V^k^UOf(=$uEJn2jN03ks5?PI{t9Io*+ZCQR0}=1*j*lJf~ts?>;8{ySPOqs^?pRAPLf zeGK%QOr0Z_#3{J~yBvJuoNny9@?DvyoCsAaNlC+dcFPeH1a;_zzT{yLkB@M&3u-<#}4p2i>~0^cXDc`>(ap({X?Z{?*1VM z3?0V*4&6|kh{kp1!Sc4@v)U{O_Xlz&9K$X`y9dSX^6Xhy|3N}sEVUZ3e@mHxmA2Rp&!-vPFy{<*FjmzNX7$2c zkr$?}@?UD5$AXDt`l3zy<0Qj|iNUX^x)41KuHKjDea>q&WY?TCS#@kZ;f_$YK(-JU zs6*^&qGG&a?qY8#1SypG)Od|}aFW}2VlsZ=Wf8B?n|B|Z9d3qIMtb(}I|ahmF7x)H z%IsuXpB8_tthIhwe|=a=Q5kzXd}X`K_ShJk@u_Q^7kItaruTi|lXbSQkyWf2IP}J> zoIfL7sQg*2O%mN2({7#KL@B|!GKjhR+Nd=vINlD_TI4jJvyC;7)29}261E@+@iA#h zJI)&W!`cr*_#(5zRz+1zFSQU)9bX!6N4h4`5zH3M7UqIt1$hWLjXn)OjX(WB#4opc{6FYd-WT_cmm<6ygFSQYpEE9+mGgs4Ijf3{AuWE%xR=z z9Vz#C^Z32^GcpEJej#lUZDDQEWg)MxH_%oHhj`Nn#ilq11;uIj~5JSuap!;^cJBfFe%a1(5T?l##Qdjpjb!*&ATLsgy4CD;@%nV zNHM5xa(3xIJ^y%MpZEsR|0tq)ecQ2z+}7h}4gHcdlQ-4fh82Udz?sM)sKuU@euU3B zHp}&FS~7&+JD^<#M55v8X289<&po=~5L}#zQHbfatWq`cbSYCcc~1)PLjo0Tw+F^h zO?3yxTqQO$tSd){x>IO=;E zeWzGc=_|iWk6QM#0cSiY-&r*D&pN#hbc9FvJd#q2jZr~ec2Mi9t?R>1xOJC((HKjm za!(9LcX=sSvX#Bx#Bz{vydd&1D(H7d59`YS72|Z*`OgD!0rl0d_m?5rE-)tlvR0uA z1LwH#B+%9&bn|`wJ$A>@CbQUkv8XKT%CP^ zc8FcXAp2emk?C8s0Q=P!EQU6zR$S*;#XT;oW5z(UjzLS=7-8&w z1bUDsrAgr?XlefQN7QQ1GVY(pUz7QPpxt<*k6W)%EJpP^G6ztOYbwEmn#4VK7X$sO zC>sZ>=`4%?i`sRu=Jz+12Cv*zH4b#%63$3upDQk0wP*g7Ji5o)+k*43XENSBRyQ`B zaQ^Zv1?t-0KQvkz8L48oA8f7jUcL}L$OW2Zmc@*?D9+4_Lh;#N@_j0@)HjW5e@IT1 zX6o}9#L2j-Y!0I#&h>hb*iXf=a<`u(ivB0QyL_IOL=p7#INq{A>Gj}hNB2|vnerL+ zK!y+S;BA4lwWbur&RLF{+w z;6(TwkO)G2H?cX|5TJ+^7G@F4N!CM5m_If;$2&J^>A{I#fTVsH+i$ka2S~Gu1u)JK zNsg3%z%SsARr2DHC?}anbWZ)eqZQUk{JJfps=45}A1m^&zp>lVyi9*aJ?IvEZzYKh zvKQQwva6^Kku;r0*2UVxPv~}`XlTu6bDf)YK_Pf0eW{5UVM8HEJ0vdMwSn=(-M5LM zX{ae2yjI%Lq6cxg+wp2nozA#r3qc(I%WuCO#e)PYfBbLWD`z*^Xp%6G7}4AH8Qr#; zYlR%F1eMeLp(9QAl#DZc=DMZc(Ww!5J1p}?vLq84Q$bODQDzIGM%m{=;kC2uImk`_ zvZ3OM=tcX}_el-Yb-XEizM6z2)@RRDmDAXg)tdM<~WWLKEZ}9%G#N1dY-R`v!F@Dm_5M z=H=hO(dFge!Qtm^9$;kq1kKZMJwfB-84BFfsa^ko>h%tbah$tZDik8<4^-uFi|Xjk zaLh0lul(}m&qF)IPP~3;F)EGh1-)*h{=;W9ntX+Bry3vjYTuv%8Y$nP7{zb5aBc#^ zdl)+cKh|kzeS(4&l^&r}6qTN!V-y{3;1mRek1?`+gQ66LGlwv|PEm!|KKh^Y<-)D01cKD0Fbv{NtDnlhcglXaPQBeD9D?ak2w?Z+h!?D!re z?(rj5eC<8?P`gTeE$P2*kvv_F){?-_Ut@{<{ zHkJJG3q@Wc?5Sq+YK@BSDHWRM`M@uTILBhE>%5ai zp%r`h*K>o{CEwYdva=%kZGhaPlx^!tm|oK+d6(f(;m5eGud{G*!Y)WQ3Z(L6YeF3% zF0fYbt&k6ir{7IO6;l;s6|)y36-!Df#<#@hl64Eo5C;?UUZq2-3YbpvqDqEP!Iazl z*pcS_xQWeig{#p1_vhO<`!dEw$S>sfBEn^`{pviE@#dIdcC;F$koz1A9`!8mbKb`K z=q^vRCT<;z0}$T2bZDok6onwcR-l%lE!eb1&Jh)h0FRg3~aE z%qeLWuIZ8L&DJi+Aq0hj>SI4MJJP-nK zhyFsD$^egx*pYkPiVG3Bo$#EEa9)x0{`4HY|K;mT>oMYV7MHV7(yWkor9gWt4&A@S${c$g z@b5ibN9eS_{c7wO#4AxfgSw1lVlh&%iN=UljpoF)XIVGOHp({X8fyFg5a1Z$7~&Y? z7<`Ea!T>S=Euxullb*o4L#&2*mOmjH{W$B_L~tb;ZzHx>0}(82#BmX5JCjJxH$^sY z#6A8lEqARGP%*e~_(S3rx<9kso&{gRgV=#^z>h$J=vLfw=5gk6mUR$vAeRd9IuXV{I196tOc$6^Uavu=CiyTOv;!xwC|JN+spW!#URxb%TOqij^ zSMxvS>EKC9%yo~K1s=<{23{8M7%aQTOM8$VY}sPF1{hs_KjJ!U3v4vE42h=1+ zj&)FDR2i6HRN;VEk#yokm3|5XD0&8my++mj1p{$5cS=+`wV!L!iHmO138?&qJ9$E` zmCKfUA$~;#XQSbaFjloTMZr1tr)sT%$xH_LVTXi!DmYQ4*}(uhnrx^k_G<2b(KEm< zd$X9}jL3Bn?t_KpCf0*JI4&F}J;R{D3%ilap}V~Ux-@63)m|Ab`Ng)6OT&2FU&6S? zY}a5erB+<`9Nn{NL;U;l2$er8O6yY}_}GTaSt`m9J=mg0G4S@--l_<*=kqz<;zgO@ zfP*)lnpa+t&)dt*^DvubnIbhL1AvoA26ZoLan}ppJWt z(E|UStHrkI?j<_kMtaK+wG^vyHqXjin=dNYU2$yLR>R(ezDkZi=qAW<%PbYRCKBm0 zoN80T>iGFCFJ}8WiF6W#bG;wd`w>tCJv7bklP^i}4v0aoVPi3YZb~nH=pJUs(Sp1r#m^X=nLHm$! zUx1p-l@y%WH6oX(ZTWrhE>w5`Bt2*_P~ogA%VL6=Hf5+#f`Zsr~)>-D+&VL?-yy3#Wkcf#lFSr@5QX#yj2D(~snBh-b=R zE!T#uwzKc}=|*Jrcx45YrhIC@=Ps+(IKRKZ-L0dO;au!D-GrQrl z>^q)OO`n8{hri7gfc>q+J=Yhb{mv*G-X2?9uqaIYT~`;&{RAk2UJpO}bx;Jo9|rq> zl3YN^=??`N)`8j#>w+D-m7jmhZ<;MwOVImdSJsTpIA`n$#U|Um;^`t|W=(f{5P^3O z{T&Bp7SeHI^&vt0oS|PBy?>i9c|6SbyA$VndYo-}qcHJ3Z*G-@X%g#udw{n7adN_q z3TBqT_krixl25i#AifQ=#*G%k?A5^9xoE=hnPzs_ZPu|sLaViB02ko_!yWU9Cy5o| z(}Qs?`!>23g(UV?iirBmMGl(t0>M9e$na;Mhd0Li0Voslc6`-;@4-)ln8CHb+pK;5 zfq!=*jn~-@{bruPAw1=!U2$T-!byb+DOzk??-@NCddtwF5K@mZfqrn?h+!cql?*_M zvp+W=mb}%W5USN9fUIn&f2wEzM{PAqccr_;%Tn$W9({WLv|Fajx2dyjr9SO4b>a22 z2G#$AR|{|UrW;I2{ zO->I+I1Ftcn5Zk;;I6k2xDVlhwPf=Dq8TL+Oe}Mi0SQYTSXus!Qi*Vo(c~;v`BdTw zo^zpCCz>Ky_pk}<%0j$qT|~SBQ|C(-fR#^>5a0U?=T7;yt+Vcw7&Xs-q0LQHdubN{ zh6$)6XAYkm%|y2|PZeE%zpg221fOfr%%1BGdeVoj5&;F$`nIb5hHiF+0}QwA5LwY9Hz|U7}(={lk|`Mjxc%9mlQoBCWBcT@9L zZ~1HPfzR~*6(6_r?-Q{3d;@cd<5jI7n$wDQP65e;j7M0}cpWVK)W%k8X6g+14Jzb$ zD#RiN`5)bmC@ECTghegTKzWYT?+-?C7aP0($2DqpKvt^B0wkty>}@E12UQcz1Uuxj z!?q`>tkF6W1N6zx709)-$g$&&P3le=SC}cB{5*n}sB#%(VDREV&76Z|A)n}+d-8P` zoD7GZ=3ny&Qz3xUglY{3h)OyWVsT{#D<>7T+sqF0aRuuFP#9;Zqk$QF)_s<&PuY9% zqsMyh$k=eHq99UQn_<&V-TMNgWtpN@; z0M46Qzo$dYcUt8fC%!UCL#DWUpw782JG-qt$(yitZU@~AP#3i7>O085qnKm26LwZ4 zTO|7Fv=9czQ)9KQ63$=5rKFa7OOci%u)acp!GGGC;{Y4@KY8O`5&kwec>PNJ$xA-b zG-c(L^4CYsI4hNfzE{(ISY0L#FU4)2(@RGFTK#)mN&VJo?)O^to@>_vDC-yLWChM$ zvT^FUA?&eP7r0<-a8UETAEajHn5)=j(H=`3BJBufxA?y}#@{6V?whBI6Gp-I91XZs z8ok4fg(r;sL3@jA+S6DbT2xo2_E58Zc){5I@y z9(P!m0lwW2i6g+3Oik?!H_`K{h-}}Tg-(l(nqzlm^LcC`4gNL1h7i3QD;$Q?!W6xx zmMHX2+U6(@x2bu?ZQOIt}RygWz%CpsC^?3B|?vkBO+J08}Z#ZT=E)d!Sa`;#AzrB8vEZgmNMxH)U=4h-F zAjG%H>{pLtp?Uk6nYIO}+B2p9N3s83yl12(p#p3u_Y}Ma%q_*+ zwzsP_)|s`Q$vwdP017j8ojKj}-MiT0PW>+eo7*{nLLdnTV180i3$Uvpie%UEUv%^T z5^*TW4@+E)L@{=)W4u47oyR}WZ4QoDn)8b{lw_D|UUepZ;jJesSsuH1U)OqjFwk{R zmSc89{0dgVcH)~lVV}GXL(T83S^f%csAO?-{q8AbQz1|Sa+sI6IYdZryngXXfu@~t!{EE| z`N*-YqeN@|!ATP%!`Qcc8XIU<%hDRCYb*m3I={d95U1MBh?ds~TwqQ9Np0};-u$B# zM;%0>&Wbi}9c+TJBF06xC}v2QHGBeInmkvm3zdAJ+jviQFG`UASv)EW9!V*b>$b(B z_lsfAc9h)UU$hqmf=CRn&;*29ftHn>4% ze3dNjq+o)S1HlBT#AAE5HhQ>9tlKXA3-LUvW1+ZDc(8IGvIqyie`Rh#x^5P4Mw!3` z%6w35J;3jGkqqq#rV!oGmn6BIMUs?#g=fwl$A z_d?A4saV+oldxh@0S_-_B1CXxBC4Wzb|9C*?c=hUCh{ulqW58|pRP+?4Uj*QZf z#)9V>3+*;#13L9O&OGg#Aiu=`S*o^5GG6S+X`SC}6yG3Xu|`Z?HBc~C7?}5h*(W9J z!+;6hWk3QI8%-ej?;$qEBvG-^0IqpaHkxTsT?~d8^R$=mKtq>+wsZir^%moB)av7n zRV(c`MF?S9ab`w~&Ll2h`>TLu#%}omCPcFFuSr~BdY1sd!G8nc9RHDqJZtK!FmEHQ zBoT^5Ks%DMb?*0m9OD(HA8Y7Xdl-#0-T9Lt6ti9Th%c`u(UJY?SjzFI@U6!Fh0q5j zYbBKiah^pxqk#bflDg;xQ3C@GB+TSUL18kI=x9ta0|Rjo8Vf>6qodV8s5%Hu1fgWn z(a9jx41}hFQ1a;L1rX{ALNh@qMYN+4#bh)kd~^Q=ouX2qr9c~%+tFmcj`5xPnAs1; ziJKDxh5kk(M7_)`ZXP7egzqk#w9BmonX} z_5mnt9u@}i6CT>b>+?ZVYQ_!~Q}h%DTtv9+k)k5A#FNo?K@J8yM7T{L^cIA^fzYp@ z+9**GG?K|^$Y2Kpeo!q4WdWfGAXE@k8!alL3_{UCs4%EDT6At9TGH8aZ}9@0N}aP~ zU7d{se|uK<3Bd{bj7#0fB1!Cm(4JVqg-QO2*XXW(fDxlcC}Jm%@A{3MGjXNak|N|) z!WcKNSs1p!1eU~RRc0@JgiCZnPNVky$Iw@1SlZnxN$4tzj64#15abN-H5|HXHUk6O`GL)l2(HS=s~P>$dTrW1b=xKka~*PxFw50|e@I3m&W+ z+t+8kP6~Qo-FRtlb=+y`&O{^)D+bqCy%OfmyxQj-c=fHC-PznR>s!cVmrqk}YB|#5 z@tvv~j6HOL5f4y`<^kRzn4hSOYzmWXf%*YqA(-4$Mg*Y4rt|az5<@Uesf>OVC0!Kg z2LMAb`>j#vDV0Ka&F_a|PmTi9SF&s0gcsM5z`e7GCVZ|wCTb_8@ z@wPK|Hkhq>_eg++m(S*8JJZsBpPioXTyH)y)qXVL9b%7;VAW#8JL(SHTqu89AB9)4 zXp^3;3ZcGAG?=~3bl{wEoiH*r~eNXLp2z70omxfp1nz9&5y?Gy>ru+;xr>~wpsXR#jt<3XvPuR$H`rFmM>O)RH$s!4kJfK~MT)e?9{(!zv4G+I(3X+K<5pkyehh9`EznF%PvE?89^~2g3!ubese7iUy z{5=6Mu$U;HKU1U`$QBM}F~so^Qu%hNK={`KVB9cKkeDbmjbtfCvlvqO2-$qQ>>>P5 z0mZ80osX3(?qH3#?GF%@7A)=>T*3?KJ&F~&M&(|kAB zAO-B;19or#qwE+N_6&l1x=Emlbl(jQPz&$@Cpdr=c8qp=2ERSsUT3txOR9zkQ!VrQ z8`E^qnAWyNi*uUqP;s(qUfjWm_$iikUrTpHS!#()AqN!TPQy8j7UHfiBuBfM24<*D z+lRX9$~;N8-V6)rg3gF9|G6Hd$M8vTB(!9Y;xKNq%?yc5txLyDgkC zg{&!yFJ_Oas19pX2u=J`4o zuMlP)U^8X{bEW!{k>YNBZjI#s4?55FU(ME89%=J7hBsU})Lh(Xz35eMX(1TK9!l@b zyp*XncJ$e?wsx}Kb+pDv%+^;G3^&$?|GJ(!WVdZvFQ4CKFJxZgfE+a1(h)UJ(6-Tu z3ls~)cha;;#0}%5Z#>%VpIX~_jL*GX;LsqM^Qvn_m`$;Ku`sN;FkVXS#b!4gpBm%6 zZPqfgJ#+Q+x4Oyivy>V;%Wk+eTLQA4c7BX7<(N*26$poBn@+M62rpuuP7)Ca7h{@E zA`}QeV31RJJeD+e{=e?QF0y2>e zInZ{sgi6xlYE=N-Bw>}SNPHvCqFLA8B-8AR>u1O}x&1_L>pAuEy!Ao^a2v;KF9iE6EhZ}guyH~vqDJHM#*mXrQTG*`*3+Bw30hz8v zBKu!C!ry2B5nR3X%V_68FstOy{b1B=U0qIHG4GA0^764WjQ1PXw1`cr+8&5`s~XVf z@V{g-^QaX*l#)r@{v1(gla=lq&C&e%(HYg{Dw(x(h#jMgtW&OZ;1_d-w%0>gw~um<0~H@ z;RWIzjayo90hhxls;ay~wGJ}AaZqfU? z#Ave~;Y%VtCj9jo?!zK{bhC3BS?qJXsA8KSYZ;zDW> zKFZG$#vo%N?qDAPp4=Cu`r-1%QxYbiXW1waQg@Vm<=HjKV6KN5#czawRq zbp7byn%H;xkv!VVyZ0wX*^Gzzu-D@8j7{tA{4@C9=5ndWQpAyP%t^&P%5EnZU^-7i zOex?FLa}*`^st<@do=)bxGaJN{`kmRCJCk(BXJD%KvFrR8?j312#HYnfP}$$KdrURwW7nMW+0IcUZ$X-#_~Yf}?PWUkTX+2H&CN}J$IBmIl4v>0z^hAEndCjv zw%Tp$@)IB!?yR?H_eukBXPV>4VZtu8E=?m&V@$$ec!i8MQvQgZe#i9+NzJ-q0yS?| zM|(NlOIz-ZyEWg^YU@;2Zwih!;A)xVNZZD3B@*Si{b0o&w8{7FAHE$_n_HxHi_V6qH@y~)<=Wo1{Rufi<_^F;IOk2~^9Tfa}Ym zG(#qT_vvHQ>d#6tI+1In0Ol%A-fMa_wWsFeRfGdv7{#2Y)+g`TrjQS1-Q^^8-zU*) z$2@Dsli;2vgbk3DdpzqIG21vrZq2{jdp#Z8S$DL$-#sDC~|>hH{|TLMGIy9#3e`5ZP|jMX{hw)}sZt z1-Gb^+&&+hN6!;`77FQ~N$WR#5*ZF>n<1{^w5?xm58b)dqRmb9cWBU@%0Ux@vL{nr z(DW8lR|{5Oc5|+K4uaXR*^aZBmgujRhFPN5VeMYJYvOoyBMd&?z=^5bpo6-CmtRes*DGX--y zXyuNV-i~h+-|^Oeo9X5GwDtD9w7=$x&)fLcUiY%&#N)c6T%pK*mzdj)->I68fY=(4 z(0?V<3ve$kDe#t&Dr3q#-1Eo;8VHhlcN|yk8AuAFzS^xn8=7t4A+b{8dNqmwhjf&c z;_P!?tJw`_(>_~Et9>b;Ig<4Y)tb?-BDc0Av^0xO(pjA)vb9#LAU&m{MR%->icu#i z@9K9gm0g!cn_S>;o+hgD?yf@%hO4wrNI0z%^|+PfI(%uhuIR+id^UJg+Tp2TCS{GX zhORi8QMeyp*4uTR-Ha8EQ8<{8SFSfAF@ozB^GvptW=&TE&91hYjaOs!Ul|f!E))Gv z_8fF^-!P8k&y;nm+GmUXo`N-K(R4_^Xzg@gGHriQ-tk$r^Y@BpVl#tvBlxZ+ z5Hn?jM_~=^n&o*!`+VZuJq3&@;2jG6;3bHzbSvV!S4w#yj`T734b^faS|!(PgwqhDG~Nj`<>Ta!;V zJJOSqsv4?J)9$Yj+S3OQ%Cn>u+LMtU?q6Fbq(444#=oc`3JFVD6%~OtImpkUA&L%r zU`Dt?liT8dT`05}8zR}fEGZPEK_KRiY$j-#Hv#5Nve|@Gl_n{kkV#$F_zXuLBs`V( zJ<^4mhw_?;BkIUU`X~9x_nWTur6Y~Go8lz<5cn7q&qhx)>0EL&h^rEcGo*S*RBB%< zw<>R99K+oMSFIwK_CpaCCF#vT^k+JZm_Gs*Hj#5-d;$AP__$yGXMmeJv zwN2sml@kd!CS3fTJ?eAcY@LksEsv_^lgy0uPozqHgX5D_MUamS^wU8%WjFQpwWLZ@ zgB@9`@NQJQnv%6Ev(cxe1@3k=%n#QH#yK0UO7JE7?{C+x)g&lCGdPmyXs^~#8Z*p@ z`$`^;*9j-H+cUbWMOsj4NpdRsXevUA@!I#9?*ib7*cWEM&9wc%hKf$*OM1N4ol?WU~`J95dgl2s;;gnv+yj zkdKV?SENb<4-QC$N>aX}ij$Jbwlg2=_o!zUFoV3_U~(BwjF#+oDLmLTF7d6^Nh+pj zb{PJX^dm1 zL_IXw-M!Rf`_xkx)Klkj8A)}fbLt*Kw53wc<*qS;GD#a_mlrC`>-)&aQ(bG}VeOY^ za~Z=|W^&sg<)lofr5>^u=1Tnsd$Y;HUmnGzu%0j@Sr!Mse;a36%p(gAx2#|`J@#*$ z$@TBJUAfieJ)F=Mns<}RaL~J6+&n3U_pA;4W`=jBQ`Z>LOv=d-LE?j~Fhh_yYv~b3 z{KzwTes&qkd~JsliY~d*nY&V=jbU-;dNTXf4_dyzc=KboscKHFeD}5cr_jzQAz~fB zt#@ro@l410V+@w`;pkG|_LeG6%L=apra^&CPS-rn(m~|cU$d*Nm&FI4J$U^^O6M{h zf|*^q$Y-}NzJ{mF&7o;mX@fQm%x*(>sKyqs_od80jjP6zYkrf;I1gu8E!rJl_@uzO zG;UY4lyM%(yo3y2O%~}kkYWwV3$A>3lE^bjreH-G9rUI_o~$ypEJmd5L9R9}pYl;Y z`AMhhV8+F3_59Z;GiT#czCzOrQR#&OTDbcsc$RBrC9CqpW_B-{u5>ILtFgB0c&i9k zG$;ELGgg&Y^5wL$HkTbGW2@5znT_4UI`S1wsSF!?P|rtawxL;7BL8+HCACB?{6A`p zx6;jc$|pw`JQE~VZv9eA7XJ9z^5{awrN~NyPIFU=9r2!z;PYB@Q)|ku75v3vOA|05 zCUtoDGN|uYLEc{b@B{aG^8R&FU23!AN`&SH{o1_!B_>mhdAf^~wc+^E4r6FLnG+2? zTJ5~2#EQ1M97l`2@Yo6a$z_WzmsM3Y3(&ZRtX`j`F3JdPa+zO!ny86D>pOjd@aoWj z2DS?}v9Lr)EP`mf|`|1u7Sss5;kO|;L z*L9%H8-HZHhm&_d{n(hpdsr)n9!pMceXB>zXuRao*7P)TgzU!l)X9eDH2Kwc?Lo}Y za$#%ZPV~VF&yo98vR8~rLc$)_R4x?d%GQxi;!?Tqc2;<`Z(s?_g_GFYdK)*awP%16 z%Y~fy%wk(BthFTCiRtoGc(r?gxT)mYHdjOrKe6Ki&I4yq@Ed-hb39KuLA;T1obC?u z6-l!}P8H$O|R7Hi;=kaW>h7!3!%xru5Q02KRk#a$a7O ze=SHsV%0Y#EThS-^RKOnsz;mrhq9+KO0+$VK15_S7UyDzi$0Ss((NYLxCug(D1ml3 z-hL{dNI4!Sq#R~A+MDxYSqN-sgs#YvMihIHLz z&M2HaR=OXZh*Jp}WB{W3zg4)(H`ivT#@<1&c7(pb&s z=qYW4OH!L6RJfhun%&(avb5M;np`SxBNn3t)Be;RVtx-q)CZhQ1>ezVn%rdnFAY&> zb)w6As-hr){?GR(Q^1bIJ;J&qdi)|+m?u#5)GK-cqik9%^U8qR13q48^JzJ{lB_0W zB(4AMA~HiT+*z3abJvAOdwxjet#pb7ag7)`k4rsVrfA~cNT%X!JUc!|_n4XjvbmA| zs8p#CH(Kf=oq5$g=wPtmI+iMs*qiNH{7*tJS=AW%=BG0`{u8{n%y+Nq!hDK71EsLJ4&eUXV*|rrTDBX5;~{(B4#SrQ zOYT!d`Bf_wLM++vfQ39ltl_XI?@gjmlmvpnO30fte%xgSZbq{5Ey0xpYH4yXGiwzZ zNE_&$Zo;1IR#iPx=iauolq@X^u4AgYC7bQ+5tl5@33mKkHAXhu+tZw)%8GnsrOz!% zgH$7qAYg}l!Emk3a?NmkupEGw|LaFgU^W+qci`YK5oQXXdA@yN8IeA){AJMx^Ds%zl~;;&@*K zlSeb4`g4X1Zp1{7F5FNvayi=Koa>lPV1jc4V%yBAZ?5aZOSs zF=|Q>ib#(Uy_mCO1J3F?r|O1t!=9|z@Hu3gG}$@TSmsKi1^A2e$oMxKRukRuhZ|NC zVEVR8CS+PWw&$?<9rBN;nYBgJcM+NxC>`ieO2Q^f9qLz0!dhl3DK}$qn9?jO?dqFs z$TefYnkz3@Pyg9pXmo|W=G@S#%Go;Cfu(=4tMPPwXA$Q?`W5+7T7bM!(|vsyWm9c| zX2eE}A^RvxeE%mpK4q|#>|cD-)dHV{h#O$z9xzDT!{D1kBwk;ZDs;5_(!Z4PDJvYx zR@XvUW+o|d#1%JxjxG>|K9Hp;#nei_I;sjoN45N8@+8<$IF?Irpun1Il>|;^t1d34 zEdKAaM}xw*>r-;guNI%KQ6hV^`+%L2hiG<*PAc}eRvMkb;XOv}QT+{r(aTSbiabPG zIA^oQBci#X#Kfd&fIXW1Tz1&%5c$m5T^dndE!B$?vb7QuX&jL;7D*y}>(7@)H_m#; z*C08rDk=eMs&u7Y85HaY()%jyQ|8*-AI%kNQt+-SmYQ2Do6@VA+RB;)q|_71h02Cu zD^5*v(k*IhPBJuKnGwcjZs%7I>>@Ud=gP7ULPwMvrcEa~WRit`d)CX+UP?9&cvFA+(Kp~(I(%;jn@WFg{ed7{%G zm7SlH+fX1a0L;F@lIt8>;d5Zh)y0F~m(xzr_Aj)zG;A-xV z-w2ryYVHSsAO@l524WCylOJI~4C432G}p6!3-yBgz)*%(-NOj(uX5vtm)(5`J4bA-ahr zk$A=3w9S0WEogq|2D8ZY?Bj~~n6XTU3OYu>=U=3BuM&J}^_i{-^>2>x(Z6Y8zhC0CHF|*bmzhE*Ud3zf8fxWm(9k7h) z=sXl)5pekL1Az8HbOg#I~>3Fca8uB3ZX<)T(tcWT7d1z%MloX%aDXx5hucqHiNZJwImP;)Ci_xz4XZIoCC)5p5wDA%| zSdJ4FLNuuK&Xm3XHY@5E@BL3e@}LYt*T3cye*96040}D#UX?XfI4VpFY7Z^wV-GMZ zzK4iD6ddbgzu}Fope;-NmxGPL>Xk*HC>RMjw?XbltSF@vH@eS9eEZW*#6#(P4u511 zM$`iBL7adSY(QM$b3m8?6GRTeBXsrI#{{AWX%o8o?&AUxf;e8jBIHdW_{b$Nu+LLS z)AUK3>~%x1AMuf7;R}YkN0lB)SIRV>!5K5VcyvMzJ}T9ute57M3tu;lUFvsGU^RuG z4QzOz;WFPUqlybS3QkaNj}dSL?tcJE1eO1n9H`KOk%-)U*MJFF!3xASzH9giNWt{U ztiVZ1xX#fyGV)&DUwoAOxswD)vMD~wSVg~}Yc30NsS21op_#O3Ln=L5D8W}kg5Aa;-yVYBZ(9uP8!43V4v`r`zXAU$Fm|24z}ryxEeE6^@aZvFJkBxu#E}7hULI<`xH# zxadJV)zZ9BR<#uTxkeI&Q6H~;i8P5*nYo>~I>GChQ)gsZ%`9iHQ+kJub~iA;s(rZ> zx(8%66w9V?RWiJ|Rxf|a;6MGBSQI6kD@FPw;@ z1Q~!zPnO|EUGH7~b(I9(%^qTg5HXcm!%F!?rjGXOfVT6+djT1T18ZVF z62x?V2Z#xFL3Bhdeh2UgWdWWbI~~u5TLe~=iHLiH5qkOvogfL$QB-mE#BOEheeoA%lT02Hf5Td=8#`hr*wAvt zTR-d2xs$>2MC$_Z@XCl|SZv!=UtCrdkr(VJ`n&>WULmfL+uH?di;yW8#c3z${fDd8 zYTX`2wK{6Fx=Sp*Iq%Nn08)UF)A4}r_hD&((cSTg?)ND=(47;kF5K^PR==S! z-S2aFfL$@_K)_x>lrWx?nQ|+HR@KaHX`VrE^JaeNHOEKC;4di?&CVUqM;p>liJI>$ zyP(58w;smKLPh!vKxwk@qH&lvi^f|&X>VUDKWxi;!s!7qC7U{_-KLF4evl6P{mHm$ zBg&Y4!;LC;AdaDzZPQj#xce8Zo!8mi5RoEk3T_Lp&q!=$J8SITqwjnuhw)T6Z<&1c z-P(MQo zG-EDxhJo(T@zuR%e#q4Y%Og+ut{G&)w4aqdQK=-DX5M`Ng;aFo(H8COQ##ZDFIx6C zyQgxd^LTg6M13uXWky_$e2!*;#DZyIIAVh-{o7Nh9dMk13FSbcDc386qyS2eX;ri5pjc-B8do0#D4&%AO7JES5 zzwqNY|NIPxKAm+yyDYF#fQ`3Rq5PcB0(#}TLejm05g!~Bnyl-k>m9^fX3vc>f0Yw{ zGwWrgTyeQOCOOEOd4YP(4H+SfD@?^6&d7;kBRqhY0Z;bgwR0u5SD3597}&v@Iwth~Njfz?yF$tEB^_ zefKh_U;g@_+8MuOuh(P?mT%!yCW5e2&q>UF;L%Osr?f_=!+Yeq?Qdf+5364O8*5}U3!qEsRAOp0g>V{O?}WRznH9rw+wDy> zYUOk2*rGIjR{5SH%-r8V-dZDIuQQc=0Y&x0E8bwsd>U>nk zk@xsud138J5%qxD|M7GBHrf*b+()}>;TCp7@9jO~!F)s(#t*|sg(&Tm6VE( z>Ts~5x`S}@xH^>gDO<&*RTA>Y4Xv+V>Kq@yvN%U|D==}s_zHr8PY~pz>mzXYZXOeW z@{~>~rbcrJ1S`a7=G!B2HDcf0YT=&UYKUO%S&rxK7z?>FXO&FIvp}Y1{A`S!(cT`4 z`4vkNVHdjCO9}}=_f|Q06y6{t{T`cgW(93;6ym&qCgpB^CRIByLg7+afQ~(n*H_k* z&IIi9g)wQSd2&Oqb}@J>*nS>Xu|mGKYY*BFN7ujXXxzvgPTX!!f>k4)?KU zAKSg-1Ye`R=j%iU=cT&;W3tGdm-wuqJeRK{L|mbs5VA)3(hXVX0-CouxGz#7n+PT( zlqygkNhy^Sr7WnDcMV;^T#{F{*P5jTVJWw}F z;k?ZUAB9WqJjW*!<$1*RR$B+ZAS_LMYQN_g2}pQz@VnA^=I@rdih&C-yV)r*pUs31!((rM_FdG z6f+2>QQdM(nlHYjtcag`==cW^yBEH-Zrm`r58Kvr*8P1)*v1U*OV`oCJE-oZI?rHU zWj*m(A$t&h#AYvNJ=7Clq{zG{oEVnXkj8s~MYm$vt@9reF#NQ`2-(shhlh)Ptn7Ex zej#(y7uJ()@mbu7D#N=&zk^Y93HgDm)tV|YG7&;yR2L1alELQ>`D!Lr@x`~!sB!{O zpVIlC29X#On>hsf=z1~c@)zG0qw`;YG;zwvmRQQT!XazYkaHKEico}jXoHY>sE36! zXtEg5Hj#Gp(@)yQTJ zfS1x~Q>cDF2M!qlBLsBSDl`@l>JpUCMWDog^&j??xPo$givW+|hZAIZd7Lf0k?S*v z=~5o(GXNv;_t6T~;>%}OC<{-! zwrC~t@WFU!FG7$hNF(T?%kf*w>H4mMs3qV4HbE=sgt#Jb{chqTP$VdWz%^hW9HHUke)a4I#@7&7bA8GE6o8EYwOuZaL%oImsu$t3^mv;CL1+38BslT@ zo6#z7c%Kl=Ep93^1KAT~n%XWjsTi^GqC{DCP<3sQ#+GtJp666}7E^ScLpIc)uCBoB z0rU*5J(d*=t$pVZ-5P*1hSvV`_ubF|kr+IB_hB2b0TmeSdiN0blV~+*N z`!9loGPI~wo~YkALe}h&YEa%b8_(DogL5KR?$(y4jiania}U&(?Z(ePiv`pS-ff}S zqHihG0v@9+DGwD9YXuCmIplsi2&R|5pUUyXiwwGpAIKM1BH+5A?OCWnOejWD9NPdl zqGxRFI>)+m_uZfkaKc#qcK>7JW48z(lfk|39K2fvu*Trte-74tD&!YgtPqc5T}qW_ zmRbl?N2chnNHWg)*`%%ADbov=ory|6Tf)2@#R!L{L{?3Zr!>1ir(H6e5z2_Xd+Zdq3;NE)<*X;!0 zWjNjj<0+QrvBMenBl1N8b{5GR$nw`(P%4KRR^t+|LI&lkU9A9&1W*uhpdjLfK6c}f zEYlPDNMyS{KT*mNGvWhN;-rAfLmmyRsh_GGBcbaQ-^(y^!EUsx+CeYwo`0xGY&`j+ zBf8F^8@K^?7(BXco#&sr$pM*+?w#kT-K>B$#^;}v$oTgpoEL;RR(NpE<=KoA=ck{% zzOV2g)Is#qy@>Xtc@=amz=MNB9)g(W4I?DX9VNQ;!PafH$#!U*76D9XsXQ8#Oi?(@ zMzVjUVnP;ZtsbKG=8y`8_Ev^9Lu>Cje773_pCL{6K4=3uU>u`e_da9;JKzk%Q+J*c z)?G2Z8gW%o%})KBVfJigH9#Hzv`=XEz=QXOBedxs)>UpXt3LIo4M3cdMoSr9GDd{D^QLr$VOFL+ z|AqgaVQ{K6?lvaahAmy?e$~+X`8muK^*9~|9%pZm7+DYTcD-<&9GsULxAjYFp2`k+ zbMRx=Q4_1hS@z2Z>9%PMJ?rc=%so@}{){;baxUE+=vzq3hPf{$=x$P6TS-A^w~m57 z50lMgzH|c(yz4HG0k8h>MBzo@1B3qF%#+S>jPc#F1`4z~C?RUe$O$S0NNNmVbUJ!3 zG?6^pf3@p9<(>!F`kUfT;$UuJJbz({`n^}7x^1bNzd+uzr{toB)9gWA|8hGQaLc*_ zzk)q`CtKc6`Jv?-ky|9Ite=FfZ+BHiL3Whf-DfoDiIyFuKG`}Y7@JY)o(VjY`kuct5vE=Kyw1lO#l2+xA1Hm` zPS%W`Khn3Sp(PVw7|68k2eWS*w{va#eTh!l$k^E#<8VS4=&YmU`iYE*AZ81<^gyyZ zR)jnOLD7ZU&^HW7K}}-FU#BQGsn}@@!=rNU??aF3p0{oezD9Ac-01|b}s(cF16Z8LMSH zK@k|#Ijcq@pG-$V#y%^$vq6AhM1$XbSVQVF9u&UJ5&LO;69a8Vq4c%#zAxBE2Gu=( z-5y+r;$FT}1*Tp39HjK3x)-dwf%8&01J^~sc`5JJI_Y6NmCjRr)=;0b*H3B3b$p5; zm&K4Uzl>%cyNJIUctQZ3b3R7Go4`R>ahBYV)g39_3LS!+xuGhh(k(Y=?yi#Cp?52x zmPvWE0G?vj5s0(173HEwr*_X>oggdbtg4kkJaj33_N)(#sBh~ zso(~^8NX=0Z_vcr-=KY4k6+%7%;AEmiYaMr6et`ke@YpC(yIzkiG)D8P?MG)VuFd0 zs;iV;&R-I?_P4<HPvOy_#7*2?C9O0Q~eIwUy#V3+u(YdhSIw;l&Q1z9_O8Yc`t z?h!dcVpI=KbW{#7CXP<`m(f`#0isBBEY9~}c{@AbJ|?!}M!hUzBW_X=Y!bPG;Z$oy zM+)|M!l|>}<6+DxG3ke8tWeYEu=#-nY{N7mojsuI^9KbAY~=sqhfT>YV_8y}xABE% znzsXMr*GbZ0IM%-KxeMRzXNIaKl}LMbOyK8&;DxaoISeHuBI1z>Ka2&*fkG9?{y0|mmipsn2@YU2&)RYK zud}hc`Ha~Vs=ncYXD(@aE;2=<GoyCTj4B?yq^Mo8Jl|9Gn}-Od!XTC?ixLpgD9bPh@VAHvA(bHEK9j&) zREzgM8jv&Sl+g9Ze+n5shg%`G34mgTnN-ULp)zA{w~347-o6b&~J9H!L(Y;A9(59 z8lnIe#rYTcgGuE`am?I52jhjj$scHQ^1FFUd;(6nd65Ag%3~%29h6(tS0mP-RYFh* zbBgdfiUYS8*^Rp1jH@IYeg?WVoK+$D=&t z=2h-f|2mQ+ehD1bk_4uX0fS~N0{a`lYlK8;DHo|s#Yhkn%py(2usV$pLW}lQ z)ujw36wyWvCN$A#4JPE#$_ysd(JT!nl+jKMCbZFT4djSIBdYgFLdUB2i9*Y(_sK#} ztM>^*m!Hx^l;S;?vftW#Uxx4^S#BbKxXpy35AuTTpu)T%#tlC!AeI}*`l&z29dkLU zcaif1E!q)#uPtvad4d*}o`6mty*$BRc;EhJb?5o`5UBHN{CpCb+40hi*;?OxfG7XD z>M=l*YR4|HI4}c9Hnjh#oyAx;V{JT4R#B#sk>`(j4nEZiErT$DxjjbM{EMT08$y5P z%dvVWH>vrU@x5?o1CH^=7H32acFP`Hq|F{1=Vm;`3n$eJCv=+Qd8y`4p{5^ydN*s| z)*A;#ITfwEv5^)1eReSEabLS_rmYHi?m-K>nyvV&Fjw21=)*xybGh7BcgqV^xHt8t zcXYSR39Xx@0T(s;4Ix!(7KXhRXNH?k8ggabE0ZEm%;AP&Pe%bqY9Z3IYj?^v&gWa{ zUQ*VFOTlI324Y|NLdM2KDwYo8rO88R4T{@N63B+yUmku0`d|sR=J}{Ah=}03NJc*0 zgnSZ=Y|Hyr{r)jg1xclmGROW!V3~azv@T(~xos2TSVWcjK441{w*d$J5MA3f)CoRSiOS9w2tl3m|bVP}Bax z6+s;NF`G{Of(?&MZPq&I9R(G(O?H4x0r3}+JTG$ZP+RmCzGvpPAUrDkx$Rp`F{|Yt zVhdWD)lo#Vh8gio9jvcvR>9+|a>%G9O#YYWT#(Q!1A1i@$m96pF6!td0V8^?Wu~v5g4bA)KLT! zS0o;9qkd2|RyEkDy+RE%oAVGl34Xy1%-2%EYXBFlGwr|%EB|mYHGopxvrr2k%b&uQ zn!qN;yDc%T<~iKa&Uxe<$AMQ6L0+_FYBNxFOM~%oWSps+zLVhMDs<%hGlm0n$;dee zQ|lnWnyI^GkfzP{KWIh$;kpKEzx~ZQ=F`$!m%u# zpwgcjnURm}t$L5uJvmM=C&0NIYqT@oeQGm0K`Rkj4zDOXhx%eEwiOX;yg;|)en`(uX3LV<{J|3p z_DBN1PaGkNZ~cQuKk6y|d;mKu5`*M2fM@lcN9+4XXRzxFi2W_NUm^g0z2Dbbz|o#GiRMpSD5^=i$nlE3>rIxFeZK$)Y8!@9~jBPEzH*0gkO~+LW2x zp=8)ey2S*G#9}56^l?a;Pq3BznhWoZMox49MndZGgTxUE?rRL2!--T>Zo~kI<9?~a zvB22>>oKyf^CS@9`LOHezq=I&@m(o#6^=eZL`nDHim` z&XW&H|M(?=T{nS~s1y~ZRzBzx&bkDi{YV#;)0INfnM6fxy1Y-mOFH`EXX-ix#%A*& z@Pf9EPUMf{kHg?a8PL2VZ?3UEX7yvlATYI7WA4{70exlI*rsPoUOxLQ zDAC!Ex8MILfuZn`SBjlKd;6h%%^ULP^*QM%`>)IKg~jtI74Iu#RdQvQU{1oa=4 zlaOYJ!S5GPr34;|$|feLD^OGp#O(4BiOOeC?D8XnfxTemv02dNs4$K4qP?JA1J&Cm zXo}jBM52_>Fm@`{8HQ}K32Bw(ezL>e7_MLT0p@1~1IsDhamw{64)|%wE>$O)2Z=P? zjydsX556}QPM0<#KnM&hN;`-R3&H&~6x%hV-!)_$kXa3sKw%u{?b3AL)a9hIi{>Cj zBCSP*Nio(ka>4%~D~mc{#X z4hd=q(g&4QPU5PX3gzj`>5kvEaHL3$4!s%rU5ewJe(q?`xnS{6HrEZ?4i0@D9C|l6 zv_3FYIWXiiF!W_$h+tsotbeGpe<-?tNVk88zJKUV|IqioAt)MU9YD`@Mmz7kc8|e_ z0hBM9b+g_!&OOeE)b9QXwT1OmbfeE_j||jzTl23^H>amB$~yB@fLJ^lI&gbUWZsvj zo35>_S8eCvC7-9qDE#)Q#7-ffEIwN!ui!N1QBwo^+nn2tYHJj8DyQ?$b= z%tP~i=-r*M#}#(P6QdSoGDjvUMItRlB6H{Q%bAABtK=-IkR1hf6nJz4<1#O6BF>R-$6!J;JZ7YzwI3eaKl_WM;4gQhao5t8ER85kp%pmq| zcVS9lqmKt`qF3=UQiQRvbLji;0jeyBGXm_X(X9=~@ZBA3XjVUkMj+W z(+iK|504`Zj{^&j+X##63X97Qi}MYO(+i8^4~v6WD@9T*IWa(Odq1eI&XrZSKS=RY zY$+>3{A5LY<>W+%?)7-0EsQh4Zy=D|@%cExbD&_T0lUDsWN5$#qHfimx(AHx&*Cc+ ziK{vSfy9pWzbQ?&od>qx4wg8+ELrV+URu-X^_4evfrVjbs1vA|Q0%U+|Atx->ae_v zGFsfg6w@xoVrH4u3Q5 ztkH;4$u4bQ0OQ!dQ9-TyPM&!dHN%@@^6|p-ps*Q+C}q|?=d%9VF9!mDLv^LEgho$t z0otXTRrP&+S5jpJXkQ~nhF#JXU4W+RX2nFQ?+UC$L=UhEf2{=Zlj|AAdszFO7y4b1 zmN0wtaJE=24N3u4;S-=YvFa1-FxePy4*K1Z8?J3dagq+(ooO+=*&!wFdjh}eTI>iV zH3uC`P^9W#n=mnak&SyJA1SFnHJ^SZ-1THj<^Y#IN^i?Z+&zSVM8+hOr!Im4g9yzL z7EbVUgSNFVodZ)#96!9Sw`=fy5H55N{ZBvs6n4y<5Pz^VcFgw8=$4SGqY7AvB^=D1 z)z0W!=>85&$H_VY|50$95)yRvIwL`bcqhS%kpRL&+eDw2M(ce$j~_ixMbY_J+wO@! zedZpU>I9?OxVf)ohCMa8nQzMBPmHvZ2(>aDXklcmrtQEczhAG+daGUJKTYHbe;hem ziwc!Kv?gr`-5=9yP_I@Y<&)$X2vk6;L(SXLCV!!Xk1J2ChFf-E*@2xr`s2I7xWJJ>#y6!bxV+>|9JIr{s))>H&Z8tRw_7hITjSOk7`2?o zyvF$X<6GU|feDGFBa(%c5jDyUP7XK^O%yV64v6zNkUcU0`hS)-+(a4@d~sZQQoA=P zyP}G|OSeYmD__~WIPrd#Q~Tv= zFf(-%p-aufW&3#J8L?5xQ!pT;{xZlWsLZFi%>Bh5vld~7@ zl-u+X4S^s44Prd|XZcNXsbOmRV)c451BV{?f%rgz^d+AC zZ73=Xy##Imr~;P+$u*w+jr^u-9OoPPDVWa^xNj)z-sJUyMWg=Pi71%7F__q_J^wgs z11{}Y^|oaXdGWEsvVCN%p}9M~pd+WBSf0G317XZ=^|x7K7B!W(LWoiuR-MKY)s0SQ z^5j6R8;7UHa8IsX#}5!_QYo4I@p4^pa{SZk5rnaF{IPOfF>?Gda$V7K{Lyk295pw(ya$Vta{NZw4VRHN<>a(XjzG!6r@sy94WbM-2Y)kAbM`>g9Aqt-z zTJ*Y|G^HJEl&4#pTpT#*o)yZxic_2DSIiTH4h!!YR?L++u00eUz8p5*GQzMx1I*y* z!{PocbLNl$Ik5jnX@YyxQ}nMwf@H_oS#)8n3qqs$>ZOf!2Cva2M_t?*#~|E~&DvhF z`H^`$x{0jJF7{ic=hp9kwB%8G5L2HRot;|Jy<_6pF=WIi^M@hpfgyuU3$pvmc{UOC2&1L<_HrwKLH?>-BY&b zyQ!S^_ZX%hVa!Qxb-`4OeYIx66%A8jm5mlC?PCu`LpGKtq2jua-yP+hZ5%Xr1VZ!6 z9hxKy<0a)Rr{$9u_UJV)mbH7>?k;V-pAEv;Q>;UX`*!+)tuLLD0-eNB0=JXb4wN1R zlmZ7PTtU=ObMKC|->j_+fey1U@{34WWaAsxePTwLn`(^YPIjlOuMV&cSD%f@=xOdL z=%w-O*Ls+pmald!?O#4G&xL_s*0X~7X{3GbE0YdBx4 z-_W;k1H-Ty$#*@sRed#U!|#~2=k#M|(Hf?t3x6^fHqN33Sfw{9qo#fe8>{1{eoqbF z)(dup3iDkO_cNW{o3`s%uE_(o-FAe)Y+>3ust z9S?PgS;&vr9zX362=|}b-G%4fuK-cun;ofBh zT^ZP(QCDNTNolfWfkSllMXnph)}IP;+tcXCgi?E|VfZsD9t4rJ7ugO}_?5T@H z07?-|)p-ujj#)o%8UC}7Zy6?UZYODbgNQ$yoI%7g&_pr-0vTw0833Nl%f3o4*&AO6e3UIpI^2o3i7`Wp{c8n*;u zrXc`yNQNQ47ih^A6*CtL*n$ak!W)(?i8xA&DJ;0Ur*{>|Y|`glm%PwCzUq&-hOlW>=*@U^KGo8!q9218b+>&!`Nm*R6`;1!ISY5%tUSEq)C{NLnN} z`YPcre3FjeC6mfL6@K%vr^T#oQAxL|R`vk9ot(VO!mkU?B`f_#zi2?h^jls}Z%en7 zj(JGK-eY{LUsZJ~#r|#^#U;1%oBF%|7P|&XurPTe{^re;$p464WB&uYre(88j~RGM zcFZT#PBsn$`-w==RB6_HF<{QQg*6fa-7ft{L1)49ZQD48m>3ST--7tjz#b3p(9sEj z#9jo&m{9`Z+I4gfC8dLQ8PnCn1$`=!D31J2UOd{!_ZTxrw%0qo`XqB8TR0ZX+l2A& z{mGnts?&(?g<7hO(YPt{PkmWtiXJgDsuR82EUGf zGs1jJr;M6TS!|@INwB4SX><=OL!S-tv-$-{|AI+%(|-(|`K9ae)BR*8oJ{3eMy0Ed zT8m=ymK(s^V}-$Qu7S*ETl(nd(VhQp4JnxODQu*^FaX`V`R?KeG7(n^)>TGL8}A`kN8X2t_=S}Wfuo?23f5{PkLdzWlASpj zZ}96(w0;Q@t5sa^F`UO<3CuRvt89tq8m$#=5Or-^`=BW_Xm`b@#9ldC%dCCqnRx3k z!gsUZ8Zz_8_x$_wOfxvMpq-UUir`^X3EvvxMlUPEvhDA+9+_#<0PqD~tH^p*S234G zvGZK7nLAdgK)W$T)0SfIbs0bQ(5~jgcVC?Sed+~2mGg;j{@hmYZp2`~dN&zC1b!z2 zV&xacp)^mjHwueJU*-Fl;eBd<2k!LJ^S#n##+kFf7jg3woE5f*(@LdS&Ql`^sz0bxj&prI#%YfHX`C&Z> zx3(>HC5TkHWH+IkWH z5Gsenr?5W!*SqVb2EIMM=lkh-{I9R`ua76UbT%sur!RLCAk2Nw(_zrd{&9=WEAwk= ztJm$?O4lE_wb9?WwKb^?50kT->wzqXab_|vH=K4ali^qTe31YC_5Ad!*Yo))UtC{l z?S9+yiu|0>5CPqNOViCIwEGb>IK~NbmdbcpSemzX(@43dx?+W9;+@J#L(>Iy!^?$o zSp)U?Aztl+aMN`X+vT=Shv}3-M{%v2mWK{{vgpI7Ypr2Ntqt~xrB;@`z`dyHr`W7l z5cci$8h_;L^9w%T4h_M=8_H9{ik*?AMtcZ|TWDUdFv3Tr{6I5{348{7n%(?9(LVhn zb!yMB5R0lS8eYw?1e-~tF*iJVy<=Lqmd2-K&{)IdBXDRK=bRs^XowT>)?2>boFB^ zLtf>my<^5zd8#?muhN7!|74!!;XW4YW~OikVkNZ^Q;y8hLSNR~oJ&m!?(yh{Y%#La z*mK9p&+99^d?qvwi69C-iD zH_&+6yxv`~;y=@8?4rLi%iJCxKJ~OFeTDQq$|+BbHGL}GOH4YZ0N%tqc}UD;qBay- zE-KVZljF@fI7f(;k{#p3sD5QP6&n!wG7RLr$LN5 zz1RY{GF@Yk2%+qUlj=R#F){BuCnooVe$nH^*~Tvyd1$uLahYDa&BuPxq4LGMfh)|* z=gEx+qhUEio^C3uHYo|eX_dIykg8T!+2~emq}R2JdTyxrX*58I>(UtY>Bn(O*3}lD zeLn-oC|%9v?Q~B4(Av%G!{rkG>)x92E9>1|*OSXaT}Q(_euqmxeg)qgRbgHo{&*7m z>zhh>wP*FO;)z>K(mXVt#@1SQQe2~tR}#jGo67_qpB<`;&w~uMkR9Ns$w%KgzUVFR ze^phH6i0aIxrtZu7;fevF_#TlQeZu%SR(x}fzL{0_jMt-_gRk^^--OVm55=3!%Qp$ z5N|GKg7`Cs&?r^Zd$u+>4d6H*{V`QpJ5(`4l)50K75Sn#L3DyRk+$*!&Dzg|+r7u4 zbS0_x_bTcHGJN>hFV{}XSc;wz_hk5}v{?zQq&LGkQ9~AvX8D6D)9zk{>_`JrueG-9 zO?8L1t+7}M$xH5$?_exTImptmgP0hQ$QtO8r$@QR`9{lN@MN@TNEr5Q7ZsF`DGqPq zi+D(^Wq;HbS}rL#A5%P%emI#!iVzzu!(+))AV#&={-^MTF0-x68jm85HnMKDh4E90 zd&YqL=t~qIG~pxTyKvkgLoPOjmmK<_WvL1#Hz`)(5wIyMD#4r{>2%q?T>jxIYU zW5n#hw4t&Ur2?==2T#&UW;SAba?&Adx0~#*VTp!3>w1 zm6jr4(e{)HqzR2uX+mh7aupF8m2wp;BPGG3sIDBWMt|X;&@ofg8b6fLF6*-kG{&n8 zq)SKJy)UY^cjNtPVrBlMdb`42-QDlXcp1KnzdLupt!AZlzl1-%7jgSBiE~jxRO8Sz zCKCP76bH|0DZD?aHMMFXXtf_qGBe&Uvn2e8ka#E^gicH!$T#+E>h#qH9LSw)H}Bh6FR?FA&-y!Wy%y zZf-!9o|?Ky^q;0xk@b_4^_+_`qDySK3|Y>m{rWw%xuH(x{rZ{oa#iwZho$;jiltK5 z2o+CEhT6Gu0BsOJL6GnBrc+a>4FwSH zKGB(B%WZ)W`531fWyWN1Fm19%WVViku5ki;mYUYdqbY6JW~h;Z37_kgo|xBOq124l z97x%;&4y`FtY4;BS{I5|tXdUXCs$PuYW=}wOqKfsD21f#QL(Bh+F_nP38iw0euh$M zSZG4AstVfSTzH?sh*D`#Nnv#2Y?-Ln0xS-1dh!q81_^~tVw9S6gH}g6zv((d{dfBMnlcVSyU8t)^ zAC;8$zCSbxj#yn3jS=aulm^6z9$o}edI(oA>c`1$wdTh*$Y0D!Xyaqwwe!3 zk+qNsih-9dpwPe%qX>OPYW3HywU;wtwiVEiPK~gyvjHv+PL$=*f4XQ8;)0Nu1!-GV4(`SR35E>O0X^9c7? zX1$`JGq);~EnL@{5Zj=9K}15Qe8DQQsywtr;r0mN-X%juG~-6q66IpaW4KM-H!Y5I zo&%+nQZMj`od+M-S9$BIg zYgtmXEV9=-Pv~8A+=TBdM=-})PL1Vj&U7pZ4dgV??80QxMbyp3s1VJ{2!*lbO9`#8 zV^+C%SoeQWx^a=%RBYqLx8}yW$X}q6&?#MvORNeHjl{PCV_g(3P9#drOV$cAX((P>LD)#dxH`4d$ok4D#y_%N zsz%t>}9P$;&SQn{V78WgnupYjh8 zCjgyn^hg*_%8tBgR&cI=%$*>fXP!jlPpU4G$z#TwL6l*p`i3S!SEktzdp{5{8NTn= z-zhg3z;F|;$&tUb#nb7%1=q;s)QK<5BDb{Hi6{HE-@b(KA(Jjg<82GtBKS!}&4)at zV`&U+I?wz%zy~w2DnRXIbUpS|Im*zjwg<`Sw{5(H*0|Uta<}4G7v&3D3GISyt%TOJ z0=rd~ydZ#3weq27H>G|$GKsG#TRl;N#j)qR3C^r+zIW=2WSe(c)z#l#huGdXoblD( zE;RPfYZ7Qc-i==FyZZ)>K9827L{d;&P@+~sv%?v_U3FbUSK~4)X z3`C0Q=yL2SX~AX-F=XklME8vCCFU`9jK`dp3vir@Mn$BUjULCI5*8e0&I1A*r=w4>rvwFw?n|6h^o$q;$qsOj zm(gMto2VqjeZk3_y`OsHltQw;L4N3uYXfkyP|Sz*HjZh@=$-6dUWOUhzlDB`k;@qQ zxkEv2xv0&=OszGJ*^<7<37=$e>r#_Xf@qdcSjLhkCK$?+7Xhf9jRxt5qq4^=dHw*$ znP{0mRYNtwW>c{gfa7emb!xo}NCUz~9a1S9fgPEgDJkSwD&|d#WfQ1oTNvJXH}phj zOTwkJmoMX9rA-?&3{L1_Ost&7Agwnpcym|$k)JqobOO68n#V%%z1`{vli~-~`$jTZ zxk;~vCL^jJsiA$2-=#%9L~JeW8k_s(p*h22~-GR>bI0tv`xYJEr$1+pAJ(TBwss)yI+-VSVE(mR70)k-TtON(HVfIeLzy`k(2l5Va`7@WH_O$<0a< zCI}NDx5*JfTzffI7v?eaA;-GARn3ZM3lH=GSN#i+iZjviF)O`h(}EJ%SZ#tHru)Eq zOD;rK5r_#fNp-2NXh86klS1Qn?mAaAZnwih1((L%;o08T@Ms-3`4AaqO|?LZ(Dat| z7B2}Puj4nZh=N6U!{*`F&)0cQBFGX-9;07u6+{k!U9jR6tp4BSD~U&DOHVA z&I$JoE0zL7oj^aVqMQ@#BVqtLXgU!Y;bL4TBu0kGe6#x5_@#q6vgEtmRy@G}WdEM+ zW1iU!GT_zFs2UG%=FCNA=+E}PWK0P;RZ}7XIejg}(;&-vqyuzK~R<$zZ z0)n9EVF-=*wZl(UD%ahD*1A-SGNn?nzEGvoh|oHvD(5lP(UoJIDo>1=Z5oF;Ij6G! zs?>mby##L+v>%5G>nk7nnFsVV5}0?;Xnh$n=W-VC>TCWsr`~jZQ9)sGWWEoVEHh>dGqE%2o)r0>TpIQ>-ABv=*sMz zQX~AZ!i1V6nnk|;UlK|)p|3?Q1+y5RS`_LZE0y|(Iw@AkP%7u>=Tpw9^)(l%ri3~v zRasNcY4y1mJz#{W-KXF!ywxCj;F6S&y4M~8aO%0 zG$CX(e3C++(-of)Cm`Ugxg(?Z{_9ghN)GK z+8V{Awb?`$noLOIiDzM+UYYqRj+RB4zA&Y7slHWe6?fsxHr9iTc+VnRv!Hj`|F^Ip zyjz`2F=+lO)AViTthXDiH9M=+2(=ix+d_b7AEqx83I6if+!5 zC=txZlR1%74vUK36Q168r|?jRT1{##P(e>~VzL@NT}RF9G(eY97wS4_I@Vxi6gnf@*&%=<}T} z#}BX;Yo=bPCfm%#4}9FKGdtYi&`Y~ewP>T#xX(4K*bFL5;#5t7$|16`Jnp@#gAO6M z=p(F8aJF{q-jIKy2R)tdCrfck23fEs`d1Q!4GiwYujY~-b)RF~(%bgEW^B5SubYZw z%Z53zZbT~Dw?(e%Mkpt9K_S2h9>4UnjbPrA5>lWX-2^5`Q{d`+a=fpSHosc^dC@9z z6RHPe%GToKYMcwG_55--KacvXO*MS6LH;3oM*8mQN{H5t8U=;C33>MKKc_-toJ0m zbn@DCFKZ87zphL{kvX5iZh>V-DawYaeQ8N(`N;0JvKJ3~RXR)SWg&h*eMo#rGW8-E zMm+7Jl~BKCVCk0IQ1D~RMCpdtg=vF@!biTd63~_2dQ8uatUe0P4~eN+@N6D_^Ek;V zt%v^@)4})ty?`5e07}j&Dv;YgB;TTv*6usj!()ABY3O_E5pWrLzSmq~_5AMELA&!% z(hb4QiRj7b0ZMgZOrV&Ef2wMqcP313zA+;(sv_ zfw{+ywQZ5k-zOtE7sW*-9cvXuxeiS+@Mxwd%Q+XNMO#1?0bazXlaVw!5T&hvsrlo0 zVppI&X9M#G+D+IU$BpmWI8owxWg{0M%K91koF${wvr4#Q7U2h@lDk-wOLvv_?;Ti- z^NPB}SD0&r=`CZz&7T$8_nIq*p5uOO(ND}%j0l!_zACONk4YBb*@M02<$#u`R#SN= zm4BGB|EZkib9}QQA?LF391zA1aCc-mb=R^=>V13(L=jI4;*QS>YUpoC7)(6sE<|=t zj0SLiE29*CAfejiGH-J#8CD1HRJJ0H`pY{_L!8}U{Pz)?xD8`E(u|I2wypCM>!&8? zIMwrV`#2XxM3)Xsx$vwia`7B%b0eLnCjZT(0!BJdPo8it3W^ek053)Aunk*L@{3Fl zBZBAVaFoo@Y!pM8FGS+ObFpP*8$Mo$$iN}ImkE*0IpDk0QUqvO>fbXNZbPwxhIPR- z;v&DKqy6GD;$K7<6d{9uqT7BGiC~2keiO<}p(m+?Z2kyv$fcbT^pl`q zL3p|4U-3H;1la#Z75Ig1lO{+N_)QjL`kQT*Die;Kk3zHvzj~m_Bxz12Lr+-eH>4@bI3fw6k{{ZpWD0L|{`c!XNhK86K!f^jfQG5$bo%ek>J)*!;|Z zedrvG;1h!8pnb649D~%)%z?jYYQ}%#w^;OjQ!`ND7ju_nsDo!@sNcZ`_r)orgjG|2 zm|3?ZO6(xc;T$hDDwblyZLCG|MXe~ z{IL^Ju>F=`5v`5{VZ*$#`9Bdb%c}2iSk@-f+_Ov7_meIpX3 zUg(`zzA*3gD|wqEIrtk<_cn*&pDZk%UraN|9E#u2KxY4aw46T!OHklv$RkwWEz3~v z2QNA6`-*+0&a4epjW^0pz%^-!rAC}+6HnkrgztV~Hc51KqkiH?#M($mA0`FGl0pCK z+AgfP|IPftzW*FCH3)N@YsyA8r3(ytEdw>l8;q`A%&;Vc?r%N9FRiANn}8kX-ZHjr zp?4zF{s6L*Ayv#vkI*-2xW7r&-zqV-`K~7tF7hyTS_g$)+HiXN>JdNzqsZ@+yA0$ zF#DMz{FC+fi?qk=YYO)p4mU>N9=MN~;}XnEVBQPPGW;`qJyD+@Y_v?-{3t=4_ORU) zt%|$?U$q#Ey=!XoTLJyvlI*=479p|C7lVjjO~Flse0GGL{NHunw>F3_Qj2Z>b!7)4 zA@c@#2=R3d{~QenDi(&^m#T{UH-IzJ`!oWKPxTa|y!NsC%}1);f@J=uWWvyW+rLVYN9gIwH*xgsll@?Yua<*bfNmfxvM%hdAbM_IK`e zgJqmiBE=Gu6|YY_Nj4o=dZvq&*6#~GUaV1svOf@+#yO9Zv zp}0nQN~PP|3*n0b7kkWjpvCT38*Mi#{>LoVU8R;JC-n7jA-1n?c1GQ`FD2(zn5(k< z3nQ%bYgFWRu3Kw5dpXpcEOr}*&0&o~wi0QRctR4wE}o8PhqX<%bExZ-j&hDLtTf}x z)U37Q5no)6%+3^?u||!>kP(cd4hMB_T0z7%WNJNn4}A*%AH-Zt$z&l0^d6>I{y!92 znr)+a>Tvwsz}cWLXYvtl7@e8YPsOTNz8#kGd29q!sB32g?sc;Dh#nxxXgx(zuH?vL zbkHsF49iN1QUJyeRB`gjlP&$i$l@Onu+N{&4X@;8n|VD>9W)d;s<#VU1pYZKd%k4$ zd5qX)O2lnFjpBpx*`{Al=lWzpa-*EZZ*fc4bauS$pnHEtHK4ZE410&yz5*jusDLo> z_GWK<1TPNyRgW`mJL~Ozr1+NXug5(FQOS65JuWQ<7i~SVNR@4aI$>0YH0H^Chb)CE zCpm38bX+RBdZ%-biuunO_g*w2&R+}ft7&gss$1|}?%bg2*8r@p=t@W)`;flp9wwSY zwqvQcGdGXob|=YHq2rXgRC3fw$R!l=0PV@}P~5Jqp{2b<6=GLK3lJgj?`f}BMCW>9 zvw5sHaG$x(!23wcVat1nCkn;~>q@jjT6XA03np`@|7DS3h_{%^+$xpDgM1payJ3|9&wT4jxo!g1 z@%A*UYnW(JZ)F_)CYl9|l*Q?I1-otT-mTi1KogV}&`bmN%RE6(?~LfAI+ zx6Q7?(!(W!=9{Hp_44o4&x^*-=}j&;uB=)IA*=6;PJC}+tZUunWQtOW-+_|4mXPoj z%~7M?K}!zll3 z;iW!79i|HIqz4^;MV^d+0dEU(lgK0#8=A&P;NzTi;Jd4+dFIXwl;O-oS7*mbv>=WI z+#4`RypgZa^XrPGRiLt`8q%e~5{l)g#-YYMx4J-v^;E-kp2 zR7wmC=vTwBB4IW{dZ%LA+J!}eT#SL;rOWXFK{>WdoiX^6zbfN`7_^3U=jx(N=Pj`uq}_l}e1-^6~%m&joVvQk`eAYk4L z3QGw3&EFM~rl!Inph5ZPw_p4UolqK1kK-w6bCq4#+TGa8~5DDm^+ME@V&`{2ZT7kSvYpguyOx7 z6j=g`_~!CTSTMT1i0>eZ^I78AvwZDl^}9>T`-@qLE%I$(Y@g{q+E|g8YPExWcR)ci zBuFy*Hg8y#(tK>9A%a8%*5ytd(rS+@KnQ}#v%|#U?N!)0miLD{GB8<4ZnU!+A00^U zE%xaws)4pnsHdmX?ne(X#LoSs?{E>`3FybyVXnv5@Tgh2T_MCe+A|5qJ|}+KYEEKK z>1X50C0m7p*bhIWLCt@6upbFf6s1>Pq?!jte(6y!A&f=PAJQepvTX8YGdAS&+wd5_ z?GF#1I3wP>?D+}>TG0OJ7;l|^JDYGaX1}y6z)5VEAS3-S@p6D%=9B>T);Y1|UgEsB zlqg1kL9<>EzBYH=%6RF6l8Bv=5n-9!fFm+RY;E;2n<3pTlkQg32KgbJ_DQZH-3ASx zBUUQ336Jf+cKnw8mfI56*e+$p1#xJPp>+Z*ToV5{!)P(+Vnf?R1lm}BQY=jp|1?9} zpok4HIpWVnUO3nM#mPs*85k%s)bDlP>~-?Ms_Ew7)m?Mm>t)vR~G#13vasxY|FKT;KgU zVXv+q|MRtOgf;#VQx4icmb`s~MA{|dwYsR!8lKp*TZ#$Q6ELB9Wwb{_zr;77pu#z+ zUhiWGJ}>gH8BUvJ@e{M&WyPSZ_K1kIag)yqaW-Ex`uwe9ILpnkv;Iwn{cpA5jYkkg zn_PlqC}!TUu>MCHWG%tyqDp+Vd|WaL1>9P-o()vR7dHqzNq7WD5x3L=6tQXMqlkOB zv}5x)J6?+|OB|eHh-&zPw5%bzR5=uFf1+H@i0r%3Z{Re9F27i1#KHZv2WH_86P{JD z_J6|L{s%J)wAb{5lrH9ifF*OOA#)&uBbBPb-STu|3q(Kz=Bl1+)CRnd0ZRiFQfDUu~PZroyJ3b~WxPo(9 zYNF+bg5L9_x2JEW-Rc&zm&0PMTn^&>?dG&Q~S2`9^@K!;0o=2asPV5hI01iYIN`EaM9#e8$45+hd@ z5bre8DLMKt5g$=EmpOtOlG}RDKK8&M;4^r3+xl&M_)RNI*ZT|BwZ5zCj%QgSA|TOP z7ob>TMNH_(A}b)q7`lup^6_C7TjAUr-I-3h@ia+{;6!#0qmafa*2SP<*AeeySPfWaf&hUDW5pc?Uq-RZq zL$<)A*B$&#ZH3<7Efc6S$R2p*BaCo!#@&v8<8ptFcqT+r+Cs1+_hCKL%0~Kz$CGPB zCCT>W8SyQKZIHqOHPKpv*rCLt-I3{H&YGY#@3Bbl7p%@UU}GV0;Aor;bM-Gs8?XLd z0EiDq@taY`-tz|_?femhDJwz(UAWWRAW6YVX#iMcRjw`8+WS@7o@>>6T(0otRmUDHnBxQBM-)BFe6ni#?i}=c7gswOh2n|E0LkO2U?#AbSo^gjr2zaebz5ehMR4etCl5PDReMr|6_WoGh;gf;)X{<`0Vt7#Yli6>OAs;+8zUm zGW8!DJzu9yI{TM41X*3%g2J_nOI4|tyP0$uIDTnPTKfM|OZbzXLuuId&%MT%eNI_d z9Q?s>r12ZOY`2Y}^%@+vZ;0#14!5x?sYqARRuU#=T8y0GO(uMxBsIAq>giM5pgoCm zZZ*{)THT-t;n?H3$NBYmpMGvR*6iR{vNfKA8yd4q9;vYIvrfL{)B5d z$5@pq&F|TCky+2JeU+l!p`X}cCw#Ibx3JEh4l==HS%wiGN&#uNd1U38H6HCAH&30| z*57xs%@ij5s8WtKaNS9iJNtw21JMgS`xCwTRAl5usV{HbYGSuV7 zlb+?ig{8>Uk6pE`hS%tY?9v3$a9RTwl?gU+Nr3e!6KpzWhUtvAdLeCVk5(&B(s0#a zL)@4ff;5S3!iT4*<+fnK-~Fvs*IU2up9||fB!)iTT{5hx8qJd4f{$|^gsCKmt*MgM zR5htkX&Wok#&IGh(f^F ziSx~Mfc`<@;!LVxK6_L*yj(@fse|aH-(S-YTG`Jt=h%GQ9!m*5HB>(6V-&?zW3}ck z##qr?x}+Gnw&pMF#!aNB&%61ZIkiu{(<&sFj2E0wO-dD6Wf4^>Q-ahZ$GU2up(*mFDb$n0g zG%504b|9X;u7nn3dX)y`GlfQ!%0)pb$rtiUg8lz!uu(2)>hl6U+MP4u$Mic`ua89Q%%9v@box8@94e%%#Mzz({d9;gnTR)ShQk9b?7Z#pCY8PED)Mj#oc_o?h@|)!HD*44d1hdsgsYIAl)9$BQ#{Wy z%{9x{4tIN$oPAyU1h*CSa9w+DI8~APLcak!XVri-hHYk0Y+?}V1MsLFEYhwRmN+>3 z-;V=tBu?eMHj)3FyKw8AJCs}ByZwp^#H)QZX`_=A@cic<{r<(zKh?UnEv}w?s2EFB zP{`U2Jc2^*UPBIAL{AT%4d)P*^M^- z%NCJp_644WgJxTh%^ld}&l(u0v@1uEqdBq~RqYlbj%48jgK@MQ5xGQ6OkwHOOFQ)n z?+>&1U>oIZi+6mGIDqrWW~E!WXoZ_54=8?yFm|m2yl^|INMK^F=3Nd9C*xZ|||B zm-0*zc1zS7te;Pu?U&AN8~EGuh_ha&Z1qp`S=+$z{ybBJYF;anK)&X@RF$w(r%2MQ zo*bz7Q{2-FYLX_PRwyIc^+HO@vf}dRuY0V*K}|a%@q3_oEo3&8>Lqs0Z{I)WfklYN ze0w7O@;s{Xx*2SL)Dtuh4T?3v2HF{yR3KG4)DzKqiX()|2HrB{6VlolM2?8$ml|eu z`R+1N)x;oCRc(F$V=^p4$Ws949 zui&j_NK;GU<og?))2ISUM|>2aI5Mi;)gO&p7sM&WTo$fhmtEpLqPZO?Xk zD|REQ=1S+ojZZ~2y2?VqOZFeb93_@#g7M|4FgAKrr1H$eSW<{&e7R)B7l)KQ=vB0` zcBAPFDTjv9+!`O0B)e(WH0o4RHYV@}?-9bO=cY=Y+C;;e{R!Lmd2Sz8K&5i`x2FCl z&`glav*mb|N!M$=6VbKk$0U>P%F(Ca+eczN&Kb|0Nd@#AGF>Lvi^~?zR@Kf&n2=zx|J{bN;C>akpt<`C#RY3`2>o*~4s%wiLodTz z$6*I~{>XpyKDgkI%t8n`e|J0ahoT=?+2k9Eg&XS1BiV>rRHM~lLN7-Hpa({wi{Dp5 zSRLoN!%W)e_5ybmjag%ON6;0OSA^RghtnY#_-5pMKxXn%C8hWZEHcC=VAr-=5L1Wg z2?otmPQFg-cwGlj^jlbY6tms*tqv(AKma%4p7JO zGVH{A60{h}Kp5TZq#}k@C`&k#cI^QZ7ORJcOT0QrH%5R|F2Vc%69TM9HF`3vyOu}ae@_0D*90^azoz8Kccj}cT$4i?kI0(rRnQKGyXO$~r`8a8 z=_cx%1>14@%cpbsDHZ(1$7bH~tXd-=?Q-Vk?bniT+|Q%yd{QPWx64?&16e5>57zFF ztB$BO8mC(+ryz$c8N3x6BQ3RSsk&UxlTsDd@-K%h12RN*mV(MLQAd^VEl? z^ei!I(P%>S{FN^i;Q@Jl6k}`tMcFH8@@Wg}Qu>FB-0O~dDE`KQ{>lUN&!@Ej`vgvq zv%4DPG-Dqx$UyzqCK>S90XMM+optgn*AuoEg@;fh7LHf8P{O@!7a!wAF`ehu*z!d; zD31RD{L(D89psN?TL|7Gs$Utw1H~BuSB8sS%G-;WOm;^;-KR?IybpvK7!zP!B`+%- z@Gf$6^}Yh>Ocs^#|CfW&U!P;#;Md<7)D!voKmBFD{O>>Z*8WglR-jk~+N&OdW!{+T!T&D{Ie+N=7U(`T)!vv<|D zs`@H{BPlo>ao&XmD~ugC_FjvSS}A`z(!{x+;xqXPhk4~|0(r$Z{KB-#B5}%p1PEf# zm;>5FaLr^y*Axldi?&?h?prLTgMZGSLyX#YE!N|}Pads~qlRNI5Wx}h@`4x%`0*{) zx3uwiyG?0$BZRehamBrFC%#VQKvuYAZ;AS|c^qW{0Y(3pVxau5EZ(Vj{RWH~c3Xx*Ljd_EtlTMMF_ z@N|UXetPVpc>%p#_l>Q2fMf_xQanoYxjkRey4fR6i6~hmbh_k;awofs6bkg1y%Jg z)>lagZr)<*n$*#8#*TFM2qqhoP6s5z`i=7S(cAQlpG$)yJ9FFIrkEPg3KCU)K7WWt zJEm!3UF`B6KBWb53TfmJq{;u38{%#IwFzmJm^?aH319v0v9dMzSY6JFuAHiXdpI6f5K{5U@( z#oyXIZ+td5eMV`|A%)GI9fm8{7d(ANZ{IA6T_)F;G=0Wo-z0XeKxU%b#qMY zg2@g!=0V9-_^PODOBi4mkaDTe!89JV8yH2pPSsUA#q;Q+C({@(5yF=?l!^J8 zUYI7Ho}+>!w5Qb-C1s{o`&G2^^%7q3$=Q3(M#{A*0~W>sD@^v85W`i#a&uPIfz`HGO0$%$0q8*QeHtC$RsO9n_M10<3GqR9ZEWB`9M zfF~Kil?-4{2CyUp82>`cSJczxE|~CJ8d+fUcD6Uih@WOW!reF>J_PWJPx+0`JYLP; z(L6|{dE7nLm3b&>Es*=MmaD8TkU!Ohe;rk2&0c%iJ7|lGbaoZOSXohcsACUtNGP)- zYXg4u{funu0W&db!q)i_*!52oEE(23Kll~`yvBR5kz=9^qM>gYLh`t+&UlgWbw!Wu$CX|PdIorIff0mM;@Sk}-y;3vAkfKNLk)Z&NagDp(rY7*Q0T#&9R{u+R>BV&=cO!`?{gWvY|&iIbfY(NU=6( z{T30}^$FMz^|Z5@<>kN9P*-pI#Rdy@!oq6Gs`7Nk7f0bw7c2wRl@P> zd-VE$Mjv4sKbFSAE?TjqyscTAf`B>YVAVfY_95<&l-TH)u&Lqt{e|;>`2P*(fq)7& z$Rew`YQ>3Ij`A$iGPculw##z1+j6$Ya<*3$Z0{=A-dC_e-uhIrgBzrb9irS~)s4Bx za`vs?h*yUF`u9&ob=9~2=lczl7zEiR_^ya(TNM?jyRDsXWC2A9kqGh=3~%HsR*@CwDZ?s?WN$3 z+Bk3XGUz2ugxVH#GgtQcYm4%uC+#Kwj@oc<^N#x8*a`R&_rOtjF|_Hkry zJqnPdI|4w}69=thwcQHE+e3{UL_5CHV=&C#0Prm0`%#EXwHlBiZ^@rlgs&Nm$fabN zTo-pdr_j#xR5g27@g~d`xQ5LJeE3%S3ypn_z*uGOyJ_jhyvNOqY!96sXFhnI4L8Sa z)+6sil=b=m@AQI0(PqJ99~!mn+o|M1ds!}_Jw^*){ik@`H$=7fJ9>YoxgphkKxZJ2 z7zPv{QSxQ_6+bV+Jypx_DFNs!%N<1kot4jqip<-g37ddc@VwA0!pdnkSdyYjKC~2v7SWijyk0pw3^d(Fc6B}+u zY!4KZn}LgK%rSfWS-zntXb6!rxS;pe&qBcL{!E{G2ZqyKR6Yop5kTW3gGep4p^2e8 zxZTd6Z#UphdoX_mGq%l>Z8N0oC3n9Pb$WsH0{XrR)d#Oyo1Co_)(1KgW>9w~ZzXwDhCW;@mWS6@YN zY5#&k82ASQ(Wx-j8$bXNG!LUVk~h&-Cz0iMk=0kx&96zE--@3@$8W;X4q$3WF}D-Z z8MW*WzlwdV`7ZWs!kfG4M`|@ld0L=anIM&aFpmL$!H$sz-4hn|^J8W%83o$-J$kQp zPA!ue;L2vMxBf4%8O z#N~vDE#3#G+DewzXe)d8RrV&c5qdP_*A3z8Am;ZN=2v8vuVi&TErYDj^Du;+-$tA( zeHJEW=J%2}Fb3}eglAf#PtD$wE!lX}9G);hb&Ck} z9dGUJ`*Y||kO%#^Q16kv#YK3eZRoV6H(v1uzJ(2a~ zz7&GKWP-lH?fXuoER--7bU*opF};nUHd7F;Sr2i!z9rbeG@J2z-=qbaN^DSso7TJ= zt9v7>>^qx8QG?-xis>kM@LZMR)UNJV`x9b!Z2({jMH}tQU&(CdJs|7Ne@-DKh3J*9 zx&~fl4Fccl8at5FnpJ&4%ji{oh^=UUdL%x#HH7R_(LIs`O+!OrU4;_iKr_#w!Alyj zt?Qls(!2eo_iA15{kq;ZvZNnk3YIQ9knaP~`vb7+1Mn>@kPjB<4GZjo1-||0IGgSp z-QgR(?i>BuFB;!3dI+m)pEhfsOB!9A{-;4+Da(SvL3EUKjnPx2x8s$=&Q8Oq1Tmj< z>Z&_CoVc}@@wD7~X_Tq;g_3voYh@2?&(^8aXLxLSGzfBiS<`1M_8cGJsV46jSeS5s zrP`9GLHT=7ZaG`252f7mSuGzv&}a<$T=$J@(~RM3^7|iN>Mt963bh5bsd@As>_%{B zTW0e8IH;RI#3|V2bk2d&12c}7@#M{&={BQYUU=SYz~%I4i(se^GQtNd;41cAxtwMG zTBKL=nDo73B5*$*NxcbvTRH0<)|t7YzGGd-j)QVk%9Mt1nAU$N8nos7ZDLveJL=>k z%2dX^J83%#)>Fq?+O!oJ>Ss|Af`gpp6=`zouaI$;!`62s@B%kDv~==$IJsCe@8n01-B+M*5puu$ov?1xskrcC&Jdc^H=9 z-a$#cS}c&jQ=i*B*ZB$om|91I@nOI6=V94npe%&p6Ly3TJ7U)OUTpW_R4oKl%LI@a5{aGS_)sU3k|seq^k zD%jczoJkpbIW+C6s;RfXmYLOzdi*C#YYipaL_EtvM^|XOlPN5YEyM#FUi=V{v5pA? z>BS0n5@ZZUkdEq+vxd2rD6uiM3W4vcS&8gG!nCL;R`d?RVRdVr!EOh z^FyS%yu|F-+T_;1J4Xgx29p0{sK2ZTsMM~L9!vhb zg#d&U=u(S?za)7I<-$&hVQqGbUb@ zi(s`2VxOJGve?BzjZM8zmh55waou;q1&jR$8xQ38r(wWS_r1^tcsu>V7w1* zXnPsDrcAW)-BJHl*8hT_1SEH+5YJh}VXUmiivHOCRZ!uIth~|o?$f!NdC4kvmTC|$ z1}apHAQ4NdLUV8a^y0Jp-Q8eiq1u#Yn?3@Ic{oVn4bP6R@9g(Iaiy&t5@>^ z|1DM&>mOKA@@kezv}o>W)i2*4aZ0^>NV9_!6-sg?CKfHm!st3+dxsq?imumg64VF; z_Jw6OKHjo(?cBCM9`}DVmCDh{6pt`$>dz!bB%@&d;NtF#CJ~v#KrN>p5Jj@f|D{pQ z?U{l5eS-pP&f*&%Sz^+;#zfXI0@f|%h0IK1jsc%dySajJFE!yU z2j3lT)}}A#PfzHNT<=k?w0o&pmns-|EG=9^({IGVVao-^)TM1kbY-@cQNhl>%$EuK zb0)+kA$zD5KQJQ{HYv9y+sO4lz1RIgZ2U7lhgr?zVgD{eMSlzuJ0bPXpN$TG971%!vN$jdF?WDeVRd-!GNPi^Ad_^{{nw&6)G8q(Z z@2Ood`gybKjLe;W@fZFA8P#psw#<>57VN>k?Cz56SRK#Oy9o|aeM!cx6yWfXuV+`#NO<- zefV@D^+w_SK6r#AoQ5OS<(%phT!OrU;`zl5cdPq@>BW#%o7>afnCH_P?(^e0gY*6R zgXh!v*vX|9cS|eh(`ld9rB-kr?z1cX!`2)`JEe`(owRv802y03#!_3KA5BXjhq=&5 zUF6~BKe5+5KO78=dA{7trM)~CRVvq^Oy#wiij3C|@BeI@s%ZKgj{*F}a(d9rZezZZ zZ0EQcXuUa`>A6|k!*zZTPgrJpKqK|!v4qmJwvewSujc8TN%3+k5bS%Fq?*)M0j~9; zHU_$2iz@O+f$CQ;l`tPW4u(Y9Hs@Y0p2|F*c8;|?pLb}kuzxkbcJcIYjH8h{Y55!v zC-tkVHXgIBuqNHL;!AjL*5q}We2k1nvb2SMJkxBXqv{C!EWcX7s0cy^LA~eU`k>7wZNg zMXj<@zsiAA=mq{(^U>B@O858&O`S^M#j@`$mpXHtMufGX}C)hM^Qb4U#{lrPF@bUXJ6>l zp7~!=r7lm>S}AwNf;CSRIn2vWZBC;sS}~1i7Cj?9n_&Q*WLF@9tWW6?B(#mR$->&~JcoJ0=%+~~>^2rE~6l@?#C z(v`!xP}fz|Frc? z_xO5Z@pV`b?omT%Fs?8-7&m1bk$W+h=TTaz5SROBINczg$2fg^z^#lL0&0*!FVT^| z`MHVYb`6R52WeYssf6iZ-LQscVyO7VA8&@$u(e4!YF=+U>tLB49OTc`()G$7@fWrT ze+iJ!5VrqR5SAv3?%5H|k1WDp!5<^cU%+qoGk`vjG%kP|Nd_M=7HL9(ppYmtX?LMM zSia;YGtzf!EnQ8uWM*PN_Q6Z-5WzfkWTrS&HnON%!Np4Vu07TvMKU2sDth6fnEUgM z4z-@K)V;-6cjV_aNs{M+`i0G{qXqS9Ei%W3?6pg@R3{zGl9AB&76-k}ILB8mOu*6V zP#~>@CMi856LR7%v*kV`K+EX-TH%FpZDuGCJ4vsyMI5DHFGJiRVjzruL4+h&uTq&r zOs`U&gndXaLHy^C9%@2y#DHkRA$ha{{elEZ(~#a^&)r7coP7x!YVh0mJEXS7-3u(t z&UDG1@`Z~7yYfW6o^m0YmhL9DS~q7^2Je0*Pbt?jkNdvtQIIsNo42)6%Hr9b*IXkV zvraTCS)RFDg|Td3?tMA3rqQ-VRdz-aGgE;)-_A!-kb+2bQrJ9E)KQD!YHFcj%>SmT zI?;3XGG?-^rgmnsjT03g_PB798Z})i3Lsgi+*)(AB%DYKw4`4B@VO5G22pFv!hyF| z=qjJR7xUY$^>Xfv3CA|3^i=&D?qf)YC$NqUMDF~^hZ6$B)hxA2l%a6sYNH3P=~eKe zed!l?NZ1GU^2P>=#lI#{{v1F`po|?DrB@+|-lAU+AfeE66d{Y&(61CFX&TUTOb~Bi zS(}`d9m7#6ST%L$sBhltehCtv81K!>?C78uzE%AlA+3N0tM zk}h3yxxDX838&S1+o9pDqcMw^c>0Yk&%Uz*U@#P~2I(}mPh*y}Bho%W1~tk)QU>*x z{ZU>GlIbnp6@h#TpGMjICRqDeX~)m|$Gj=$Zmszu$K&8}BfESK4EQ9)+a=PK+wua3 zUR9Nw8le2Q=<{DNG8ZGl9?C~JLMNf{3S*btE3k{xs)h&Z%Xo{Nxmf-a7R%eqfr~~D zvpF0d^Ae^j?>-~eMA9sd>>K4VbBsHDABj>FYMGEsiDSPPcVq4wbw!exKHtCQ)xewf z1W>6#T>ts+P?#pHzQ%>viYEVyW_%!n7Q}{L(Nh^J@XU;gLo+caH33bry zG_N(R z@=CmIP*TM{{Bf|1mg!y}a@Zg}Go=k`NFXV2&2)DxK zJQQ1VX_8&s9_jydM3mXWk1Wie!EYB45SE72!XXtJ7#b~&`oX?JI_|xFfwUvaK79r? z>OM8E2L5y`?}|cxkxygNCVrzB|6p6>P>ma&l{)ge>J6Jfk-Op^y!(%83Uoc$L4*b2KaG~{7T8cZnRNaEaVdZ9z66 zWqu`qCC>Ebw~GmoM3Nyw>_L(tK+Hj!P$3v3%2X$46z2Ekmlx$%3e1lRut1v7AduA@ z!Yp1+_5Go6&@Bi}v2`b1PEWY3_(G0ckgM1b>R-rwDw`v$ndn4|B{!p%+SJbbmcc-L z<~Rc99ZHcgr;LPmyH1WUIiZ$z0fiO=_Ka6fsXJYPBSU?kSZirwd**en8W@7eROXq$ zj?obQH})b;h!6x3WhxVh5@pI0u!->}@W+VpqXyFM+(%#S?F`qF}l_P|EIEA$L@H70fP;jUsmqE+XKm#g1Y842 zBLa8>ckvLhi8KGbLF2cJ3y66*_oJvY%;1T((IHh&`0|Zem4RBlU=W-1!#KyJBzB;V zy5;b+1Ha0rmdLQYc2_xZIAm4fIEGtJ8>&wq0#a zoX@JwOe#zfu0bt$6A9_x@B}kwa)pkd63Of!{zBz!QT{^tY_?$e1mW0VdDO_lPX(fp z`{c3;1Tzws*~h~5EF>?|#9<46E<=!#cQ`Sk9I&*$d z+QFQbOc*s%u&A2xcp$_sv!1&@z&SY?qY0ktUg#*Ol{6cDXo$u(zv*FN`gneKTL+EBmIfyz7ZGGk%|K z@z-ovfkJO#d*lL10tF)39s&gd*&KoymFz+OLiOy%0C`_wg&=vQ$U>w73xXMqY}wIa zOdF0cZ{DzTSwcR^q93}%Lj62Ah(1^f$U3yg#!Kq>hSDcq2&@_{{;T`ZbeH$(s<#eb zcuZlX&^tKTXXLQ0)suAc>?HzvbK%8~a+Nhc+>9i4%BcmFv?fYtOp1%ojJiVqv+%)# zY5ax4*+Bw@{Mn)cg;m0_f$|N)_J{?rk%h0Knctt%quB%2qh1YZBAlrH+~XW{_TM?GP(F!b<(ssC7cNDCc~#51 zV*`EDPri^Loi|kg*FHP;aItivmC7{L81zGvF$+&sYHUxo+@b&MYR2bvP0KJmE7FYS zQ@zp+?$rNA$RW8qf~@ectb<~q0M_xk8Zq`T>rb*?5m~Tb0lbn++pw%x@C%6O%O=t0 zZ3ZdM+qiQ6kqT9knCk#`Ex31Ie(qxcB!sSn^PfE)n@W!^&eh9s3(AZc*OnLe3E4~u zBUEQD8130JENG_x%TWRY?YC3sr3N)a-KQpi_Pxxx;IYAwmgNDU{fQuF;bj$@uH?g` z$Vw;6*kM)M*ez%?Z3bOxLf~47Mz8X`iXLpD$z1>%DpPQJ0kwhVT{yMi z{8K@os@d87fSS5@Yfwyx+QJ zov8THN=3-x6tdYk+ynRnKx>XC=8+zq21i>ww|dhI4{HkvGA~fjiKZtH^j;!mOZDtAAOgPJZEciLpyUzVI-t$F+5tBKDtb3B+OB&L*0Lq zL5;i*&$}X%&;G%F!qDC?Jj}O|KVQtZvFfx@@4t7xTp-RDxOVn4z06pUCT59lX6m{V z8CLRd_I>d-~z3-NkfT)|xd*^%GC$PLo)QkX^{3b>FPGQ>7DKPdjuV2iZ< zLDZ`KeFO<1`{68xd zDMNd~!$6-#<$N)pM)`d9kM;?tClMd*Q8P+E?Tcm{lTSm?yRFn z{epceJc3viV_`SEe;_|>qTxtmFaphEs$&_n&$ic7TnqVg-%=|i-ra0C^xnmC8x^E3 zXbWPM#_ZVqs?Tsi1k*apNP>~XRyyYjhgn4cZ@2Hu9HVs~Nx zi&)DU96gpG?FP}x+GHvZ6fLSuOk4$7H595EGl6rnB3ZZ7u>NNS=C+%*<9Q{5q4|p? zolh9$5(r($yeseI-p58VW6TH;V_*geA{Uck?4`(uO++p79u2DktslI2E-b4qQhBiz zoBx+FYi8sej0}Tp4N0#cAu%Rq=zZm?QGxv=DMn4_HwKd>^$8LWb zN7^g}apwJp`Uk0}1!?pZ>?<1SuTXUt{!1H&5TS_SDt z4vLl?#ab=9oTUSgg=%}uMlU*^-9nBB{SPKCZYd8Jttcmo@HHOOej-|_W58y3@-L^O zqULf(RO!r|fP;!XlK9J

wKR?0ykP=_Y@M?+iYdul5ipKW8Im(`A3iCd)Py1_|2= z(?p;|{EX1VFTrc!aq*AA(Xo?@B9u)k>tiJ##@Gpn2*8p8$=k6cN?t4H$pa-0Vp`@9 zq_$kiIk>mI`yf4tZ~BiOWS$kuG0J-+cggaR|E7HB)#Rtix7k9%QW1g?y%8i4xe=-O zBX}eDEnii?s`9${R{yw!-ouu)LD{V(G{nDaZA1yRMcEgYM6^OlDc+%6WKxpt(nh6u z!I?PPIC93WGGe95RQVEq$P-TADFQc`Po27hc8LcW!|T(>_;7e|_?%x|d@kP^zcU6H zzcGg0L!SgpVoiR`hRP<+RuZNVwv1TCC%*Br5VgUD&5AOf1+K^RKb^f%fbPS&v{R$d z(Be34GbK#`Cs~zMS9m-$qOs=7G_gL6-6dH{gXY%;*s~n!MzuNHO;o2!7a&J;&lu}( zS~CP3U%uuDgEoGf|DQEL$lHR_h5o~*Dx@5vD*5Jo;AAMI z*M<`VE~A;|wk4YY2h_9Dj3td}%xN@{dE7F1LRaamkW*1f%ZqSbY42UhG+*0ZJp|)d z#_#uDPZDMWg$adqg+B|+36Dh>L~KWB;!)vI;kWRr^0;_k!jm;1KswIDZTB{UtMo-+ z3hs3iG}7yv4877=j77%E6bV6ilf`GQ8d*O3(mwH4rtPi)rQg~c0xra4Bh|{==xz6( zR<&p1ThV&7K58^Hk?-?cd}zgDVbTAFu*)G*_m!F8w+ zqDk~MRv@@c)scNF`HUgXSvosFLjBOD%)iWiC(iZ6jDfzQd8$dkzD;#K{<+UFA17-|o0l0AFVXk0=h zOmN*gOptgiEYVFt$i@};%cxQJYE^FWHy0TaYBUx!pGR^=90(n!A@`i)T)sk6s>~D4 zk5^lJt{2T?!?76V`~ttQ?+7D~@mBaSzGCpYe7}6Z_hIt=B--S=N#sdTHhMN|HmdMP zVKL#d2*rq1ycRyuQQnoOQMI4RzMWY%3N=QMkHa?ByI%k+a~e)KFghDoN-kDHt+=no zM5DXi+B1=B*ssLPO?=l}Z}6BZKVZYmISG2c?Xzrie*m^5S}nO7nB#nWYO-aH&M`E0 zQKZow3d>YHdJs}U(=D-|a+6&kFFF;6+m*)0byRDis+IMUv$(1bZ(Jl#NFZ@oaumapbi z{%j@{;K+Or{^raW>|mMVo?tlbO6lUxIcoar%eHrHIh|&P3`Vp{nZaVkRDW?!Zpcsf zHUkNRz0^gS?7KaAVaJHS=}CtCtCqlMV`-(ey||q)@wecH0QA)O<=d`o05^z3*U92M z;jZM>l(r5U@-ic315RnK*V(WFpiKn^?K$-L6i2y)R+GYPEUvDJC=fF3L9;*i!zGTW zXWU5PslGt1U`J?1plXmn1#&>+^}X7XC&9ZOhnn=X*DH2R#Q~X# z&|H=V%dVIu7q=Ytc)%5&qdyHWXm_Sez3$Wa^$%Tgerf9qhX1#ky2G_|g^cDVr8fi7 zj7!v7!)n?&X&)T7UmZPL*#9*AQRKA`@}wE7E!v}jf!9ig7W2%)^3k9-wqPt#m>TDa zw>4JXvHs-UgA9%}hN=0_%{^6mQq(eU@Bn+ggRpU(HpePviOuY<8+Is2FfYmu1y;Uq z$lyx^QC@VsStgD9m9GW@`HkaB+(@9vSSlNa=1g}&l>je`hlgLSD6sEs+)iT~mV*J; z$vo}oKFJ+kAeKbvUg0}E8*q>i3Uga@7dlZgj|s-`PFI{(7dTC5(U9=ScF4o&L91BI zB(xtI%0M%91jChht}7Ol&*F`F;n-%&4DU<^R#Fx?pBwz)>bXZXTW5QSNo6;-et?Q* z%A{S>!@E_wnr1lk%zYIndv6QP;37-mAJ%-}tYK>iPrfOxWpJ@$T;@5JJ8_qeG)gLE zgxX`whG`&Gm^;uuw9u8u6NnfWrweenALrt3*a5s5#oGkHCwD}GSQ4PODMh={yOVi3 z(X*2~5+1&w8%EedqmHKk0*H?O8bp^*?g$0d+d^+P$1f$WR?44Y>;&msp6ZABGpVwy zI(Lw>4?0Vvu6?^C+?gqB1Yw24p^yqSU@53)$CY!|CC(NOSh3E2H%HS5BjZV_IEb_2R5bAdCyOF--Z+gT(C z4#^#IEEaD?C7uIr=So}KdbjgLuo5$ zVneGf0Qu;zUi4+ww>FBy<`Sc#3R~#hs&buYqp+~t+^PqS6`Co~tPxopVHzMKi#dR- z#PI$?`m5j}FfBB@yIx!0H<$PIIJ~33+R?FCy+0d%vWBKJjIe?xGql*Fp zueBwB&Ko_f^7Hyd2%D={2!A!qT@kI2?EAOhz0evO(eRTk^asNT8)(B(JTSm!^j9Z3 zax%{Vx;m?O5UAc7`X<9G;i$ROse~3MMTKm-QEpJN+BC>c4jZ@@8ph#Ay z?dh?NUzPlWh3>9oh}R<T$*V9A1eS^pH5 zwUPddz$^`-sUHIed2#lnRA-buGbN$!KQE=&ksmPfC%;)X`^2dJgIw`k*+7$mqPa6G z#EqJV__K@pn@rbv+yB&6erLB7DP}}R+!MNho1@@_Kkw2b-G@=fEs8s|ihD&OIbj~t zJDb;_V%lk}cN{J&*hL0cH1BgTQ(OrvnLc6MqrH!TIhNe@c(~SuJ+>6*t*ZZND ztk*CE6k;1fgNBM0ho(hWO56I)4OizvlGrg5$#%t&{_bJ27yPxyT#BL^g zQt77z4^<<+x+_@x(K2__=p#*6vL}ZrRYq$u{6d;En(t0|2nUKMbnvwBbX?kQSUMNs z_MI%9EWIopEIqd&p2~jKVP{a_H((?%Memnh=8zM#XqxYJzxC5^3ZA8_^>OxOzyAq>$C`5Yn_ zGKOY|wu7ccOHE5n*Q%rb&8_P;jPCA@Q&l955i5=%%j(A%06YWbf+FtGlMIbKH&qzh zN6U@P2|?rO;-n;8Z7U2FZ~)Uph|0QA=x$Zo6>MKpP50|F$rOK~mOhqtmhM~H?vvgG zTsp3ASl_U8leLp|+;IEyXgN}UVZWEubUV8zmRgxe#w6`Y4v4#9d>#Gx-{L~w*H5swT*rj7PaW_Dtu6h zVVlVi_5>uIbmeX4zVP)-H9cQgRg z(5h~=4%5$Fw|a5 z)%n{m8bnWSh#qF~P6pLmK|>iz+dwOgrf&c&k~>mB>He(}i^XEca|1(TGzKXGPmfmc zgN2k0G8&*SrALJOLM30wVFXxH>`KFFUQ_zbgyjk%^@wtZm3Bn43|zEOSQf>(a6zWo zO>};0SC5}q4Ns=@Y8muh{fT`LQ=_S1-U7(ZNe4;`n!Im_s&bR34azUxq3Vt!ia znskr7r|eyOUW#Azmu5a5Km*sZ09Lt;ciQ9H#WU+rcn?l1pB3_CAD`|2I{9R;z`o*nJwX zPS^F>FYhmr$LQ(Du8)3kfHwMOo!iiLN584AKuhg&qBIfV8r;Ls$c>(%`xa`go@tJ*&-tG?`>J&sg`Ldq@2NZ7io9}EBi-9!5}avkU=1PDh@ zrRC7SO;|_ovh~wK)1+n=V& zTr>~8+rV|iE?qw$KnmT%z^V5%c^%ac6+naTq3_glirIAn$m_#dYv1fteOpW3dUq#K zf_cc@yDBWz~KakcTp$M|#B70zUK zh=Y!UzM~;bzUajJ^4pEW%VS z-UloCjAq_3e?`;{%56c`<#Fxb?9{BDWD5^BHZ_+_w)>yNes@u6EbkgdBhKMpJKo#Oy+-EnP*T0)7>LZ50&2z#;_(kC0 zaq_zSVEDn{$Kb`_dkHXxHiqAWntU^fG)a;DC7W6JBqB=eYaMoj4+@A6ZHSok3r@Q- z_3OfB>I^xRtS{Ev$C*RRM^uF9tTwpRgs#<-*WHKqlNtrA)9bzq|CE38=E7_k{D=Or zyt!QmOLKaT{O4pb)&Q;CoKyWAtSujkEq90Dfgrq!wu%zxXB|&uA#RCM`!uMnTMJa% zmk9l5Cf8%QA{bN7kJhr5Sy&cofANG5CV0csWEMgs8~-OsD@91s>I7J5aaiA3uF5!B zkgwZekLlV!Sx|iQ*?RwoSO4T{*7N8OC2U+h4*onH$-(t{g7c8|KTLwVy=gq^2Ua^B z#tRe(hN+|vvK#El|CIU!oS<$bT3n6qiSK@~LSNARSqn7q6~G<^&3OGzm&*M$%}$%W zZD@w~|C|80!LQ-c^*&QwkHqbrDvT^c(&$l|TjkqgBAImrUUsN|B#*HabGCe*2z&uC zeY9>g&tk}xR9H1TN%}Gvq#_~17S7FMmnYQ)mWp8dzW6)W-iOb4(D?Q5a}NK?x7}lW zx1xNBah@=&d{Ctf;anPoa~Xk0l5DY87zC0M=CYP;93zRv&$%As{=YSM^oN|pKhk`1 zk~#)@>lW4J&(-Nq1ss14UH)y_J#Cyse>Ay{^3De(96OP8ra$9@yt6+^%Ha8jjpE6x zOp36DFD+~NOp6WkHRcLxP0dB*%kYcPwuNHWOsZLr=klI|%@*bmR-4iZ%y%ZV;^I#6=WNWkt1V0A-?ngS?fpW zqR-&Gw|08c`_L|^=G=M-=P{)+!I@quN;i|+_g$a0!*LGaKo)tSWZQ}{bS-=@qk!?UR_FaKj03* zMRIS8T~&d5(w0AImnx%as4Z_fTyO;)t$F8U zzZ4vlg#C6O87>1sS*nwQYIkm#gQ?bF?7#c?-M2p0KGq@DKdgh!(7^yO7WiW?R4;L_ zQV2zeCE6MtiT^uG1semZ8*xvj6Yg0wx12NtsdGcQ=tV{ItBU2RFC!1NC6iLogm<=N zqn9Qz4BmI=PJ0hl)g|^zKD>UOoxExiE5QYSk7L?z1I|8x--FS>@4(1lP%nBfYcFcZ z#}KiQGBhQ$HQH8P@m1a(Dz+27bCg18ohQV-wItw1xYqOIwb~5{e-?3E-L8^kwY;X~ zYXwcqiGtz45EuN7zpm_Ge&Jt|^1KD(wmn@RrCq+Hdfc829L8yR-XD1~t+l?~LmpUE&clu0C{H(mUyUp#)uze9G2sAAe3+8;A>sW7vYf+mu$W-s`;tS78yz6#~F zDPkoG%o|y zs~$rZ^g5rNLUnmXvy_+_=fy7k4l>}97h500jEf##mj)l`j$I>`qu`rbYg2_4_XO4i zC7zb7@{3Q1>qouA|nq3S-k0aZXry5ex zOldZk3(?!CnXf=`oh8tw;+@@SSH)5Z{%nd^fkI~*S&l+xYHdJ8KTBXpMO`fLr*UCw z4PgnoH8VRap+YiZ_{m{iRwzp2Twx?=e~{<-g_Y1!VIYTPq(mmMi_4ZheW)Mjx*!JM z0AF%0b-IhIsJfMH9(*)d{m&Ig>wlxp=Pwo*RwXJH7*tJG2@a|0R|)=TDpNjJtw&W_ zEYPF6tQ^d4%5xm#iq)^tq2APoH?A4r9cY`pMhx&J)@~V4p)%}(Bf6oq(+{Vd_o@Q>dUwcIh(b;;XTvi@= zF8@b*>XR3M-W28oN+ArbO37=Id{`vkrL=Pj(ILgP;RG-@&nTA->u{;A`+0L!dzl$h zB>%@ppKCmzMfJ4A#dWm>J;WTTDLqPAUtB#`H4tC}s0G5?#MJ;T=15KGo#!^YwK2-X zhO~9HA|vXXs(~JJLFoXGS`cecJzX4YkQntO9uUKS4acigOJ2VhzPEe^`_0&>LOFMN z8FO}n@f-v%>y@|Z5B%)9iu?Ibp{|#I5_)ro49X7iDc9a^${DY{-H=mW+y0hj+9oxY zrt1j`;-vNh(K;xfQ*iFeH?jKGZz54q*7^Y*SwQtSB_L)-`Q65@qjYIEA$u-M@eFB{ZrQ>65Z`Tu3CWBwm4i0Gv%V#1uJF9#G#e!d9Y;R`} zQ_e$i-;oavQ47lx!aH9`ewrtQ$8$v;wv3&*j=ZFmlMp|F8=h&Mo*6zmIrx^}#$ltI zz&7+yBU3O zC__m8ufKXGve=v2Zio(d*N?jL(C6$c33EE5A zC+~PCX^c4V-V@$>&2{rDk0Ofw+vAbyE+~tWoA00J{g^o>!U-vOv|>kixbQk&1t&ABE7&>$l^(r|waX8#w?z9KB-2hINdAlpJ_nw)JYs>DB3vK+jr|O{Z6v zT<^W{zw)CH6Tw!+LhcW|e%yKm;rt4c`+X4pS78)l64%Or-jE>$V(za`c zYwLH{r(!pq3@+>KOZOV>3A&x^^4rS*BBWg`6k;S;w2zQ*J+D7(yy|v*WyJNK310~N zuSI`>&;6C;8JEpxgKzw`P6I{hRpX4}-TMQ2;T_PU$95=m&Mm#4eYkdEiR~RQB2UAt z2H)eBnItV)u5Oqd*dC&TG`t9=AGlwgML~M@zvkO|mFe_qoa?>C^;ebKlz)rC|H_K; z9S8P*ei0n~yw}f)!ZwZpmH8PyE2WoFS!dwez+La#nA(PzgG$cSlynBwOu{|qeyp@| zMi@KaB_)CjLxSmQlfPBH|IrwM1(89Tu%C`dk$qg5Z+yY^zV-1aWqPOm66W1Ow``@X zseQvEcK6L`%)Qcr z-M$?RuN>8bY&I6{K%zcFq_W4CjBf&{(PrepQGdP4y(4O)y9Xm6I4`#ZFN_d3XuZ2(h3Bzi6g8#?pbuyYF)*Z z>g3k3ZqU{3w=~@Bw$_#bq*uJD)rQPH-Nxc!eMrDa9L|T^4Q32ImIw$yB`Zq-1o*;O zY(o^LB?z><-U*lYPW!Kdr(TO&zS<;P!l?|D)M-z$09EV$>>5N7y)|b2Mqv1DV1B{D zFXGh?-bvjr+}itIOMbbhR|pdvKKcF*YrKg^tMjA!mB-}|0nZQZf|Y5RJLMnU89SEg z?M~XF`vT5&rl!1f2=s4!Ml%Ko3`#4i7u?3@>uv1!J5TOXTl~Ghtav^!CetwPS*n)s zb$)w`*C{_9JKj3qG>Zcr+fb4XBGfx9u=i=`%qUnHTyIULIL5~z5~^05zZi)l+mr8S zj;X_z(2gRb?~em+Nk%&5^}#t434VxYdfb2I@A^uR=RFaD=U&!p-X)3NXdsz*efW(~I)Cta&XE9YD1#D)aFpaUyi$B*0v> zNTskos$OFZV`hYE>=4Z;Av$p)AQ@_;QfOaTk8!UN^JW#clV%hboj5*_3?&lqk7y_K z(A^_Xt|BS@UVu5%!)xbQ4vtd_q6SN{wk3BVGpjo7Y|v$l{;lhx9A!5oM@o29RoA^; zf63LdEIlZ;?wiq}23mS_&{rntKvM(=@Jvxc4QGoUR+Zkz`PvhOLYzA=tQF2EBS?r3 zI?!kHVIcszIu`PF;58D)`zlCg6a*nIOwGa9d>9HI`w*CzP#iRHAGQNj6I6W}Qxj>R znB7dEZ6==9SG_4dw~i)bX_)-|nz|k zUBCW7fB<1aQP9Gvs`~%L(M}7d`G;A zQ(~T{v@$+9wSPPYr`_}|hMSyIJbq;C+di@QO%f@mPkFZz$}42lYjGHRk`WGX``F_* zaipB0@@~bRJ4We&mFqrPN87xsK9yV1tlIaG7d20Aje%PpZa9MA6 zI66IVO){{z9P9+YtBt~+;-wiyNY{@FB!i1&{?JF&ZH#Zk9AA}!6V_+hZOmuHoKuCZ zq8UX)*N+J#LyW8r>D%u%_BCSeuEI9ajAEg)ADi$cK=m-#O>HG`XeUE|!~f%!W!_se zzJJHJ{rGC4p^r+srJ2~SowfJEtRe=JHdTJ8vIq10TosU&&|64;t|jHG|Hj}gDZS^W zgcrcy=z+!!P$A-<5$H&NoF>W_64dU%3h013eRGblztj3C&>i|DfWFb0K~!>iWdi+} z5O^l#FE0wd^d?#M!{wEY)ML`Rgzwl~+T#_xL;Wn}VSnLKe#Pwom^RW7d~jGena?0t zLwh*ebJQf+8a*;ua^SrB@Y9)FpIvS|c`tNenR|gT-Sh3kV)NwHgqBO=@q3u)^pj?5 z*UBnYv4!Ha?rz0f%Y(g7-&I2(76v9@5oqBs)X}KQh!r)3zf=X?*1UDp{6U7~`&J!I zvW)nXrZ92!W-W1bkdQHSU@rnhBqkItEgYLVnq?XB1o#)#L54wUq(@C9432%n&jr<^ zN6WE8Mz?2>(L~L2Bm$?Ud5jx3E1b*OseAh6rspd`(H6r4Z+aW2zr=y&rN%zcE%3Ot zb|>bu-MC1eNg+Z|V?rs@z~!l;Nfr}-QWhr8m(07*ju4U-Bl;sENJzS`P)r&+FahB| zza3<$T=L0HH>0V${`gUz+HP&mDRHak^4VxwB^}#7)H+GIG>?xuzZuS@lh`-oM8{ zDVTOV9l8BWs7ER}GTw?ZX^>Y~_!U3&>yC_)lLu&9lPlK(7FIwwK=<<0%e;zRk%@xR z(QXD#=YqX{Yoo?W{#66TJu>ZY+%?#kT2T>n;$pyD(nzJazM?^6QB!8z8f`TokFS>p{pb24QnireFz`JTRY$w$;LWpe}V~Y{9d0@7A8>wDe`I*=+)IaU3`j`nkr3@2w8s)`SW>)7=EeZNd3r zt3BLTY^IQ7Ba2(*{&-EgUu4u(jP3;hoFokgnNT2hXpC&^aK1`$|493}c}k|qIE(gY zv+-_I{7%!97M4?*U@7fIwYq@1G&hyb1Q0%hzNQ1pX2(QK07y>4xM?ZOAdZjKDygyW z(tPR3qX)mAV=HA~d)>EkGVU+F>q0ItkL#)gd>)u5#`R*9&AkV>)imIPXu;4I_G9WQ zZ^t{Re?VWj~`h>xLN<+gzm?lWc&%cUh#HD@gPFj3I9#dWGkC`qCQLkG|^I9OnCyw3&by; z2IMP68AvFMS1_;Oz*SH3%2RSF8C&$#D?fgSS2*ApPg_P02WJ}>OEWW97slTn%uIh! zUzUESzM$P@tiZn|Rkc)`wO;x#=kCK1^Nwzlt5z;h?&0(y9ZynH#}caJ?e#hL9`DfR z$)Lll_0VvM z(n{HG8C)@>(7&iJ1(y2-IXEYR%CZ3D(`nLQT}Ofh2JzOrn{rzo;)R+6O?< zsW)Ki3xawnVKj&tOnq7H$VBV1dB0EdkMz0aL1Qk(A^jo^!h;ig$kR|^{t;t!Fvm;7r%tIgZDdGT|pi}Vvs4- zG88^Z`TLE_dg%N7(@p4{9HV_#u=H4R@^jA3MdwKZDo@&#`H8Ggq;rmtH4*7M-8kxI zNDxM{Rc}XC>Mr7ElgBd!*-KN!)^YfYBCD@t;GSixicj#31!X%^2a}HFKo`+yytQ}m z3#}iq>8Y8$YMEReexlPGT^&wBCLUV&w0uVT`E57zkUTPUclDJT&ggl^%zE!eT#a3} zu&$*-7b|+pTi#U_G2&y#HUhSwFe+h$I-tdFeMsG=$uyB>K_teVqCmcaI3C|ypM2<- zNdaqLyoT-;bK0M)>f?GSP<{>@IS;LEte%Pr`69hK3+pyT1m z;OEO)r|->rJDB_OypjBJ_jom)74QVhE+E+Me|Nn#o^P%j(czEddl$3xk~i1c!T)mG z*xB)P+nBXztgQ31^Kn!P=r&@rc)`hYeGGNK2?YV`kFye^tF1xl$Lp+s-`rqH*ld3u z`#7~T2%5S-N;dCwi(EgoDeNq&d%h2;czj~SjqY7nU0#XJm_v4za>z+;Fm1E6)SgfC zub)HJ*O$MS@qexqe7PEbNq@SGeR(WygAC#n+VY_Cxw|)8;0m0-hWEV%LY`~P0$nRt zpTim(`Yi02tu>LBfhy~p?RLzU^u4ng4?un=UPcIt%?=$H+52M^Eoj-^@dlKXFvM2) zlrY*fDB0e8IY^4lC3|qNY5p4B$<3of%0PG3@2jZp>bqZo$*+LVkhJ22`d7dW4(tI; z0F9eBzXDuv;K6ror{B9naGARRzD;RMivFmcFRW?T#Q)KITkzCgT=$AuFz`4tk99M& zY8BLUVRG|{T@QSz-nEJUM{n2X8LfHxOVzJ;7^?1=@9qJo3zh} zMAYhQX1XPRYn1`6l8>Ckt9cCF1BO%-R$S6TRX5e82;YQ1kk*Ga0F|ykN$Q5JzRU5* zUP@Di1s&dgl5VPMdx&;DWZ?El##kj!K8d&SkUGvRaSC!Ybed;%k8#3q_5Kimt)M@W ztnTd7&bS`$bnbTy|I+9ZpulolO>_)OM!KDJqJDAJyDlCvqiMwepJ`DxA&@Wbq3$}w zH|X%?-45SBJGSDoe)dT&{6^TM5U>&3nMh#zaBLyK?Jrot111gq%zC*8H~*CbD8Pr{ zN$7=lzge)8(-)8b3O6J2E30`UueEq4Cr)_AylqF*`j$Vfud*OPw}q8nhyv&4!0*D?`huD`mvs?CMt2HNBdB1}*pGvKH`N!Q^QY zZxuFg~e(NO{~}tS(RDRXm28Tl4lAnq8B1TV<>pF%ZLN z@#Aj5))ACR#BevHveg9Np#C4EPsC~^CETPhlGFE$2sZk|P8$a6-U?59bAsG6X{$WV zMTciT#W0pe zY)N4|zUBT>iiX5ot#56_)a&|h?c367I;|J)nw+;vzI+m2)@nMhS98k1?V60=Ds%B4 zIp@Scqz+!P8fg~Tdu1dE$R%_STZNE*y7~H^bllE;dSy#uRdpa%-SUgn3rco&ojoIB zdj$0&>iV{r{OT-4`GGvQ%K*iWvZmp#y=ZlA-Is-P@oE#%xWX6$h0R1K+@r!I=WVQ< z&thziVL;&I6sfkB=uxQXCd>lgTioINgiuxIFze;uTZL&4E!r(@pkx zf@uhcqcX)SN3EIyQ!~B9;OFOTxlvy`60{M0X^s^pg(TJn z9ii<~BzkcV+We%WoJf~@D|(elEw09ak+(nof*&^!2~}*Vv2)6^jfHjxIVxMyELG1` zoJ27@oTr_piU*=$E>;Fv+bEk+%#P*#K5{ITcPM6u@_5ozL!{i8iz_HFA8IrQBe;vt zM-w+wcJf7=KtHf+zeEMAzZ}%_zffR#+Gk#se^%EJ|5RzPR(ft|1Ep4A$|vw4RdtuN zVem7n1$rcXJ4-`CX#TCxv`rS7H*9Pla10Hp!%B5JoGtQT02paa$bWp#*OOZwB2~{; ztWGhzl(&?k8ZMQXs;UE`VJ&v2m|e@`Nl}#m(Qp>aQ_L>r(a4rWO1ZHYFC_OF%Y2hk zvNd9yWpAh|v#}Mf888_sPZl3`Uj;_cuhuk-^a_5~)hMWESLqiN5nsc|V6|pA^E_}H zqpIniK1wz!;Uh;-VN<&2&eT3YchG5ha^Ke=G>Mu_mYTRPkIHv8Ug6(a}ves8p1e$O2&YE;N%?XFkMcDSY2Zn3UG3gbeKK|B!@_kMyjX12AG+EPKEt?%EJ~r?Bny@@dvu^ zq3di;a!lxBIDHVKHaM2L@&fr~0ZF_nJ%Jw;Xz(q)yA0fc{JxkO-8s!-byZg(NUG{E5e*S@tq!XI0(i?Uzrgzd5!in8 zwm{>qS)#&nKil2VeiDV+F3%|5cpXV8I~|@j<(&EFu#(f>WCsn zT3P|h>2j`2NF9VanGfSbUB?xS1=#C{8aTAfLC3Z=?Q-r_LoxkC44jAZNv#*Q08HRy zA8CuW=#TNiK5qe3E*h{n%zK>M_jaj;43XTo#U;_G34(AE@{iL^8Wl+)*ck9cc} z7K_!@hp|LjS^$pebArWO>%yEP_e}vA^f}$6Et;YlkzwuXY`P&CP$8vDL>rFeoW#xO zqSewBqH@`TVl-N&mhKwtFiN@*Tg?%$$IZ>o!AJB}6NWPIbXWch>W1g36#Vwq&_Krd zm}uj`e7X=tH&vLV8f=l%MLV@C2$`}{nyRy1)8@s&3{x)g4`TnD&Uvjp`Sr~;9foGp z{VRM}ufPy~gT}8xqUT?oRoEe9R>fU3^cSaV`mK|{c0YttE3X#r^^WDnmMAFo*b{FK z_u_0IwcIB=9qm&SHJ~{iV(6de+O6dX*$yg52F#|132Ogj0)5 ztv`<;f62GTxBaBj|5!*ri`D(HifW5Ci@DaU_*g_1RfRDCCud2G!Gsqod2pHF>gvMOfs;$5OPZqLVhP%!I+0YhVeY`mHBuf;QHe;Z#xQx{F|~iT83RT+2F8(u~pG%X7pr*FJ~S^2ck5- z8ugk)v0RB>twHI(4SE-FivtP`)U_NFjjgt|CDKV^xF!Wk!?qe&BCm)dE$spHbUFEA zbv0pgz{&qh1%{0Qm5cy-bUCGBy(hG|PTeP#KiVk$q~T^D;-_6tpm@dbfs+9c5)oqQ zt^0&dP4u7MN%Rykkfjdm?oJFjNJ2%MlJi(Vx$LJaz4d7|P&X;E9ad7%ipjo_W&Nmz zvf7%S9(k?SN4R!Kr^X7hwpXT?EyoSUaFqC_zj5Wf65&cYWB(*vL&@rYALE%phIirYSu{Tk`z#mP@jH@$(PO3 zGHW?NNHIH=r~O&g24robOirO@t!yV( z@-1)av#Jm1&`_B`u4E;T=d-E>sD!DQfns(x&)5Qp%ncY2CJAj<9r)Cf)p3ID{7C|e zB-DnwuAkpchibbnf_k51PhB>B z8IjDiMo?p*^QuWBv-4G3<>u3t@;2nDP|FfVRekKc7H_~mu6~2LM^OQ9K2oj32A5cm zMU%z?H>;zr7pN)-cYd<%h_k{&tk!fy-tc~n<5-ki6m$r zG-sLNNHF0zfuc%y(X|5iXUk`7O*07I{+q68!6Mvl_W~la};w>s2MBgQ_Rlg z4a=5fNY!%|qfpFF<_*i0fTY|wiV;AE#>#qfH16hX1oJ~qii2R`i+NjUUAqJw?Sxf6x-h}cm?{tf1Un*nf zP@l=2HGTxMRJZbYsZwC(AG{G5Bi`e}68>t=tW&umHXelOzg zvL|=ICTn-{+GIC&jc1ciAy$BIvm`ybrMsvV+f5@kX4SN;=v~jlP-VXSZCJh!G*PVD ziF)e8)&<(_mekapGK0R#&G9G7gm8^kzvS?n`x&D8^i)m4oIJP_GR23AA2v%weVnV; z-ocH9$z4yc)_}Sv?P{>6$No4vI*t#kCDLH2cp&zMX1(X3S8D@ba87I(^J*jl0AAgx z=mld0TxTOhvf1Xe*n3^lR@>0{Tr6-=x|CK|igd8uWOD7zX3C>mMss-p7xCbdKF}2_ zHTtPiUK^XnKKtc-93CrL#tR4yR`Oswbk037oyquD!T1f>c*F_Nk>Q#VK3q@>-}zlC z6dvKD+<7L(^cWqt=yCb_<0|pz_6t`2EOY7E-o^WT=r)iY6>V=ucHfrYtkgRDYsyN0 zJE+%Ztggk8b66CmwbM8iYX722W!}eAYCL!RuJ`$DydbT$lL`MQXW&m6j#f&>b^~XX z{jf&DP5+zsnE-lA`#B$~`08ZQNOTKG$%Eq%A@{_1MlYJ@+IKPq(~SwEwy6X%Mr#L{ zwfg}Qc|&ua+nn7Ezt(!*FIFqqpVs_po5|H((38x3mH^gR{SG<`WjZynnd;jOgX>Rd*WZIg8M(t`s;$kd=gt}&_4!fc!Or*62iC| zp`*HcyeZ{B-OJtuCCU-eU+7HfYNpa(cma9*u91@#E$fhAB97?uO*Ql;NQxsi)5}e! zFQ%btD2Zcnm_ldS6fcdgdPq6V)&^w8u{cJtyle`a_QMji!?8F-!Mki4BIjN`RKbDM z+GOb)!HpiFpgRsR_a%|>0wYUvj$MG=Ec#ZJ*9s%c+K5+R)-F&Ka8ILN2*3G4X={}_G>C(lI^>k|Wso^Z=YSIFN$B0w-mflVxN#>E5btVD#7i&(6Jv&UH8H zNdr!Do^Gjb0EyuJZv@1fmY>{T*Z;M`<0H3J4^A?j>{@!N;FX;3|4wt^)Y{_Dx3?** z>4jl0w{vKzZSs@7yWWv1p*-bAwC|1#wZ(7(hK;R&{l6fruoPQAxI!P`P9gtBp=dPWy8srNI-}v=Btg zK1RiNhj4&2#hr&Jnf$4OSnpj+;4hyQS41nA1C%NL-@qihU^EH;J8dEFz@G>Qo>A4U z$40AYm_h;#W{^~Vimw&o-0|#g z%B}qlwQD~H5&pimX$W^L#~!h4rmZ19e^yw|jG-}WI9B&VRGV-#>+dC^g!nrp0^@Mj zti4Uk z!}RWpk@@GK^S?9pe`mRTbmElg8LG{oFu0R6I)%a?J=ToANoYCFPE4h#y;eun3~(u~ zT^%G?Rkto7w9M*mx7tkI=d-`8t|S$^nZ{9ayAyhb%VU-N>vPUF-R%kLTFo{cw#TrV zTaJA&u7L8u&)SD-aGjo%+~}HR%$OnC-*~CX7pP7AtXF9{%ws9cc^%#DGFsq1Z^j`# zjrsI^W8i;xMfB4;$L_|f{8Dp2z~Q9e#W%~wnkZklzkwh`<5))mAQntf*`cJRnTSYR zKQ77;FL-}rZ*T+#x$M{nIB3CFYPD0hE>0+{3fS7bj?w+OmE4@E#pu(~dUb6i2;Ln& z0j8~_oz$$=O3FT(mg<|vl%6CNH|^IpsOyG%rA08+S*mE6w9AQU&8nqEbXu+!7>_zn z7Z{&qg_V*BuzyCND^P2Mh#X0Tr>mHkH`EG{H+1SlfrylG$I#TR)iu;Q)-iON9kW}( z(kMsM)UA*Fx>=$JodiA7A2dQBb;7(~KbQ3cf4&^Ora25O6zP#@u)f+!RBX*s%4PJ@ zKRGWlVAjbrp3o*Z+N7q*Px7LbH@p!m?V^PYdN}us*ehufR$lM#>~c-TI}iGOdd~;0fc1fPn15g9#3v}7_}_#S zBCON!fpkUBGP-XN1gRe3D&_xeBwL7Z9xU*~Y}AWfIL-Edv+yE|S_J{d=)XK|)Q??i zIktc5WLogCPOiKXFxez5Hy?7l5engxvU#ckQJx<$Ah#93bJp{Rr+;#338Srl53XP?I(aB?Zt_A&x1s%}M@a6`kDE0}Sh^VFeBy zTX4R4)fCd)l#suN2^q3}pPOV}*O&9f;u^k|vk5DS2Rn&}vfQVN+^3G*r|jIP>RdmD z<36NesaEK*RN-tKUAEv-b0K*}ua!a68Zk$s&1abW75|^&eNP{f30CkDZ`xuDHTMOU zAAdeB;U5geEsWBB==U;9{PMc!VA{tgzz3J@KG|U3iGh6EUFXV|r)hQQi@vT^vrGWg z0H)~Xi86D_xlzuu>z}fk+SSjB#wkRKY|5OZ%DP|;^VvO~chs;F2cIoC*Zh+W4CUze zG8~N5E-}_7YnbiEgyqynpbnM)2<`CY!G~1>Ogacxyr=d?!4ld{p7yyrvA`8p>@zTI4cZm#ru@d*M&PY6J8&sA|Qi^u5Sd~ zCp`&-KyB3DGsqqsZSg&Nx7T1Jb{8YKobSUAl4*J8LYyD$!NKN^Gf2*(-}7+N*@Hz` zn=D~8M&GY|c2Pt3L4`w#MFlIXh=wFHif3RkAFiKbXG5|9xeuUwK2wAD7c`EB3Yt z1yP@7dy{cPPRJKKa~^p5`JXG8wTd(!U^>8N)G!z4ZK`kyjxBqzEbD?5jL+!zFF5$@ z!T8T;|C$@61>jbvx8ExYQ#4}l?S#}uOreStBQ@r8dNEEJmI~;K?zfG<@7E8fmjd+r z@xgFL3os;&5sYIbh-G$HfFiW?BK*jEd}35J;gynoin0Py+f$v@L0!GXa0bk|9u4Va zOot~AEl}-}a?!Kh=CA}W{biwx`tG%>O3bQVY$#6$Q4w!Gx$RlO24=vvN_f!%{H?)w z^TH}--Qu3_aPh82dR3$yZ9BE(NHTSe_{P5G6G)mPZ>n?DGAPUSw$4=9FuKXJo5Z`ZnNLVm<*m z`o$gP0Ah+fk5+Qtj>doeKgH^Kntzk42f!1!uWWSdeTw$%?7&x5^TrwaLeUlQ$r4J! z)?d?hnOD!2wL+OAx^Cf)|319VMj|msL^Hg+YE%>a-_ev-pun1v+)Rq|_m&@r&NOXd zKiVVCstr3b8%swBN}Ed~mqqNmQ=fVLOI4mz)r4SbhQ$ZPfs2NCZKaXXG=dNFoLb{X z5$8x--2I4A$=H=kn)0rd%xi|fpe1d6Ts@wl=Y(63{Sr|wzM~4*S&|-pUP9sZ=VXmXC*>xoX*}vH-6g$-|P1+*3she_0PuF(Rv?u zE+n<4g45UjFh0Lmo!heeKJ48lDA<0zcJlJr%O}t?SKw-oct>utbhaH0@=KHHTx+1@ zh`0x}#KL77UEyy*_sc|SU{@|{K7Ysijpot_rn&qg*Z6|HJu9_izz(0RV6Rdvu#4w3 z8JvUK1tt6G4+Y#+_j5{Lw(+d0m!e_)HWg?@hpy!D+G9elWez5I0osKlcShQw9iaFZxCmuhcJ5Z1#S05!vw zs1*gZBDC|QxEQY6!&gR!;NDq9eTm~ND)vzZT3eId6WcT^UhbC92=Q%Qh868$GcxHL zw^?sxb6v9c|7S6sVk+H? z5 zC4C65GT%%)%I@%@4)kJkZTqW@R@?RBgkv$hPOEM?&&iL`#+BsQ<4l<*ZC^dZ&k?r% zdkO#4j907gSZ7vmQNY7{PZgTHmF${7yHo z{h+;Ah+gA493d}%zryEe!HRy9=39{3CCi#(1A{m&rcGWgQFGripUt(`?jddb)A!8VvNDP=F*4o< zOU-|{>*1h}I`dC!ng(r8()HwUES+q6h`J8yi}9Aid~Cze;sa1rMpHwP=-SfAa=``h zU8Ta4(oxqrO;M`=M6rvR!Sn`V(UHvV=tX_3KRzC9QS`1W@;BBy!E)~)^va7|qD z-ffBf@ding-oJ`5YFLa@#ui*}UUiQ2AwVimSV1=wy!d&;kfM@YjtRe2KV=JUzqTO! zZ?e(PecHyW1%3f-`j44q@plI;Cnp8B=`=j}fs~ZmCC#d71A{k8Jc*<{YFLV6ni;sKx1FDq zRX>H|1lAH#g)4B<*@A1$6)j<~M!%QfOrV#p)NkuqBi8o^X&m&y*W)5S%b*cezp3%a zj{oXf;c?1h8Bm+O=f`;D|MR0qF)xZ2mW%u8S<sxTv+>M1`K;xT#R^QEupn#D{<6C+ryTKcBis`R-tF+PFt$-j%+opIcwxJO+ z{Z3y{p&Bp!5rFr7ROX`V3$s>vkIL*Cq*xERfIhp1-}pRUM-t(|tk+wEeZu-VRdx?x zswE303M#i#Q}x=;uhpT_qgrXD)se-lopASQlvPYn<5pxtCZrOhL;Y5KBfMKB!~rw& zPx6_@t<;7^2o^#o)nkdHx8XUm?AlJ8tI>aQ+Y4-qVE&m$~;aW}LyR{HRQce%X zbEyw)k?c<%eA~kXa;KZ@atqR0a9gJRVK*txl+35BDz~~Dh#`!a9m==b8+aj<2nv`T zYMupus5C`qx*L=sS(y4%k2z7AhZV83d@>um+@m$xU*>fZiSPL z*4wS(=j-C8azUE;jnImmxDJa}&#Yz^4JL<$PkYZC%k-M6`LQZ^8Z^ zf&rCp@mXT`Ji z&jan8+qH(G26Eu@{q=jn7PsjXpIuptmq>T_6o)u@H?r{W041c9twT*2tFo8xtk7Gi; z>ameGsf-W6av^SH`$3~hS7Cz@ z^o}Rk^`GKLkU%(l^KU7}N8;K4qmPp(w|+Q;L~FdKrb`VW(ZbO*$$`!=n08B&&Lb-O}gcQB<^ z4r=LwiYNK~Ny*2wqH!Luiv77aV?{JWaDi^}hN9+HV9gINARS{+@>;#LiHy^MY&hoM zTIlyQq5a-0e$go!BVHA53H?btF4Ja1l+E*RLgME-^oTx40Vho`fgwITqhc!(mJaF1 z$`EJ;+sVgYMDfSOlf+tuaL=@e}`M9)_a~|c!NvpwA)V9Vq0leJrd7#l8 zY)e{R?#XvwaYZt1zCz3B$^5H(8P2c37~04ck?vK2>#xME~cixaw_KXi^IRew16|7yyfzx$IdI}uzY zU8!hZf8EJqo(`F_>hGWHj*Y#FR_uT5j&$23{&{<3+hihIOyDEP zdBd~sVIft;?I*J@-M&d%5c7=*yS~O@rigt`;?JcaImf%nhk~G3nwy)WztM^Pe|2I?>lHW6o|2dz+@c5+8~c)vSz!hB z9gS1kt+(M)QG&Tzb)BPkvAOCWb(7Ic5)3*Mh}t^6 z@;b^0ue;PMPa9d&Z{@Q5r)L~e@8n!Mtu{F6p9&J!>#ox{{8ctN^Zlpi9EKkYGS=&E z(op=TCmn{L3P9_w*J%iy2b-LFPX*^(6TAMpOW!;?Wfo(kwbo9R=jRq=_Wajd;LIKv z*e!&kxps1$K*OfVvG1cfvCs}BX0RVj@$Oq(_R|JGjS>HK+xrxmt{~xtjyu@?r~o}< za6T7w6C6_Q~})t8zQ257HpXKe7Q!CtOqhHap!fj zz+2YT$egRY(cKkFu#l-&F3E5kUTl%5B}%a^^2QIGin~zo7`JBC$$9Y&h;{Jcw>g|M z?H!^W?Fy3Qc`{8)X)N)9oBms4Fdy7u+c2>VkrZfDL(ZN~J6iX?dWa{2iq>@eW@iX! z`HB_Wb027KR@l}@S1%{7;ciyn*u@%-P2tW;5k=pep>|q1J3!PT*@_b7qIT@gz#{y; z^qs)z=SU0HFT^J?IS77<*ZV$|LvqPisQJZJuj<^v*u?^iuBJnv*r!qyxA?WP3Yae6+b%_ zZRp?4W;|P8O8i1nmdQizuL08^K~o-dYN!GZu#{L@GE#ykqZimoih5i zP4K8_RXIT+aCGo--rGDBshexV60zgVYSE2;CJLxhUG4?Op=CI|82z11*I&0tXv}cM zKM`ginqv~)4@N(oXcAz>*$-UwbC>_9kv}DS96H?P8n8)?vX`?=SHbH`3PsB}f8G7Z z((wP@1Eg+-0$(5@^FIGgF2ideUlaqXu$rq^krrk(=5uj{mJ6*PR+gg7vWy_)9iZ}4 z9H%rq4)P9DAI|y1d)a-h;BSrZ)KP(=-dw>=gSM23=k{c2Gs1hl@$lcpEXcPv)On0~+hUST7 zV_=LfSl~dh!YO=6-P;Ce6sJKQQa3BfrD}?TCB{YiIASN%24>GY3 z++GQ(w1i?nn%1E&ym2ZN+p@Qop40=S6-yIC%yJo(KBdqz$Iu1xaJCg|kAh=kW)kkp z5jmZ2wqc!1kq0t_S(qOEI}KP-kxYSKnLn+M=TJ?)@v^=-OkH^m*2iT0#;vPe^A1P! zD)t?tV6d*)Ve1{_=Pp6*+|`YJsNw{CPo14h#$$+MN6YBlY@Adk5~XeN&C) zMU|se|M#Djyf-d+QZS{WP`_24~>#Z~I?ILT| z+h*p7)}fosj_C;u8r`%5mWp(7Z48WXQ3jU)I<~4P`!PhyQU8CRl}EHoC6vmWcLq0C z{`;5ef^`r6_XJnTor5Z?2HuJW9v>$kW-40a&7vS#`Zp^~6+MsuE*Jn8e1OYafD0YK zg$>}s2XGMuxX1!rQ~@r!02fn$i!H##72x6vaQOgmi2}GJ09?`mE_nc#GJs1R;Bq$_ zDT=50@~P{=KA)$0AMsK%s7kL`p6=R9C#&CgU4bO#xhzJ>s_{4u79_`jTvAy(^vu*a z{ltAa{&;dcih%i$h)KgRg|ApUx<3DZNP7#YxRz~EI}lt#aF+yvyE`GloyHx4ySqam z2{aA~?%Fs6cWVgl-e_=lhrg4v_u2d2|K0KSxo?ab6jaxuR+r40v*eo(GGTmhbYLzz z2`)M?C!GW*9hieof`bmsPA9=m2WF#_V50-G(n+w=fm!GzSm?mabP~*TU?w^VCOR-9 zodhGDo&Fa-?Hx~H@+5}L1C85eAK|J&V}Yu+r78oyjHuL2sC&ZixCuOsm(h54(a#%I zuITa)RpcNAiH(YEt{ES8g?lD_!;0>3{*w<%4~>QUS1(+bXMBAjSDv2aPHl(ewnsnT zo(os)37ZhE2LLPUF$gC~-*=OUcaunVlSp@y$aa&+catb~lPGtSsC1L4b(3gxlW295 z=ya3lb(0u$lNfcA7Sa*}yb~k%cKF?7&70%^1+D6^-m2&N@7zw|N zHeD+G?nHjNivVD5xqpP77vFH7@3xC_PqtP0W{x}@MYvtgs@DW5NN2p%?`Saq`YHi^ zHG#gWKwn*;F97JP4fIt9`sxFHm4UumKwmYWuO84>1?Z~-^wj|R8UP=ZfDf9$2UXyM zF7N>WRCeEENtBE{2!41veq<_OA$Ce; zV#fWXt@%oOaVh&yL2)J3j&dD|HZnMymtM?KHDaxhsz+HbEN2Zz5|=)*UzKDE#7nQ~ zs9LjD=+dM75Y~kh)`b<;MH1FU9oEGf*2N#zB^K7D5M~W_qQKjuXwmW)W^_eM&-kj1 z9vPJP09p|7I365Y8iDbGB_^K)zFc>#u2EQT;UL_6AA`Lbo4Sq!$ zyh;+BMG|a868wrJc$GLfi#XVbIQSKD@G4Po7E!PfQSd9G;8nulEW%(T!eFi+w07}l zKNE#-?n&MaV^4P|C#N<0{q(SoAjC+gI}jo*SQxTfa60?(u%f`8Of<%aIWM4Sr#lwG z4Inwva=eyuM3QzSkanb%cI1_Il#+JTlyT{4DL5DD7A%?N~4E*eC5cEA6-~ z?RYKih$Q1kAmd0Y<2a=JLJUBfyL#^P`a4EC8D>;2wDgLe;C1^UMgf4K&{hWkT>wC90Pq6< zs09Ej0f3SKpfCW)1pqPvfRq3r5deq@03re89t!FcdoF(y-kiG3 zit7Z`wQdoQZ?EyqSHmA_Zc7e4j|NH#YU=J2)dj)|ra}Yks)+tquDG~O+4k@f3gm<%+r`%HAj%_P# z&0b0v)GVjDA+3jK9qb!6Mfqpcpc*HuAsfr-gpAFLk>P;HPMl8Ly=-d&@@1t-I$^bW z*cr~x%^E`@DXD1hU(<3@(%Mkc=2FrwQqtm3(aKTLex#ypR#mDilEv@%TyqS}9F54V zh|G+R%5;d%REo)Djm>-+mw6nQnQVi`En|DTSUZv?T&IU9xf(wcs=eG7tP{QDblQn$ z5(2u#$FYB}G})F}=r5}L6qNl4FaCfU22H(Sp&e#~`|FQgga1P=)nfl(5Pc+jsutyIR3QdYRDG>Nd;s;%?e#nQ!0w)5A&yIq79w zm!I1#%Zn~`keS6Dq+w5bjLn0OjvhhMQavTRU^?t-Hs2zJf6mG>aUlzzbV2~uz$#It zc$J`<2>OoytDxx&Tyv1yqVC-f*61w>=G_$Zuub%~LU)Vu{n#X*#;w0cAmVehY8?Ev z>}G$`%~6nRkT3FT(cmKF;y7mQW$YSj?3z;SnnUbbbnIG1?AmDT+HvgK%eXbxxHYA? zHHWyh=(x3txV6!^wd1(8m+@eLVF)C=m=4eHbh>eLSE)C%g< zgm+ExsyznMDTlrNyYTe-k7B_(;98?aTf z{^spKJSlL%`+$6fE>xg)) zE4#HBkayp^=fmS6H?BDw;+OT8s?ogov``WXj~0r62IaShBB6yMqlKcNg`%Q?$&WYO z`!7Eb8Z%S^UtyhLC3YJYb{p1r8}@Y@&UPDacN<=J8zS`>67(3#(}i`>rKMa#JKDx( zaQy?^u79`w%m-rqqau}3mptT;UeZ+^nqRFyd=`eG0sZcV@myC;!1@#lcBbLule}w~k_(X7t)%5SD5lpsc?*kBbZ z`-y%BK7^O!tsDKG-5=O~N-}cy$P{Q2Vyg{5MTJ$nMAh8G zj7zu|u0oFQv`=jogO8q7_`v_4a3U1|ytc@<&z=oX{LeTMw%<6BcpZBvP9*e*=0HTO zmBt+Q3lgrjT1_%{Rt=;qVm&~d#JrZTbl!h?D|NeDA`7!SvXZ>*Xp2YA?y|X}R+XN* zAIGycoKYJqrqU@Xd#j+SgFs7WoxLlB)-vpcYF5flFGgHiv~^9ks?J+lwJ>#UjW6r8 zRoNsAI;!748aTb;n?9!f773#2(Q5gOA9>Um)5XT1{9fwk8?=}$;HZu$ck0*1Kq+lQ z^xLU0HBgSD6LO?+!;TlQw@?w*&uzCK28wB0?4 zOXH;k(8gF3_bKGuO#1D2zn~V{nS2ueg%g3WI|oR{KM6}ORhZvMY91?SPIk#Pm{b{Q zh3PL|6209^N)d1{-@M$tjmVGVr*Y;>Q}vK0FY+tf9Pdx^!*V7FD0ywp?|7?482mQz zwPx%lU9#to{Q+~9Li4G(R*ps$rs?xigLlW0K|?!{uPz_fHL&Nh38DNfq)Kj7vbQbP zAIE~Q;&@QsSLbC|z*L)Gk=3h;q(Fw=vy3nv=C-OkYP1or(C6?i;oPRoLR4yXEm`sB z-xnq!>&24V6}^sjn_YSl8Y(LWqrPJ2Xr`|t#jrGU5NYa_@OrtX%V_21newe6V<$W=NA z#+6p~`q6bKV6?^@HSH_R_wZO;F^KlI%Usw&C4R^u?rM+}C#JhR#S?Lb|8!@O!8eiL zz@gtia-npQYN^`1CmH!&-N~59s)=D2zxZ9>Hw9_j*HA$NZe>u@D85^Bi2`|9nLR$( zP{4LY8Xg#qc#lo3X)#c+l~8=f@OxZ8fH||E-p7#8oj_>L|{piB*zaKGp zyg__u%SbmM_q)Dp+j(jcxj*)&{N6IK}0EO;MMJ6<;bAV2pgc;}g3BLJGbV5=2KJ|^YDa+e&* zal=tNwa2hyW623Cnp`H^=B(`%NRzvh^s(S{@zWmScuFDqG0ErY0`?YBceUGY_kS?( zzrQnhYPp>`dU}w(*z@#n9+obZxw$-^<_q}t2;EY!bwqYJZYU5X;RD zJL+TF;v}V!`PH^e{{Frh8L~iAgJkFM;SXq9-Lcf{oDekczCJXsn=CXhr1;l|L(mW1 zh26PE1SN+`c|Ac@naBptNAh0CoyKE}e%La--h&m@!aVMVZB%3uo7DT?x`*!<+V#ALg!%8#5mCxkTYFVqa)rCnN`z*STM&_}+5vOX?8F^AKgsmDh46yMuBb z)t^ZHpEjV%2=^tWvsL zuTDRsy%DBbrnv!KB3PinZ{l$irFJXd50=~4=ChEKj@}8;lP~Ibog^{LXWJl}kT$00 z%y*9dBEBx}e33vjp=R7zWLHGpy373xl{Zn=IVFvJNtfz=!si4aGy`B)AZtC^+K7_Z zd^AsqZua9;7RaNx5IbnDVzG@;p=TH76+azKOPM64KMU`_f2~QrdDeB+JX_7aOm1FX zFx*C0z9IKcaVP5j#$FA^@+)I_&yM6g5ANfm!6*NRyE>7_UVp~N^^J9!+vU0L^uV2g zv%Phl0vG-7$Umd(9?tu%{Zeuvy?0Id8b1Jhl=O2ldKy)i(myBWs#tZrY*D}F%H|Lj zzFjVaEyZz3Qu21@%Xv=@dix1lJMVD|`)kx2UMDc9H*nL>aQF2vC`DM3n|wg_9!Bj@u&W`pl;+FjpS*mjTf_ZyFYKGz;^XpkVzvxo%C${P9=!8t68O&~Ya{ zK7ptbtV^B5=`~86Y*ou_qr)|%i6(^mx{IkI=*AKO5j#-NtV!T_YdM;TqS3@*Q20@{ zQv&^r+D2oMu6zKxnshbwA*4>;IVGL4ICqmkQbW2APxnnt$YOD>nTpDr3oGK?Gy>%X z)m=aHNmOt5`yH9|8v(GyxHrpy0UMF@)bgfh&$Z~3a$e@{b`2Aogq3T=uh4rEv4CVTO`B}Hd=+tRzx&Js*~OU}IO9y6t*RA4t&KOOuDLQ-I< zQR(|I&}mK zvyJd`oqJzv*FB5=$n4ode2Wu(Ux%UyCn2SG$ypOX~ zlUMsBHigqdt2ew|y3$VE(2-4a)Xu>7S4ZvCy>HP?43BDl;e66DYsAbwdH!V@W?-TwMmjOsuBVjM!DT$GIm*KYJ$nx5| zJOn~~$l$azNtiOK^|Kii%&j8tD>&@cg`ZmK#Gn&qUZIb>XAp^pE)w0sn)1Dj&ph`JQ)7WO zMUYt>@(9aPHgO`A5;j}T{8I4uiNs5y2~lGi&U{s4ek-}ZjWnVOabtR}{9>@nM53Gp zx|(skNh@zQ`0JPVn&FkX21M9pU5PuW$#Ur6g5c7_e1#?u-%q-SE#ZUnhewaOatpA$ zMOxw<=*sBd;okfY`#%R8 zafD?t+W}|(pQ8*a!HE7o#K10Ni82v8AT|gmHMVvzZiI+dVK&<jaslsi^8b)U?M%0{O(rz&s*HSzPL@$Cpqc2n;O;S42e_SD} zS_<_uE=~@D=;Da7vO_`5R%v04nV|k0@((v& z21P+n9}L{09vQlYMFpLfWp;EJd}GG(8GMQJ?2FG2;#*tB-IZ>!@>tYvBV`Qo&o^Y2 zrFR(OTdT+2m2YM8jzH&-c;PsoV6`Q~Oyz``?%iwpk?xaidW;){;^yf|%f_Uhx9Pi$ zc?QyIaUKgYVaXov-b@YN9H;??^Lx5&cA@dL2qh6H7gR&B8w8R8X`^TFwiDZYL&sGY z0e`P9gUHBi=`6I38qmEsWC7fM$g|Hmw@qkG8?V#2eIa9zeV!+?th_T2->SPKK<67W z?yi2zEMrh~u9wi7GHxDgUlB`$MgYnk1fk_Dm{KE4Y5@9pG^K0u72ZtQ>pbe3~kI{5klQh`zdPWH8>={B2X`cW(b|?{h=N-?PGh)KLvoiEkKi{YjM7 zOiud*5FqD3KG_*kL;%eIBItr zw5+mqbX2Rq0zu1%AnMCV3wN%wfFgJ=f4*%SZMi{e!Jp-r=;|6%DA=x|YOdlUdU|>- zC{t5PNv>}ICOtbmNI1A}n**Y`OB&z~5&Qp<8g-YrT~^)$(P_3Oa&Z^r z>PjSU=8FXKJd{rhRGZpMuK9R*$+!M8n5~RrI-?IZTwM9gqAOabJ5?))$t97W#iZ{>ayO2W` z)=k-RN`^#kOaWZE>QaLa&IQ46Y_&5h$@7UxC-pQ=Y!`v|n|SK*y}eN3?FwKTQnCiI z_^QfsKwpV)akrZ!=t?=D`J&`a9slnMT<>ZbeV+l)@FBrE$H`W$EUmvrqu~X;Mz&KP z{mkD~jDA=3hQCx$(<+-nenJ#Fkb;~{M2W&6ew(B^jqPhU5O($ByYiNCn=>_<7I=c6 z)6z}P-WPa(l6QE>HC2SbpQLJ-NWe)}CB+stY^2c(l;T+ya;YV#FpVEu;ahqb2fu1X z2nQ9za9jsB{TJIoC&9YF=|HV4xIdv#7iuu5Hn1l!Xf!C$Yh*d4sg;HItNnYLw$2{p zz;~s{E$p|f!nvG3w#d;i0f#i&l6)H)+iFBCx`FdaiA@{nT;R;nPYTd5X}KW!DN~qq zK2Pc`p^`a^;sQlHMhbCcccA2Jhi6Tz;>y+FbrtJ{ca$0q-WwN`ffxwR5-{q5epg?_ z|F%6p;;6Bj=@D}>wySgy%=Dq;ySmPEQor%W1!~=99`f#@o96Ra=IF;H{U}V!8gxZ& z$ufh!R}=$>vtC-;HPj5dJTT%bo#rb<1%cwM(t>w$r&OdMyC#K&{A)wckbk%CrhN`S zqz(?;-ukDZp}sL+r2D59334J;E0f#kS1XI_S1Z&tPuTlQQKuf*#+@r%$=vPEo$T9% zGfUawSaCLNJ@nTq;Ah*TJ_p5_rP$+WabdW0vzl@wd~Zr7$w)qvMtzo4<9b+1Y0kyR z(3#UW5C8n|jePp`xx9|*)w)-t6>hFJVo^4Q8cFUvTv4qkNH5xoJeMD?D3(HvD3>3h zsER_3ESDd?D2!r?Ps$Y;kRS@Oj^4$~&Hj$sFhvU?A{62Xw*Pp_l~RiANA`&&Z;r!v zB>W+2z_Lc>$N7QCgEj8R0h59Pe|>+f*^esCRKMK05R->wKQu))mreZShl$>|bUJrY zE9=yKibLIl!vN!7n$AZ=p`>iXao@S@kAi4))}*THIJsfLswosx+)_2DfP-MwPZVlc zxiZ13y%cJAx&6Va1r%zSx%`Ml%%UK6DOYp=BuEv9<5eBvMphE45Hm)Er7oG9JT*pf z4|D!em*=cDKQ>CTZR^swV_a^58M8jmbgDdRac8hSQ1Nrk%c=gumtSjM0K`Q>yizsD z0NY^ICWkDYIG~(%~gqNV&Q0HhtE>;&cEe%#xj;4PBunkgOpqOHos=)w| z2B}I%)1w1OgH^Sn=`jFlL8{`>yCk`da7FzTQ(RK6r~qZVkVP?P?}f-gCXOcuT|21u zV8^v^+c0HwwlQ6A8GPOX_lT_EKGA6xlr{@3?Q7B#Y)1e8TXFF^X43?_EUp=KK+AuX zm~>+aSpqnVwJs7}?W}CMv1^+r(&||5LJbrz*;1s-uiDBra*fr`R`7pawThOA218HN zLVkOZgr41n8^;-^gd!?_o!~`cy>s!^lI+*24!JAQ@~l)`?-Jye zP^W25QqHO1g-w?$rKeR{uEJ&_hy=M*p#W8J={e6VkD7`(E}u?Gfn*i)i6^(+n2HLS ztBf0#so~hf*3v!=5loN>xAMSUoFzmaDn{H?w8!4QOv<{^Zwy-E%EioOgYp3C{vZMT zq7Xo70KYtEB16#%98eyBER+baM3I+5pcjuLwuzm#9@LizhR5VgQpEHK%CCVee@O{G zWckTnq5Gy3k8ktLvL-bogmB)+KUP~4cmKXRJFtQ^@5M*>Y->#?I7>7rpo8fTG{>(4 z%|g%v&A$L0P-qAm6wvX@RVR-W9pk8}q>&NAc@!$DF%drAC?^SCX(=||5VrxMwmmrT zo@8v8cUv&~I0RvbDS;z=-u9dT^YAT4wp9(fVh-Y1f)Gl%JG$jRElqe-T>8X_Rol43 z_WXNJ>nD-(U2d;TOI~4MX*$|Gx!H|eTtI93vLv_F;P&~UuRasv?p^Hj2r&6Y>NuLl z!Ek=5?6#tRRfK{Z0Y$+9~C@6B_7uSJ_Lg>9$ z;P;PEvI9B*g~-K7d6Ynq*<$dS!}qV@#GKIY%UN1_mO5^;r`SSnHF=I;HK52mG}@=H z$(w@rUC!iTJE$oMXhFgFyR47!|GX?zXjz)jvKR-S{d6>fk?%4Zo98zgvpKu?d5$13 zFg6-#bI55m8_=ex-J_9yWhP!vQ$U)xV>?OFSS4`W%BJ=Hmj}&%^0mS+T$BMmaA>q{ z#9T1X!yxA97DSO4#%C9kqaHuYzy0WwkXO-HQbcF^DU`3QGH}OY@Vk3nPHM(HZ7}>N zqsB$JcUJ$>Aqa%cnJ<^DAhLFV%#DsP1ab3MU|R_CvI1pUU)`Xs#mO?68ySI*T)nJH zP@u((z12(vJP2`(HiiCj8td|W2k#G(4Rv(>FPaU-FP;q)z!7>5Yl*yrJ5HR7gSVZX z%7R8lh-X%~S6>m3zgl5on^s{_>*-~s@6~0sVPWxU%8iK2r)8m0j+?xHr>OGvtctPt zqxvQD|49?ZcXsMNKm}iC z=ZbRINQtFx4tW2EJ<*Hs>1pI}Gh!y+(WUXTOMZ8a4qrkZT`tfCr^B+=z+N7a_c(fb z+tYfvKXk9)oZ@xFxAL@QvcBce0T*^wtBsH-ZRF+fot&eCjU|ZQ$V-FC9u0rUgVNHW ztor#hv-hgrT9#z&qyoz9p~9g-NC^g&Etk)a@89&@iAgQ4ku8>TG~O(~Z*Ch0o9pqD zYWt=os(i44e8GyN;YM?`1jYsOXv-6ZR&l*m_hk{l-V-)WP?#*(5AJo$s3P~CDA;G2 zzogsCTD+jM-aVR|GmJZYfmY2e)5$8JN7uFQLrQ2y!)TnCR+(z#)~|q8?X1o^s4$;o zggG0%t2<^sh-y zHZbvCL?@JYA;|FdHszF_!8hGjWCkhodYxA^pWld`I>y0J6V)a?&*(H zT#yI1vezbPw?z)t7dma64RU!VXudpg8mN{>?C!*!n>1#86Jahc{odu4RW9k)0c8ba z6gdiPCOhq6eYI^kCuxkrNO!t<;D5qGp;jpG)j#l2B*IY|xiUe#f8@ zYq1xf_rj>&IYzDj*V4N=;&Y5|)%u@S3UO?`uo|C}?RDPfv=HtE=H58up+oaxc8RW+}|%l zcvxS!@hO#W|LWmP9a1v0%uDY%U)FmGudhcSOO_M#zO>Yy*V8MF_wb9h^qZ15qEyHq zq$^_JbeJ7tKRqNUoU$HLVQeBVi`d_y{?Bg}D_Q@Q&XY)7#B-4QY3A1Jt&O^>n_OYB0F**0t()_yKIK+?-`D;v1V-XNAhsP{@d z52|C$72-g9_}2AQA;=-T-%F3z$MMdWcO>xX$b$IS(kmejkHfHSNXKrRNdL91-k=`zWOYoG9Fl2qBm%WS9Nwg7|(= zQ*nE1kkTndP2wZe32|TRspGs_V)=dy+Vu=<6dW0;t zmDHMWtgQQZ=V?D-l)DjL^QhYf2OpZ%Yn0)+8v)ddF|z}ZOEyoNCxfkk`b91 zl0)22CU}!&?5$%5^xGVs=O?^-EqZ<%m1g!5_V`*g$)ji`)SSMayGQM97NaFR_g1PE zNvj%ZBLy=dl~Mt!zK;t2NE6?freuGy%_1lwpnS8&kUz*~$hmY-x*5lBz8M~r zaMtox`<++FIRahJ%SIErk3@?YS!O9O-`zj2%%{o<5BGnC*ChAsByatoMeNQg-QUZY zQnKe`lhf#+9$%kbCq%2oRC9Jrg@8L81HBtA+j{#S7YEn4PfJa-7=>JoUYC}d$jZFv z)y0JGR|7IhO%bEc1y6qI9wwDY!cO8}f{K8xfuUiMK2 zHR~1lHPs?#KSSI_wT29_q=OQu{wA$r4QY@sV^=7Uv81DAf^w+(s6?5O6=E}^;moBS zV&L+q>Scmbs1irSdkU1#WBVEURUft)v)*inFachM`-n%&|B#pGsNYj z9qPmH7(eJfmKCg~TG$TjU>$sh+*or2lsBRE0$F@lCyoH~Cok9|_yljA6zmPeAfO>AARuQOW*ue|Hqtfp0bd6>emTKiCkDr}4C!$=yisZQ zkho5^y%znrjB{q{t`U5sK(g=+qQh02W6qI5;h(0wzEQdhDiRh|0Nva$q?EV$<&uu` z6P0r`MTV+;Z^0BY*4M8w_QS7H*}b(4MS1~7>}II`1an37+c7yhSmzJ6!Y|^6Pd9HD zFBGug7zjlyp#FA^Zci`)foD+*!x#zBt+sXXadG3K7A7M0Ef~0p%+n_tDec(0q&^c5 zcAMN^n?i#Xa}EQVu@WV}MmR=knjw%R=q0dcUHZ`6rP^Vi>1w-JTakzEd)1Sw5cyTx9)HU}oC zS3}o4G|jIIZazqmt|=r*h#Byg@|!MQJ7`zuz%+MyMEvsdeuA*h1Qq~Z5s-(hzX5ePN982Xq9p0W-38(Z{Te0fD806bz@u6HzqM6F>dbg2tRqTE(AvW%>#*o z@<6r-GFX{eg^hhW>btMQPSA~IUV0P(f;^zBJtMPlmSIF`6q`uB)Sn4)_T0de_OhyJ zY&F$Sd_@0R{$P{fjHe(dXZ^?je*H#wB_s5T-}!)haeP$~Ue>yciPX+oKg!QLGbwQU z&F3?;4_BMDE=!Z)d(*?W0$2yIj^#ema8cl-ePhmw6tLA`q6T*z!8mU2KQ}qF#>q$e zsSkXiB0V+}-sL7&w2890876~s)|o5ZzfHdI+lK9Unffr}LBvl&cXh3{rSwlTw`7Tl zXZfV}jfH9Nj(}5WxuJw4+j$JzdjHryZG0FN=X>I;&maN^(M<9d8R=50(hkW z4TD39s9obm7|ZvY(Lx6=4nP%Wb#8+eTg6L02{0KJU~kP5q{|9|7|$j)&&~JUaIIL^ z+AA^b#)0;5t+>|;E8cRf|5qJ}=D4N(PhI+0Zl$4J5taqc5ARxhOixd71n9Q7b3arPF}Jhe$^$hX7debW2f1?I3yv@+mJcd~ zj+_5YHT$QjBF>?dM-u`RDW)>|nedVFqto3R!+C8(vOk zvxofPDz~{@E|6TmoUeaxm^Sh}$!Q*bdX}~RzpJCz*ZM1nfi$p+@P0TR|27~{?_VDS zRx|>a;S}7<8<6(_g7l4<0KT$87FPHV~~cJ+Cm znm=;wyo`T_e8JAgQs;v~g4$18d!(St<4d+hf%D79$NO0=iMxkK{(?G*jubb|!)!Mh zi1<)N`)YlskLeYg16K(=8L}YWwZw`};1OIa{il7K$doUlzwW9`R!24l(&>a`I+rt*T?7(LY`D~t83ZZ74 z*c3iXtoo>k+%O_Iicj|1mQ7G>sfM9{*qSDV;=KPG&+(U3>O|a*Lk5OZ|Agm%7ZilP zKgq*3Q(e&y`ONW0L|2fvSV|;W|En#^n7Rs8#INu4UbTqw+aHk~Z3sSb9_o7!*bqO= zQS1@(yhCV`J8c$fy=eW&>nKcQ5`OYMe<`Do>q3~+PF|jdg*<8`jT8-f=Kb73f6 zZX1wi!j9T5tcIRw;D7zF(6zWk0bs90HEiWHjkr&K>6T=YTv0A2!pCsF9PXI@Ws+g8 zwT@wKKJIMCPJ33qVqhQ);k0|-fdsC*_n^7Z(=_-= zScs@D#1z3K`{-qo)4q-|Hy85Xo8tevOf(kMOGvyJg=b92^P~OH!UP+IS<5=8Cuz;% zYQvgsjCP~^YHoW@`e>f5A1Xgxc(fw?gJA!o1)-p>5~IBqlbxh)4y9Rb34~8`V|^2R{yUe5r7J{3$-IMpFrY#!n5&Q!Zm}B_qL_SlUCu5WI2=J zJFO{i90XLd7JqFCT%jgqBUB^&D#dWx(4Xr1pH4QW=&jG_tObL;4{9sdPfP6g>jn2w zM~Kb}>alZ{YJ$DDYO32(Auek!L|o5do2m;I5mE$XW|Ig_-KG_|42>sS|2Dhvn(Kw> z4A0rbkZ#!^L7;Z9ld!|EUx66!qw4IF@~l6Y&)R;g^Ca!#1AgZpM)1QQ)6V$k2rM&v z-n77LLfAd09>UD$=8bvDDs%#3;j)*UQuA z2!qrOVH*;@ie8Cv5U?zMfP=SKo!s$qhvoBa-|&LWaru8B$yGkDpn5!7!aclXTlWjW z`%3&8O!K`W)tty1tpbCQLTnx)@G6^Z<8CbS^uR6b#Gdj-Re^XQ%&;I4cTR)5Ip)oA zLi-;}4wT1CTP2va>#1Cwm#Nhfv%e=0qbJFZKP8KUkNN7oKAb*E+w!la78Q!hm<0vO z0i@eNlj(KbbgS7OR)gfK+?fG8ilm1QiD_bQkmm>V$A%Wdf^>;##L~C-7bmbpTIKdv z1-VN>Sme8}z%|v&=$pSK9r0iof^$F6jBJIWqwSAnpW&MUiohdYcc-w22Ov3||LbE_ z-qxIUj=(Kg5#;8V!%tc(_<`SG+feSQ9s22fU?zg&%uS7}<#(I6_C}#q?y|>VGRf9b zyi?OE$Tx+zF34M?A^2#aA!t`1*#JFqg}skZo!wEw)s}Q~nsFxA zT_4AlNPhj8B!cNdPxADaUbXe~w{;(AUw`|nug4#7XT@xqMbKHhP@>-srlbdyScW@R zNwn0SeA4Qx=Wz1m-^g%1{k%w>EC4sZgTS$oXvfNp{D}G=3D}!)V%VV>+iTAH>ecO2dY2+pw#HMPGjG7y5jqgb8Oc=b2jb8qHK9{A-jpLCS`aX#<@sJzr z64Qq7$(z2_*kt)|EPA3xMB_-~1W@>l&L0HzU*LKuMLn1kit>TN@d=?E^*2zGx_vaj zBQrdfOkU79vDDPW<0H^;bM?aXDnt^tadqr)&=XBLioI-x^5OFMbcy-f$?w^V^K0e< z!Un29YaXV2dT~LMNSlDD5!mAPpv;x}m1q{4rI<)gT(T&Eh@l-6vl~ z6&?}olsNL;n67Y)>c_49)}`TbyCw{pe43O){;Z)!!Q!RbN9+hMkJV3vN0Skto%pm) z#hl&lrA|9g24wfSZX<}reWD-o-Hk{5Fn<>E-I2!@A}`=Ru>^5(;CXYHKM8Sh;W0hT zUxc_g@!%fjS2yjf+0jGfHQXnLAY$(J&hRdN3k?sB=5dE=^Lj2-4X+PIWUq{yyy72x zQ*2v`T~2fL)2b>jKXtgNua{wLLHB{@pVY-5BDRqGVf|jF((Rg}lnk#ZrV9vRgV zad9g856YF8xH^&@-^yIzd=6wJ+wsfe;l5Bhd@aYpm9(TuqUw9oA&rF-yRwfoTjL(% z7xAxUsAj57XRWFVtU88Vt&TRVdrkv6YBizTD0_?%MU3UEv(w()!))>U9sh+R4t{66 z1%c6n8Cw*46s;xH0s&WY^5&4U6!*s|vfI&K#NGGzjo%caUR-P;roQ+0PaSI1V^LWW zg=_|thtKYBB9|uLoZjDFPtW!GA~NbuB&(y`i;e4^d_X!b@ejE51BNi4DmG)(2z@L%wH5)DF zzaEmq!NCa&$_`_cewQ>Ob^p+X6L_9^zI9a~Xq;hsVHOkw zkWx}YK}L~zB`QisMN1n32cP#+rJn91eEd7PmHd}@N+?F6^vJX!$D#lfBbdD33IB?a zMG*3$Gc$3paN^(n{y(a8RMPV)Ux*x*{oPu5kyQ%m7M?=k3tqZJN@dlO@Ptfutx+o; zj;9>igu&L3>%|m2Y%{m+Sc-6GUn+l&I;m+Q!cvl2#iGrr2zZTxN}E*?02_vUU5F>y zzUH-sq}ddG`sJY0miGh91~#6OFdIxty!6dS*-W2K*nS$ue@7o+-9U8d6#?VOPsun6 zns4VmvLwwcTF0p#kN4U*EAkQkFZxiCEui!0tyWltlNROkb__}qr zFi~kxG+k_Ox{k|KO!&PU%yhXbnR1h zh+-*`tm-L|u)#Eq(neMUtRbQPJ33Cv{p+EBJsU(H3D@&*d2(sIOdg;5%B|MosX6#A zICg^Y4S_sKi{N;1)}hFVbsqHK(sPH5xXG@4+D_!khu2VM3zS+9xJ`B-6z zvh(pS@QNctzj-64&rE3ai-vJ$Vuh4O;k} zmSMbpL12~An-2PLpjzb`MrU;Y2hFxzLx? z!ANmNAAh{kFgB2GgAEkA8mfH>!2aBj_g;?U5hm^E=2vd@b8lc~C^eh1W1pyu}JNPv!6~k8t zV$jW)j|99-1lncRd@q+hhUL#H%$}>M-h-6oj@O_|4qdU0I3bnT9H9(h**e7+DHFHD z-|Cm+Wi&Dlk`%J;oNt78P*94cpW*ZAGy89UAUF6J$@0EeBWrzc^hEyX^RpSfW<;Nv zhY<6F4y~;9PopQs+t-$tGXCl4*QVSnQ#S48)i7-#A_2?qd&#rbM@COHwgV!^R1#!s z&vHi2i1)*4+E};i#fB%wDDnB11RHh&JzgA8`_RouJUT!1P33O!4Aw6_K5Pzkt?$Uj z`cL#;m1nfw9QAZH&0s{y_0SFsYUm2v!DK(EYc7Kg)ySb6pj(ZdJ-gaKLgxq z?p^5oZ0-Ww0-w*rzp%jP&D2{7a4V(Eh3G!}ZVij@9hGF1+2L(dJIui*)+aY-m{B|C zXY)hwZ<`vPS>WG4`%-Jy0olM$`kL6uZ4GE2s|;``{Ljv?4JD<{8$2Fi2okg zfv~+dCrQjvC3=z@F}^jmh|^z`eGpciY;#Wg;u&blQ{NE&1R1E!lB#Yr zy+It=Wi~pdg{lJvpDvVGf0%qWVHad4BuW}2=`)csVP{E`3w=l3MG^NZ`p0pv$+j{3 zJ{#d_BAUBgC_i-kFzGaawyd%hCTmL_ zud4{mwaDbQ!+1-|b?RWBCf@)k3rm(UWrA7AubQ?fjTkJwl>p3fAFA-xCa8=Rrot** z;H9X31w#tahz#=cPHLZUt`m7s%_TN)M)Lr*Bm)EAy)MnQQ&aAxO!sUg-q_p7EOlc%q7%#c}@ zqF(@;sh$EX=o4HIzPE_}JCQm;P(DeQ#4wZ_pUnEfFiKv!>Lr)0Yr z_2=Vemfj>|ZkCG$$T!{et6K;b%%d-bR_aX{_6_< z@4L(;v8%tp1tGt|1>>a<5_uz~?gu3?-@w_W+K#d>JEaRgUiSGD$YI#~<>}6RSAHMk z5^BrQh!*w^J8x?wIDNZ9X)tQmLU2A*>dM3%3o`62R6Y+CKn&}e_9Q^)cT~6CQ_HJQ zb>3{QTv|~$NyOq}A`wXY#08QP!y`>)^ji2jG2;Hr!v>e^g7}x zMiv|>tDv|l6V>o;?X1P5B<$uVi>?BXL!PhkZJ?`C&e|HI0`FPAr3joh{ypIh2Hsd1 z1_)j+5Q9F(i8;25M~J3xUi;pXU_-0k2a%UwomgZA{l}i;LxrN!>`F_^M|YIrLbB1E z7ZEDW6TV#W^DyR2==zNEjh%!&H18ZP(a7XIe~#Al@5x&f8W8NJ8mPs_5!;sdlW0F1 zG=TX(K@7yX=vzaio;@p){GTBP?Eiuo=s_U{IDbM6mf^54>A6)){|m%`Zq)M0xA!Ip z`O7=ZZiTh8YoV=XGpD+(kc)SDAF}0Q%$xeM-(V6lXwZ52HIf5k@;I3}bU(-DZ@>Nj zSbGboxRz~y7ea6k?i$?PNpN=w4#C}B6D+v92B-1HJ-9<~4<58}*Vkn4d)__g|Hd2p z-uK2RYIUz}M$N8OYt^h-Rlg6bCByHLWf8tv*;=L^I!L9^qQW(aCz{-PxCSULF6)ti zI8+m&)9j@7o_73zRG+8gPy%J3Tnw2kZk|Ofn_Xo0s|_!sD^y(F5B$qA&N4O0t!;e5 z0jJsu7_n0XG+7N6-;(cxo0OiPl0-I5Aw5sp(J-u;Hx7H4_ZmubazMR!aUx;5%OBkE z$9DGz6pjq162>n&5w?P(3z$baPQRV6Q4bMZGrYhd{(_J2N~KMaYhaL_Bx!s?GQ1q^ zBWlVS-Pyjtk*BU##CGB+3(wr|-PbflLNW?Bluj?U`2D`t z-aydbL}NC>kF7i?~>|sTF-sc6Vty?*oMKd;Gog6w2u(d11p@LNJ4V?m0Ov>ukJJU5?SMQZRX@+($ zgm`rbp645F2L_a({anF^G!Au`K>N{|OMavl>D7NKfhF{)%$fG1U>1GJm>FU9Q1xt! z38zFGF>E~OLmrM3d`o<~^a$>vA0{f)NkNhQhx;&mv19k$Sry4(&mVRCoS0` z7a2v}1;`z){yb>NZ>ZWC?MERWfATDI7FyHRfTA`n1iX536?|o$|1{h`w|-`pPQoD< zu<#|^e8BTmOinEYAmi%jaRQeFpgd4ec5mlB0yzk09*4P+Oz^(=ePH{Dp7ukA?MAc} z%77{Ma~$H^SP1{;ii_Yh}7r)#2dFDW7;m13F|GR;lSf;>k=kU$V@x}?ZLjx9R85sn)kypg6G7Q9i48xg$Gyc-7i9Cu7m zby;f?8S2piSV+^&)%`WQdCgE?vzXUR@im)x&6r=as@F`C0`FqbH<7rn%h$4?{%)$4 z8;&q&nOx8hYB!oY!${LtOwsTY1~-KlTu=H!63L_}mqWiRKVj=Ng;z*-?(!^ZI=uP! zFVh=ld691Y^Vh4A*WBJQebvHCsh^kTZU>*ONH8xBmP*#jNFD~5U+$lm!I$ak>88p+ zDBB;*-u%|t3Ue{i7{h(ze%Ee8vbeU(t}e^y;%`lsc9|mgAPans*YY1~>ol4C=;S}} z{Aomi+>O18FN}C$wfzsk*X|X?-2IATeo&zM#p3d=rgLu6i93rSuU6b;|HVi5LDs4m zpnsE=QmALKEqKm&;E(dcfiIkNQZ)_xyKCStzm*nRLk_*E@Nes);P!J6KHFUzK{)?> z;fQRdmv+RQcAaAK^uG2v-qU|5FHnptAxo?+?TGT-Sivhm+bO}yS_O-0cD!K8 zUFFOSnDC7|A-Q$m1U-u}PxfVZYso-`UmcKE4rAFnazE%@U_o{0#`;^A;#_XgDk(h_ z2({P0NFJs4l&}dL(B@KFt}Ti$%8@3%QBqfS7aWS!UrklD4mfn1ehZ2wAUv2nPMkDq zr2regsZh^*kKtU18ZKSOUsbtEOR)m&8W>4pe@qtE!2ieg)&2jpy@4NW`|3XBJ5coh zX}eo-!na2L|I_wWy!e{hQmB)-_NA=*6|Y8qsk!D=OS>$kkUv8YtF*AY5DKhSzjL!N_nVYaW` zs1qs?c{$tJUGFQ9)Gd#2ZL#04?y(qWLlhaabeAed&)MWYriQh(MC&dt^lL>?rcQMT zFx+_&aWZxuGF*_JuBOF}Wnb1(>S1j92N}QbULiN{V5?#)2gaF$iSb~3H@n_l`+k+r zUePA9Ciu(EEhQe)rayICvWV>4BIeb%LlX+lx^2{>itB}uSShcPXCg@D0uJKUW8K3-BQ6A6vzaBid0!HTd- z7XaN0Z0S{$XUNpiWsNJ<(P7W#E#sZfa893Sr&Vq&6)dHK;q~>LU^usxdCR%?w87m^ z`S;pM3OC;^)cH!p%BBpQ_pdX=%07EG3=XJN$QUaD)xYOoS*uIUG^&3D9#_by%9g2r z2TGMqF`u-Ka&6=Jiq5fn#XyI}@siABbG+mA6{2Ca=J22)C^=8mh1!wO5dO6?CW4H4 zIPFVS2B@F0U~$e$RZ~r%V=pzMnr+Jn*Xt4I#_~2yp_&~o(9TdJNTA~`ZJ<&&QUS>U ze7Ui#IZ*V}$|rg7Ig0Jf2Vm|dsAl_I-d7ftDF`K}s{t_k7o8vxh|kl+zu#S_6?EbDah3K0-FqH)xKFAj>&L)q3DK&AucX0y98TXU^mtBqRB%v+vJq%opVIdI4Pg2 zz{kl2*H)6WqtC5`n#+CE-m>-X;}?SsCV`Tjl?H*D5nkLLQf{(^Kk?g&k~Lx?hrdR) z&o5VCH>f^gwVqdD7_>4-MumgYnSriD7Pl z5t0vesxXtKTkzp-fMp``rBXY#j?;O!v!P${AT?u3Ah?L5d4<`$Um(nQSNr00ZEX-{ zk~olXSbYjaL6YRK^o}&_%EV;>HLN6%O!H*Vx={Ueps;?MURV_{`;(Yz1WoNXGwjI~a_vuIvJn=h=oO54 zQR0a@Vmc8e)!*E(Cx4OieG-$5*!$CoMToo9e;cfi2gz7X!}4iNxCP7A8MJ;C+jlEe zu3y5Ui>RaD0W1@P5EvcZKr^=6Q57)$%#F!dVZP;Bcn_qRwVLy4AZxmt`Fy%fmpA*E z;LSPzLEARnt?ZH&@gsE-b{%p@W@Ew3Lt%x=T9K;r*dwr!ktU3 z^oA|avVKBtF42OwDgFx;_!nTC5WIsA`d@hMSGe{cwD!2gPx<}G5C`FWGG*cX@9!5r zI}c1{O;yoG0j|{{{IFeE+U3kcsG`!<_Qmh0_2Bq+g0>xK z$n^zPrl3nO5z(|P)k&aY<%gZaiF>v824Y;itNMj|oiC=Qyd0oZIw?(!J;BQEPhTWm z&sI7|HQQe>EDxxVR!>#qPp|?n?34rKOS`g_j)Nbe1uf}nCT8OXTg@%-3HVV@lS<W zwH>G4*t?~@><=4_iC@L*4|)}wHE2hPJyOJt4c=lP_$UFs)8UM2ubCtVRkGZEl5vOb55`_Qel(_BuUr*hlA>!aA2rHBo9ac zYu%JGQO(X4kh9BjsljhKAY(PFO@xGZSkBje{~GsAd-nT2j?C6T(Ls7Pl!QdpLQw zRRl9KwPzdc-x9H|ZvV`i$z2mQjiV~o#h)=|$(a*{wuG};9B*B&)hHtSM#hiu)e=rE z;qbi#4uoJLy=GY|P>b#c1%KsX6{P=(>&_fl2`J5N=WVk3tRgQD_+M0(bLso;(^%1y-|R{!oIm0nZLb`|iYt5GK$*r@2q0m`Js&62HxRH%0F z$+RuPo$$z`&`Q0yM5Lhc$=IfH(er>(e4^C_YW7Zh6kQeNgI2%k27SSuV&GHdlw%3UH`Z7)1z}H?s0UCEV6!eS);YEt3o3Wd_1C9y;JZFZ^|%DktB;7fQ#@7{KAc>!(zDxVUr(yt zpeCI|O#)pL!jda8y7Dw9j-+=|8voS{2Kn91?B$dZR)K;0r%OnCt$lM1rfYtDE9@Eb2zI$4_|}zL>@LZ>5+*&Ba+I?tZFQV z(qrXqfTi-uize(DHg*Y&Ho!vp2H?;f*J3}_h%K-}USq+8Kf}g8fyf4!FYg8%8s}Q< zqiS6;F>y5Llp+Y4V{p02Wm(f=DWhpuT&sVaQ|?Pcx{V!#fNs?MBMj#YvL2*VcibG$ZJ z)g6>@XbR0Pf80aRuATooVt*_oR2{jTGmj>9NwUSnTFvfQsV}iYxw*L~!eA0}pK(&< zw+9z*MMiX;1l1yk=WliEM83bA{bu=w>Y-$=#bGL)WfOu7hQ^@=E-j-3kPWb1-mQ8l znrm^0ihtPzC*#N}A+GPePJUuq)7-afqX2^6;$E_phs*f&rWJD~T$VHVlf?tVFLM5>CMvsDih!naBPc!kibp zCJOQIRZ(6+m#-u)MDR+etk$0x@7hhX!hSm4>aQtZkaO^oUcXTc2SXht8*7GlRhv<2 zeO8_Mi`2BVO-mn;e;I6>ZFDS2#cQZ##9U0?a$Tf(4^grC=SL(4VDr*aE z7W!nU=K~&k*gYOP=A<7rdcLJeqo<7jlwHfY8FpBE_o~LKZ^!VbVA%0frU-B^V>Wr4 z8>IE~zAH$JlrNYQ{PX{vNaYOHf584*0pb!4to-n*`%o3jSNowciycwBWNCsyYG#5V zm&O)5s@xP?4O`j@PGkPy1bysaDf#eptb3KTaKTf`&5H((no~)sIZUr$I&iNyM_i3R zm?O{yW}k2hx7p&cbpd~GDwLE`{IiwND|luR7W~V6(Ij+I!ca-$Xr5;S zW?*BhX>3XA=-AGo3nUxYpDxbo&5J`0oO_tOim5)imu4up6H=#UVT&6#rq?@B2=L2) zgwOxzvWXhYf?ohe9sjd}@sL|;GAGfF%{l6RAcKbKB{ji6lq0JADqF<#Bb_WjRh0>U z@43{o@kMCa{Xh>p$`?IbpaAgB?8{y+38@jbRK`aT($v3Ur9)j1!At5;rUVOM!I}@* zf7Bi>5lYz+U5t5aNmnKqEM^siYXkD?v^aptT1r|bRK=qk$41DBcqiwr_Ej_t@DER{ z?F@wyVw8L)W6p-^U{IMs|6aKlEJT3%w$B*)RL{Jw?_p3)mJFGt_F9}IuXOudY zi3#Pv2=a5P8Kk7)H?Fymumie#x-?(?afhOQ*gjYTvaAn$|M=3maC9%MT|0=UvET!0 z<>+4SZYpzP`tu4VS4p?&mHZHgcyUAie&CmEq4785jwB2ycV^)yeczU zQ(0Um>*hh_Bhhx4W|MtT%5h_Ek)+u?ZMe$+L;mRqlf+IF8vYpoM;6(dpY!$ac6f)D416&oi z3I#}C!}$Y3@GB-V)A<8@@Qch=SZ<2ePvfimU*LYn>;!O$*OP=xb_8FV*D?8tJVWBX zbv?VV$__ffK&t~G&H8)G65K6!sL!k%&BD2^YxEfEm&e$u$#B(O4JO3qe_YY^MI&4~ z|LB@?{v5q3mxGW);{yOAec6L2cz2U~cpna#u{t(0SHrA>?J=|I!MI(cHnMi z+QKXBH7BuiLTdwOd{N$SCv&wkKMtFv2!c0WzbeP&v3T;f-Y1l1Ua`gR$jasUET6&E z_2DXEvkR~J8y&Uc_~`7F2B*)h*J`%bnntZ_fxeNiO#5+|Ymf0n-LGj|MU|=>lU;s~ zHt$yZ7mSR{haNE?0)PUEWD3fm=OJaF)u_ztS@W#nRJ(mXes>Dppo|fBUwkR0zJsG3 zZdhaUW;f7Mc6!)Sln8IxzVltmTXsCqlfW@+^n#C_v@_=-sYv zE@?{+k5%P48FK(5A~|`$b>HyeH#t@*!9rMaLD%QB>DQJ~Hi9+iu{Yl%(f9E%x5lgS zTD?t}6G$+(8f@t#2ait{V!MmGsS$GM1XOP$ov%s9dHp{tPKh9XvVx>%G_1(nk^n=W z8^H+P4%J)z4cK53Uh|uSFlRFf^YKMNFXc|bJH)nG+F@iKI&T!{5<7{WwS^AXp^^Bj4**Iw55BC#V@?C zDf1(qM~bA6;f@=cpZwLMPaeb7NuR>A6ZdqP4=Cd+ z8x20{O;zEmAL^AcAqs{Le|2Z1Llh|1!}d~OVO<$xWQ8TyXtMp*oLB+YK?Knd@waz2TbYyFGyr2qK+|+(gOZg_gwWefN*UQzwE=3 zHqp_69_)q5)Qr0R~pXhg+l)>!iKX=sM(CYDE z&Kg9T@-Rmu`9Ce8JWQRmq7-Fs&4^O(1chSeeMPBqeR(MpVnx{S8|ysakMt$e@0@A1 z8=-6jeuJGjgkf~|E5K}E7of!?kLo?I| zBw<(2z2VaSPzh~QA0UFbUM-X1eB-A5Lm!dH0#cH3y#PA1J|F_yVfKv#1!tb8S7NE?{^yQ^UrZsvv^cG$ zb|h=X+jY1l0D zZ?Lt0s33l_gtTTf%!l6A{-J?LQx}khT|NH>Mf-;`B9A3_G0=J*v~E4buGp}x;JISV zoNy$MvR?g2{NA0A_--N^RaObF?noGoV0-)9P%BiGVjrz_v7wpkUADBDO|g8LA#5QO zt(Zqa{NDb`exVYn2-7ilc_*2=@Y&VE)ayT8X6wNiXg zrLbYy75ojU4f#0CXBjyeza0UsxLmUKZ>$C+-1kmuED?DUv~p;dp*9MVQwL9;q^O5 z;@~o~zA8s}1Ug28btB&VtsFrDR*ZPni#WqF-j%E(6`v z0oGnZ4SPX`@*%^|WmIE$TQ;LY37;mDIX`uV0J%58{97?tQ(=N7+qmtAN7xy{7H2;} zq;q5=E)MPq%P(V}AGcsJpDp%&oX9F%ew{Bjd9_LDTgMu`8d5M1I&s|i@#^^lk2;#i z$W*5El-8h?{hYj0+rnr>*yvIQTpqiW;6Ogp&4P*^@wAfhK086C=Z_h?pJMl%l+m&u z0fEHBS@zHO*Q4i!Li-=!1-d-K9YfBLws?Dv=9yXaKfmftfVCzFw;21O|JIuj`s~yF zM`eP(N1)ce;FQ(zar0u-j$(cnK!WzT*^@)S?XNy!l2i7yc_Gl|ZS&%d)>?2&f^}I% zL)*HP9PTz+(Byl#`iKBRBJFqcw+7x?Ote7Ymj*D`|#eFB-p%@W{lrd2~BYi z4ZMfsugfuB%~EnYil_hi>i?~ZZrvQJluk8VtCSM8kYR(#Vbn*w2r!9|uSfZCF`r6) zO+P$B&xp66`XwV3adTKZ41G*LwSA;r5k_f>p+*h|xMtu7 zw2|Mz?3h-^6cZ#?6|L^(19K;KM??ZQRehh+;E{%9=~0ndzA(?(94Qx;3vEodO`OSI z5{IFeZPUPvw<<>}E}?YtUBJIiLFK#O*r+QNL66o2@S>tI^{coqZtuh&F5*@4n9~+n zk9&rkNFWZ@2UK7y(R3OO@V>l3-hCwCKoF*z-z#_usD*<&>5AyCTGC@XpHQu3`LY(4 z&pP|P;u{8$SXEC3YLM`td}F;kU6t_YDVsxrEpwn55TxJ}))hf6oltmtvBUcGJ|uJ( z*6-l?3@JJHC+pWR$ z(@%#I29``WEeoDG2GALzsb?OqK)5s>N+ED6d21ub3q0^!qv|T#peJTu;~h-j73^kie=*>4#u*^dHEf29S0< z!+M*Q)kR`U*J^|cQ7%L97Ha=qpv&-QjPDPZ17#tz_F;+4 ziiO^So;U9lH%mNcmZ0|4w0IP-q+xCxyADzLThCy?J90F_wi9Oq6o zwbA$IFx#^!hQOqz!g`G1XT|CycF0rrOGu60iNjk9r;jk6zDDQqtbbhK3E ztrN3Veg^)cOsY@0o>W9Zrb+8~{w!}mqA6qc;jtDDGl7eAxJn)$?YC;-^PgbBKWZfW zP~F@m?74ioH`M5D3U`!WedhDF`dn9$OG=AOKg314@{LtbFDQstrTn^ueGhE7oo%7N zEl~jyvEYe>qL;UIW5Om8b8^eWCa9r;vr&yw}FD zI)dwQUc<)p^V_+n`Q3`Ad#_5O`ea0$6S9{u#8J`}g>L7H*ghV5oi?=P=R&U3bN8Nl z1qnA^25uB2Q(0|YPp3)3Zzx_a+gI1Peq2suHmJlZP7o@UNpz^;+i*iG!~jeFq<`cKbQv#ksW~m@HRxV?m8)AlWyfIZACEj^EN_1eFY7m( zfyc^NeqSYp!CJfE=&LiUO9UTIexhSx54B!-mK771Y)RWjmAwz^&thTim4|=`PRZ7G zePz9m*vZVQ*l+Kvuit)F!oF8Zqp-p+@~^0sUG}ydehqS9d=sMC5Ov87LM8jJa5_7k zFO2~6zUW??=IH`Bfd4oegj&OnArv%T9x(9hV~b|x;-65?`sAzHbGyia51by`t|;f+ zz3)`0ROo}R0M-l%^36TDz3SMcwvZU@HVBnS>1pHPXW*xs-JUljGG+yx$))*RB!C2k zz z&7+CvyplE%32*Hy=GPM`)K+ay9ha?m>&4kdfO4|@ve3?T9%FQL;Zen8^nBq8r}6fe zFw%*Bll+4@w~tmt!RYh}iubpnL{H5u$DDSVQm|t$H#|fG%hMqNYxOU`+>m3_WFw{} zL81RmK|)P{X~Ys{!k z1M^xK>d5K9t1gGX`h9L?Agp4eZsV_d*iaM_9c+Y;f*#5ypA9mHqosNF#r4m;y)h#F zyRD<_znOafM?l36EK9s9qWz&I&;=DNB~AgB5dXVLmBX(Bj~T;&>kqyp{bl#QBrs)< zpIQd*FM$g|xg$~To`w74tA`N_$|J&7;9Y6>KI=wtGah_6Hzd1Z-$T<>xAeMcJyUNM z-CrjHJdR@kGPfAuX@`IkHv~(V{eL#aaY5~(%IAL&Kg86rv9B)p6c!!4dtV^T37qqO zw-X2}dh|MW;^>Pomy@!_b*Kj%&+|bgAs2AL9Za^Jfa@6!)J`;dml} zr*H;laNQ>g24WGe$9lQXA8SqpvM*DA8#2axPdxcyT*7dg>Son+lWNj(TgIukq9-Yn zWD6R6{%C5K=H|SaqxM?^k<&CGm%*dP$x^3Zx>x4zYUF3uoGte)EZ5_yg0f;G(nAe% z%Tp{W-W6>G70`k%f=!DfLEKrZE>>Jtt1e#LTB|NjyqRHXI8Ou{WHPCbO=L2ujGbjN zsfDd)GO3OoB@xH=GT=JmmC-u!;=1~)t+6$+&Tgb=Sr4D~!hii*?P`64OAr6o$nUpZ z+%n--tHVCy2ZHY?LU?J41j(Mev%jH+dlnVGFrec9_h^66Wp=SN1F!jW>t^OjN5g-{ zJ&1o$J)vgSN>ArIr70o^(?7m>83a#m;>;*2JysG&!H?|c-5Au zcPf-k-bvn+H*uERkJ;64GInXw-($MgCp~XzR1kz{jm_mPJ@EStLjN_A_Lslj|MfinN_I1wx7RK0x?7mv4 z!mJ$N$?)T$^$)4-WaoD4)KdUmDS)%ShAr;?9SJNpuU&^J7G*DrvG zP}a!o&a)byZe`a_cK`l+T!DVisArhHtC?b1?VlOblGQVhO?q?Mz270P(=$IQEI_@kT5$S9e+}encO$#y}4)iOVc3+b8+(0y> zU7l1usZ$~ zbxT}p)U2HN;_UW9$ZLLbUmRQ8dWf_iJU^EGrOfGgJY`{Q#QgKNl`UVY;x86(V0KG46{XA|73DXgzVI}2(pFXC#lLxRo%U6_IZ(UZE(=#UMHmi7 zeBbnMYl8RJJKNcbuu0`ZvtzSYnN?_4nJxL0=2uk-e%3l|*B=}hhb?;U!g_V;8Q^g7{SvlP*DF3aNYvst* zz?%O9=s_5up7SE;albR4=dgY4tLQx z?}&K88DN~Af;(sYQn1C5DG2=6z=LAM-Mo)$eS_ofgK%9;dWWWF}>AG7W)x#=Jg!!@{yoWm@Sw0Lk*4ldstr2B#Zd z3NE(`XNM+6HpZnplEC~z-qo>4AT&%2*_xLGf0?YCSg(T8*t zK{7#ag#r~Xg(;WZW3K4(GzHZtj|q6-u~+-HNN<;;9=96x^s()aI@m2)dw^EqZ6B=J^YJ!^d#3X?Dv4owZoUQs@1O~Q2J0NY})Y8b5Pf=Mr0`8T) z6XHww$~+4whQa%vogl*w0tyZ^15K>m7^Ujc0t)I6&8Z86sszKEz1@v8t0SPBwtlqgb+&4Ft{n4QgxyHO zdY(N$abm z_v{S|Ebd}NpkyK>1yvw=Lf5w;!(J1X&uqA?c#)|{5^^yA5)^W^P;>wk{OtFj z149v4p6`6QN<=CpD0FI}SO6%*+3&#z>msfm-}!zh5q&N}VN(mm13)3qeh)b?6us(3 z=C0$x^DkKaO;T^H;d8rZ|8md)LvayPvFc@CXFVU7kDXsHr8756`IV~h9@Jk~ggx$< zQTU!>qGpZ>)yiucJVR3?tzCOlH${bWB?z*hWPdw{KbMMRB^Sa$1%;om5jkYoZ$e|t z7R!p)pNjNZE`)^&3O8XRVrae3WbZSZ11nxbDw4Kb2m=)qb^@ZN8Z5nf&Zy+MwQ>Px z{i=!3l&>iOR3&STauKK6N;+7f4$QPL_{BFUdab>L=xERO|1CL%dC%(b=#t#juN`i6 zG-Pa(;cPw6y-|h9OU@&@h_xet0@{L4R_s8F{QpqTY1?Fh@S-4(_)sJ&Bpv<odb;>Ok8Zlm=g}h3h-RWt>1;1MnNph3hNBW$r-R6PV==BAW-hYS2Sh0}0HsAEC{I zWi@EGtHA*LhRBBE_!`kiJ2OFip&{yuTNmS~F+U2_lEc3oOcMM<1p(Vmwr{>@Abn`J z(#*;5z?zDC+LP{?(7M;5CG<_4T?LWCDl$?a0UQiB8lD}g+{%a1ATl}td@jrONF_Ve zcPznPPO|Zp4?ml~z_~ze?7@7VMt*CByLt0rCWP!%8m{$=hZ|HE2q!|y9mCW+%TNT!&gul5dmLdcgPvND98<*&MhOa$AYw|fvKAF-7O{Yivtv{%YP*#(U>ll?Hk=z z;%*3-{Lshj!%+yBc376Z5O;Pc+)LsgxdeYw3?(j-XdCq#m{o|^^yI{j^mcb?5$SQ^!8-@oH*?cWxfw1we^1|DqTBLUwBbP zd~TKvC#V8=(Z?CmC<0P>+|3t~0!OIVbY>jDVf5`qFSZ{!mI~<$`Rl)9g!pyugovbv z<@Zelw@gW->)tm9&ji1llt}LqDA z^=a;znxtw?Ug_l0qjBQx;qekCjRFQsuu)iSu-rb+JAAfg$k%kIEK<s0jVmn$q^KXJYluvCWHb8tV=r2kLg~>94ZtDPR9==4wuf~S;QhmOIWt8+q1q? zCz6Pgsqr|Oz4;E<>Rx8!!db$PE)&E5et&S|8`BfQ|2?~St=?Wz$P3G!mR^7X`}KYtx{p)~Q@LZK+w(r|=e7ta3?MEptgv z0?G<4&;h|ohlleiNSUU%pP8T?Rv`XPkEDoh zvjSng0%5fRVcCuL>Ag6jsA4)x?<34q5Sby26dRg&)r6SqNg>X`d{&~PF^_$=tvpM6vKGlEr zZFghODsJR9rm+vFb<#q$58-14_WlZ}M@0mwP6W&!ce+Kki^bkE-(dft4Ptv0K^g3{L{+Ze^V z)F3~Vd$1X3u{9L>KT&1*KQvsJMwQz80*g7J%)r01c{%`ZZZvgr$#v9&M{P{MFS6cp zhQN(J9ZAl&eZ%ZL(u`fNBP}E3nRa^g?2OIXXNvcH3%{LA<$8iovhv+B9y!HJ-pR%z zy^MhKk=^NU&#cvWgAvWngO*69j$`CiqF;8@1EC%#ElkAu5%%5#H4!IfAt&Y`Bxcn2 zTlIkt(??EE0t^8DiEqPg#2VtiG#@Axj zY=i}|w2{-fZqzmGF_pg?52ge3x&eZNP z&U$HV)pI$g4~3hMPp2K*OfriIexwlOf|DlhW(^@bkmZt{jhltS8R}Ff5SEhVf;cU< z88SJTEEZ{G1!CA|{X-f7IFY82Kv7;y2kF`csC6Jp?FfFLI{6+;OSl?}?LKe>zu+BP zdVFx7*9XFFw>We4X|xyu3}M&g!CId{59&D(r~G0wO8a) zgqYqoH2|ld+M@wYhK0~=s*}^!1fMr@#GPI zHpOv_fHILMWU=0a(8hqB0Y8c~c=Y@GLUlI2v3$|7+zStyniOow!G+K~qUij+U=Tk# zNUEt{|1tX$W3BY;1*!6iwt|^5p3V*tyY(VnImvNy%5LdrV({R7!`ck>P$mmiF$JR! zM={1HD+IeGzkaSAZ$HRV1`1tK*NdFerU_!c(iw z08LLONriy2Bl2g|*IWTxSDtX4j;IF511tL0H?WMO*C+t|?bowbn-H29uuTY(l{*Mk z-Ejt4S$L`D^4E}6R48WJ8>#Z4>+?yBQnpqRwBzK>l*^0w;}&zB%z2CxYdE|n!BFY- z6noCyDq5afGiBJO#LnGz%h&yEh=y`b-4#bJ6Y$h24&GU}J*Wx>z85oav3KN}D&CZ2V2z71!v%6^3O#{aDmnW2w{Nt? zBLcSi9p+k} zBTt^4gd;YP5Sb>Z^e1-h*Va&#W>In_)oI7t5Y){NA6XLkxvr}mn9E37e14*ARlPjj zoOHh2T{jB*KkWS_LG+gnQR0h2APd@nLE%1>lnxA%sB|(K#>-D!->-I<7ABaolVim< z;fv*A!kxr!FZdLWQ6fj;+x#O&B&b&LD++eaPSieEY965%TN=HiK!(k^^u9Q`Fz zOV8Ly=cBHE@owM=`%y!ZA8jNz%d}7ww>Bo`!4DlgcH){S{FAbg_;793-|EDxcu?Sx z_SNyr+_GT-u=Yy=s8NYrf7Ln^gpw@OTTBQT2srQsAcKH>S6;cq4gnF|4hew=KI37- z>~8O5?QCIc>f+4&=fKMTmv)hkeFh8eAMGO2wNPi2+*mp*wfb}}aqCt+{{FZ0xa`Zk zwoCqN=5Um%cDXcNFh`*`Pbi}#3T@RWYIIb?Z#^3#7_{HT0i7~(ZyJjyk+fvC*!qHL zEWeJceGl#1RlLZLky)F?(+XS98>ZrqvIp2M^q>n?V+vdSm^w%1NU%*-gg#OeHL4y?)l2!f{q$-$UP{ zIEI}(_+45<*t!A*=$GlX*vs8^L5y=AaBs#NHm7qD zx$=ad9MqR}36>6jRsW$>mx2eW>u!(l6#HM&rdskIW5g#3+!68N3|FVfyREY4-w8%;uhBv^pp8r%u)5Xj&@%wR#1;1JwB!GaAE++79@ zG7ts}kl+$za0w2ByThHVwb!}lJKuBeUVGhtdS;&K=k4mM>gww1w|+%4u(@^~L3{)c zm~i5$+)r!)-WZh^B=FoYjXR*hABjXXjFu~r#g{w|V6;liE+FA<^$UAYQ*3k;IGhJh zdEFZPtMXym%3wB4W1oFtQutb-RV(DFSj`3c_eMDq6rvI2&0A93<5=P^UKiUR6;Db% z8yYsoQU=OLUK6Rijs`&E%{`1a_Lk07rUWN`an^P{<<)X_^fJ|i3AlToiE!LHZEyGDj!bIKv{%3B@mLBB z@WXy##P{Ys*{+7%Br@48=0GC^&v>{KTZK#Mfj8ylu&LnAPG_{f*hDg*PKfH^V|cPb zAg_VaPq`l|(_@1eVgb1g43C&N%^f~7b3SA;Ou>o5%CColC&ng%M}?Zy4A1|&fykKo zJk$BP!dSM^giBn^tv1b6C~fDf}8IJiNe!v0N9Mt@8;m@Y|o#w^%j-SU+I5)<1cx0aq;$ef3wTq_h$LiWu-Bb z)`^tsX#L&o!NkIy^9>4w=xEE>wPOzCBY@9`7L@fzDr71a~G0yb6xu7vdH57UregDV7)!5 z01lGCS~jl5w8c^dzW&WxPmj~4lyQTy(LJMteF%<+aEI9eCX!kWGr`=bsfZb`G~CI~ z+eVNqZ}Sr)jqKGsYwfGvbIgkpcaj-R5!d+9*vu@I+gDX_%7utMrex~e`RbY+d4)MG zq7s?qzxRSiZM*I5yY#7_sAjqw)yzzyn%VEDW-iZ`S8&h{`GRL~x99io{@=~)@235C zQ`7NXxUt}OL-Hxi(XA4faCv)hy>8&UaI-U zk)N*8mY-X$ZWZ8{V3KY%SVF}v!v$%<%YII;G;Ho>cKbsEzXz24+^Rqd9l2{}gP@tledWCFoU@viP z+Nwo_D!0A07;~MikTprz>zrV)FN>V}VsBbNGSz)6J$+lZLE}>#_&n(XcqXFlYe!$B zzC4VoZ&}%qfOHV32#H3%DslYIiU+|prZLWf`^%XFn9j;XHE9}p6B$Fd z#>Z!^EqCWqcXj9W>-ENC3*7WKH(lzZ!^_S^&U{}!tNU$*6hO(kU_v{RrwfuM+~*~J zlDy~IbI>feY037c*PK|^n>b35T?vd+9uKxkpOhs8Uw#}j?KqT$W_(0UDAT)Oj4ys3 za8P_ywXsH>l{eO5}PX^*=rIE(y2FEDT%dr*w5pouO^M28huj^Ki`Aj};o z%9n;!e&qGaWEcsY)cI&0Op-Z@P#Tc@U4 zj$!dz!$RQprO@BQjknby)xOh(eW1&Szp^7Tz!RYY2#3Lw2=1^#ln*l-h7 zhU)Z{-U-dT0t>98b%O{HMJ(G_WwVOCSw>zRpIrj7p@&zEfKI~EMXp#!y`ZzO$memO`+z{n|H35E7B_LQdW_mXfU71Zs=21qaSJThr>7Of&P$d`Y9bpp-4Evz~sluYoGWy{54$ z#C@h=DsJ&TBVuMZM36s~XSsVd+)6Na9MX>fmg8BBi%L8 z`2UkiAPV&>&Wu-&3r>d>i{t<%RKMP#i!(IiJ+y1_SUI6T8gE?sU|OlMIV2)X8_O-- zQE~fqW5?DUr_oNnRrMJ#jh)VSKk*}5bG2SPJUE|(p6qze%v6=cI&XIb;qiWu)S7*0 z31MjuGyl0NvF{}O`eEcs8 zGfB}WMQ>3K%XaYV{17a4LGT=OQP zELIz^bFoRc;AlXpfI@rFSHSDy>`n}N$YxV{dw6ZlNHpTUZAKI&M)B;DzBYQ!Sdd0w z<{Mlw&}~#k=;YnpcTxckSOWLBYKg86Ri)C)^0-4p$9;6M<+8FS-`M1UjOVZWOes=0 zVtVR$r_`8c`b;rXIO2QgQaBQO2vW9L;tF`D6q))=s|}dgO{;5Ew0liSQnuORVk3@z zh_0T^Rw(1FcM~n^T5#sXVy}CMr}FN+aU9HPh*iDPAwr4<3Qgyg-8R@B?L=N(9@uoB zE{O@ILoDkZm^bI_^fwCquB)|w6muJ2dsQsym}&jshBd#r$d&Vc*4qE2W#m(>D7}Co zdZ8WXm~9aJW%0}tSFs#B?lBZnvPN-45L~xdIS8Jok@UosIfsi7>6BCP6genk$U0^i z1TWQ)&m5u{M}E-Ytk7Lf8^2ikYQC9xteEo6e6x1_N9;gmV!r;<44hxnLZSqurOgPM zls4V7iJ^r0G-5cErZW=9QrUJD%Y++DI(l^vQP{i+8t@-~+fVuvT4lqhhm3j~pJfs{ zjtXwlJz>Z#2h_%eo)Qd8(>-C#RiF!J%mvViDgmGXm(QWRVJu0ZPr^1h@Dm7zW$EBb zfR)-N{O+Bou`l82q)()@!FU91lP2M{jX@tR-WNWx_J@Kpo)dvNpd@#f%SF~w7nsD` z8if7`uSo#o^SZ!#Yi0X-jmgHoZX>+BOs`d7+3E{k;M=YQX{nMDw=wzB5MArLGpFhC zoiFA#I+rGT8HiULy)mBEE;c!K^ke8UbK1o-k6pcUc4^0gWegd{e#jWojZp=|LB(Q^ zT~joYuv~*R+=-D&IlFXYhQXqTKD}gkE|zSa0(LMi$cB2WJ4alsSOwdu9$fHlzDgy; zWyNF1jQ27oD(I?ruTrj8p5(FxnJ+Uq^&7mmsj0;`aX3sPqdo@bs$N z8YO%?1)n-|D#dlIFmN*jp2xzz14^QJgkNWgFaC-Nb(xQu3>P(NEwnubbtT^Zs+`uiiPbEd%ASjOr#+fm!f%L! z@2%NiHkr!r7eGqS`Q zJL=uaV@!FT{3E$4?Xj?TpfBHvev}Lh(~$j09T-QDo(l$BafCNV8_3B$eQlYydzlJ$ zOO9k3S05Rpq6ULsTBos%fL&h}!f``kP2)aHjYXo{V`Q8qyGfcQbKV}b(+Yr(fZBvm zH-cd!I#C5c4&a$0pb8ML0FV!3NeERV&}712CD441pTU?5q=PE}bizW@6Q2Ngb(h`b z*<@k~o$a3Fl|A+LgaA%JZ|dE=nI#sCjPl++A58ZPSB+W~0E+b9*QwAaqBxznNN(1K z;?EVlfO)1WdApbZiZC4RT!orPsPC@yN3B5K2_bAl zWo_BSs`z#fn$`-qpm!flD&VbtExB{GKUJF*Ob01H#|jaEPo$11?vFX>%F-F$`yLKB zrV~9w!?w+@r;B}Ad*HVwy4Dc=OabsZ%#s;@hY^K4`Ai8A3wWjs2m`pphXTVk81T&q zG`aAJ2!{3O*cfvo04@ok-BzCTu``Or3V>TnT?V@0FA9KFaSXX!ZA2JAHH}`~ZS+6r(=-MMa zK3s1lK{LF0b9zk~D3M1_mDsx{zqLvtkflR3N2(+83~{9mT7Xl35;fW>KbdJMtAN!| zx!W*~d;%pv&y`x1jclXv9=RQltf_r|>l-RsySdT$Q}soU2F1k#J#cDX9aK#p2JxQbX0vPy#tKnUJbj*ZrsfBZ9ioA&l}xLqRzTL}8#Rg@jO$d@S2v z9z7lSIWR-E1egyG1?j|w`=oHkT%^Z8F;m7bKf@2sAA1MrpK#7+erb<*-Le*ahhlivnS0B28KM`cuTB$Hp$d*79 zh(bZHW38EiJF+E#3PjBjRYtYRWX*H!_hC?9QD}rO& zfy+-Ovk$?yF=O2zJ6UG5FILXM`FIt2Ja$+h7XsdX5|4(+vVh>}M;7-km6S9lOOX0w zRA73jb^V^r>xtY}=l2D8>m1fO`Kr<$CiRto+St&5D?qf|<+V1`%p5D9aV$q1m1(sG zQ^amOnO^BZ2Cs>^SZq&J^2mm!A+`fUn|$AYv#d44%clk9^`GL56-l=FV+H&KDTSmIj{gzB5PlDh5da}ppxscm;yUAOUz<*+<9qpKN-cceFJ|v_>zJZP9zQvD< z8iP1MY7qE?9Kfm;sv_6N)a76#yJIq+m`#{NUZxm_1kUjSd=S-m!Sb_54 z!8_QT>gKbZ<7NYJVGRLlFpG1vq>_@u+7cz($|rBYtj%Y1gh|5@DHz3&H{`Tr65hNV zOkl)sI#m#Zzb5Lee|-0usH(3N9c)H@oB8%@c%)uK*_y{2L*@45e_rKZ)o zOvk2TKFw7OJ9j%$$QTa%;$`U^JC`+fW(4@i4LFuqij z5q=|QhCI-v-^Tn>I1pRPvq|(Nd-mv)`cl0q;^w6E;q-44UHTA3nE@D`17)I@KcGvbB^>%YD^IgGk}mfO*-#F-w0ufjPdhnP-0$&CIJEC~Yz zmNX{~|8#uM>XFq$D{QL=D9EH|D9|Lbd=V5A89Gz-A@%Z>x`vM*hCLr_7Q3m z*>cL*ER8qPHqn(l5Yj}LXb&N%$B>>HzO$k{W!B&YX))R>Gko}Kc{<&>WmC~@GyE}Q zsgo_in+0#ZAhr)TIreUqSA5!wR$6woiR)?)X!C)OGu|mr@qpE=@n8N^ap;YYf@eW(6a=iBN#NJ6>FJXBty5Ff^5+I z#B6Pr@nfv8+&E%HYv0(m=}y;PW`;xz#H*1cp;iyt(5Lyf92O(^Vfyb8K$wH2>`B+& zA&$SZ^(y$*Ry_#tjSyBn7^gNmJ~Vj%N%NxEm;9DiyO&`X;iX!JvTN^~sC{-zdZ4DlE}r55Zfl?_}o%kJJYuI;ayKQ5~itjO%@ z=u{nBFx*mQOF)`56BW3)n}?#5#Uj*|(7=8~j#?dn=~O8s{U1n=Fek#Pg;xA0p@45Y z#C!a!B2CX`%LiubXjku%oQhq2d#~Lcm-B__H014N>BR2{N81S#Y7sES>u((}h1$It zy8mAVC&^{VrO98VzC;zB3`Uil#IVBsqwFNXA7E<&E-SYw)_og#&#z&0XzB(z75{b_9P_pn zOGV6NKN2t3$ozsDOplSQw7>P^a$#X>z4v%prj+p3*WJoi#ZFXwgz4*iK14h-)JStN z-==u(CHYHu@V~Ikp6BD|^W;Cymsik2p_-YaAl!&i*k+v1n_qebgFNY6G}+Vv9?8tP z{3~xWABCZqXqLg8QMglaQYb3lz!`XWf4PuJUPEFVwxxRVd@M8eHHw?8^=A|7pJ-fj z`)jI3-$Ef{S8pjj`G04-{r!;%;g=anWrs{bpCD-p{O@N``u{J*lv6+1t+3C!y}o{7 zUwH)A0C7{TO{rYptZ!Uw)f@X=O&nfD+b`T{)L)I}_4?f&+RTmS3Rc$lBk8$ZM+c0{ zwhLi%wN+^k8m(-EcD4(f=Ul#|$uzQ7r@d=rtxAh>)3O%I*e>k1>)k1Aw-cRFEtqqu zN~>^-gCEa4L!6EbOI4)0zMq*kuJ?0(?JNM3vM(yX@fEYZoS>9IIA7oNRrF3GJF6D% zmg^T6Y!OkHf}{}Uddo4XsFWSoJICVB{_gGzld~($9&Wl|`&>t} zOo=lmSrA9_S^ik%#CVk;vkXHeXo5yj#Ln}@u_X#aP)*nR&2nT0_WShe{%@TkeK_ya zYumqd>WQ5lObXi+e1z1vp?Z#2Ut2|*p)21od8e!^`PpP!a zT6N=A>sKF}CvI*LpFb$5FBI$M;op*KJ`5w|lW*OfPu7L4sq}29;sog9@$vXA7wke) zB3@v9e+3V5L9=@Evx~V8{L?cV&%9;N)B<%MYOMK=++`%u#@+cBw#*>y685{kZ=oF6 zQcjo(B7uY!F^(7wR&RvA1YA8IEd1ygmpEJCu!voe{_&9MrnD#d#)rA=`rA$lWO_Sc z$=u>-Nfu0O*aXJAhZr>ty3OBZhSgk{jg6c8@^d#fw5)C;RtlHl5oR-04vPo8bn3MiA`rFNg_-p}f^ZT;u$g(hBp3od#{D^*cdJBMg@yx$#b9=j)GGQ2dw%awwvEn33dY$DHmm^)eH;%fA&UnSzjr9+=fylL z_?g^XoRX!4vNX{}3iH*l+JZ-~I&3I*XJ^fcznN%TI;vCi8ab6r>EYi{BtuLF0m1K) zGo2!FrYyV)q7kNhQy4abZ#u{x7Jj_IP>G|!vlok_Kuc-vfxK{f(ZG{Npg-#o6)qf@ z%%>n6NN*A2guy&WyNvC$CQig8ROC0*k7vkx-M-J>k?@JP!p-?v{$5#2w2VN#-H;5Q z^Us?6yoQ>H*ED8gLcT+uL6?pjn~tZm-dnkgJI5I)lt?h5(dcD)dC#cJ^&jGej>SX~ zU>fXJ>TB2}v0;2|{ei{#<0L&%LVqiP+DU}7^Nng36qyKiY`?@2Oqc#f_FffjS${@v zHg%o1B}fF{?S=OXoIkaDkaiI}DqY%|?3jC*u!ApTeb$pJnFiUv|9O!l#F(t2Dbexy zq){o)hIe`4Kr_wK8m!+&PC_sCXGTm}R+)hj){`C^RqCRfFqS?mShGuJZ8!T@166w- zznIb-PPjAkad$ob%Bx8v03mIn|xZ6JOtTjSm(_XQoP5FOYeO@Cj880J*Csc z20aEof@ND+YP@N3fjyqy7Q**PIFQrg%QE(9dfP|7M21M$U!5>| z{dp#u4~a+^zse5ic=F>^-OA&xw3`mx6$r^#p}34@#~2+@WtO>?w+% z%e5`;>ftk230g&G!F6soM(#!{Y1NRfD&k{r*Vs(z()vVqXDJ=H#m;AyKmm&{i`Wka z-}I4f3kOPAIQ(S*|D__mWa+|zk$l}EfdPEN&KQgqe;Pg~K)-7xwI6vz3v(`;K_oNp z9=?DcK&F#OPsaC(r=^>^4A!;Pk2|d6cAg6rhBRuQo<*Y0QP#Yu2+14xn9NzBrou$b zzs%5|xw}vQD9Qa0mBY)5%Hriya7Mw<4#gz?ksvObWpb_db^VnKMf$z!!>_O>gSWbj z9$%EyZ<3X@cLv7FnumWW`h%0Mo-%u@&j1ZG8mO1aR#6va11B-~HIbE#BU^go26z@< z*0BFv86tsH7GKt}|BaDZ!RAVDOW@P+E75tjy*Zp}HY}vK0M=yXT663&q8R<8rMT#Z z9yJr*A~6vdyz-^2fg0FAykF!xF4WsY-JG(ieo0K-V35I&<;_#D4Qw)1*WL+e+{sS0 zYr?Rp_=IEe)B)r5;F~V8ZIM7Ui>FQ)_!du{F&qWC7AF)#;dSFq^@Y1gm`27h&v<)^oW@pTIart`4~2l82H zIAM?s{^})TVeT-1_4WumRk~8tyxVi&9n%A&t=ONOubox;``tm@vSDO0QAM;Y$D%3P z2uI!(JOg#!rP(c3GyYTJ4MUUh(Qege2%i5$(fqpebw8GxAyg z_?&nf3tyh*ZZmZ(0XDEL)mKt){ZPqFZ^3CPP5^J$_=Hsgvjo+t+B@GXk*p8-DZ@vs zt1sQ$vZqk$)}ChA)4lG&a)0!KqVhjDhOT;`&V`iG^!HPz$0!^OxPs~ur=p5Bt%{c$ zm`|qs(5r5>>%F}y?4K7M#jJ@AZ5`h0v?eYPo{ws#Ep0h^L70mgRXo*b&IJed_r3A; zk6cQt;*?YUaGb%XGlPL%0i}zlT78C|{ipA=*|)GGu08v-9Os?N`HeoGyd&mvA}8xR z!{S{UuVxXe(c{WWDTpX0Oy~#M&hZ|QFXMFH?|dEUPP)e<`G)cx>ePu$SpKL}eJCC!Zfe~T&AA4aGX9J$Rj*r}AgS-lcT(t7{Pkz8 zSN*0QQ63a`AM8;cF8AxU&KjFS;YxtQkf#ZLxjdIKdd)`b(M}6O9PV7 zWm{HESgG?7iKC54YR@zOwyEpjH9Io>Ikl zmQ8Qmj~=RB`@->mf6iSWl1dIqqvGGpVaUIXT7Qasg8Pwj=s81a(VKF{0GIfzUjS*IRFLVR*iG)ETV;C5w1s0|%wDvGUqsMa=bCq-HQYE?o6Xd;|4PoN zeuT0oa6-4aP57Hf!~yQIL?>qCt4R7tlSsnIXwo9mtQUhQ#4xTGk|qZ4eA>>#tDM;P zff-w3`Fq+dI;32WL}s5*Sr@;~$-{AFk58R7B*^97a$#Ru>N&|*^H=P))7Kz0BA}zM zd~NmlVY40j63(vp-ycXHWLL20TQ6hlkiFu-&Jc*k*fsj3=hw@ggIbyD-Irl4k)!1b zgs$tA@!^x4qSk)t?n$w*gRsctA9Qm>wm!Uqp8xXBHI6umUx)0e*b(2EoPS(=-a#Kn zvE^CS1F5oW-=}xJj$dS6J2H!uwD4%#=R3Mez;1nU-RrO#ekszt7Qy-GOhS`>QE8G? zs(@TL7T11B(1!I4G@#6Egh=~{z_gzTqVAfz%(v$|>b}Hvv2`f9)Nc-`$Bxc2kDI^W zkKDh+brTh~`nHPynUu3L`d@DIzu&J)c3-<_mdMk{+i}BqaJw8R zmCMOD?BET}D|R5Kip$9zcF%rbZav!EcwF(oCSiUh6O)77KLH*}SS>Mvc; zym5c~=zn7{J1&bb8QEco3Z82ownN`+JEwaG_YY$`8TErC+esv9#b_=>h+QPpshGRTu;(ED72LelrIG%Zt9C;=nOE=g zxAsK6S--B>Di{pdA)LxuCYXrdFGK|AF6EwcdcoNZ{f3e^jCgzo_HjDULhQ*}OwKbG zF+)CKN0U%QSvF`-@5Onf-xK#VZur`7`n%dZl9W>6Qd;_B_c#fp+m%FXcZ5wwU|6R3F}CvxiiYwY>HCw$It&j=jKl{cS&H5x>JE zgc)0kSKRD8Y>}+PIfMjTim%D^Ja|zjWN1D8V~aY5HwQA=yR>Mv>Pv-RMxRv6KJ_Mw z)=4>AH7U9QF{6H#JyLG#*o1{>QgnUDXtntz8=z_Kgis=S_x*&voM>}=t4F-e+kdrkx#{28J^pdpSFvq zxwsf=y&Fn0Mi~DQz#&|JHlgG^&{y_sk+(X;&@^bG$wmG62?>4A{}CXg81sSr{Fj9x z(fOB_F$pbTIsI`KK{@{CSm}QStu9iSUb(L{B_|{`RMLIfF;fuYn3AIzD5s<0^QJs8 zUbTT_x(G!LkG?PKw5YMT+prQ(X_inE#rz_tei)>a?z(F%NscxeIsrybz zpU!>D{JT*?i;rAls~jv59=dMX+Bki<-`Sjq*2IA0LSPHO;FkD4^-!$K zL6-XJdad5>8*g{AsX#`Z%Y*NaqLt?oyo;XwC*fT1;KL#s!rh&Zxpg036VzJ&s(Dm) zx#jvI-w7uiba-feillj_XYZ1Wx0`;EP{_q?9`6xaFC3d-A%^xJ4Ragh6q@ne;a2VI zo_ev{3qNprL&it1cjKY`R0+DPC(Or>8;R(PnTuY>6yl-=q3q^(q$!!Z&Vx2AK-IPb z6#Oet@oz6#?Cu-J_i_qX`?$`0bHqO3cc4TA8`b|T5nU$r_u5lQpMT9hAWLJie}D>s zck!sQ(bjCdx(LV=!AHg0wS-}5k^;T8n1Ay^=N26`R;w`33yOnhQg()CiTL(b#o1@5 zBiu-%j#)`zjl8Q^bS}awlow7IuBRJaZ;>dwIQoEDV+NS||}u_AKA7L`S_TVcpIyqQCA&{r`r1 zCyGLBrv5+T+}G&8A=onNby}I92agHw40y>5m%OE?EgOkySCVSzv5S)Nw@S(*O=l~G z7o<1Tw~07Yl?&^w4rso&7f-44t5e*%y6DOop zcsK>8B(NQxL0a4_K)i&Ss?*^ZnmX=(WMKWzgdpe$uG~Gbd-rH3{`Z6+{y!6fZ2l-_ z-l{(BA9|g^Fo+ZI2n;K+7O<7EY4jD0LepLru2KD{dbKAuh{MEy0r{5n-S0ZndEV5m zhOpqMGk&eQ_o8O7 zLrrhNgKbX*A4k2D4Y4~oKJByER^Ki$A9f$CyR_WC{tOh?d{-)Ve+F-~a=hBidlc@& zx>`DS{RrvH9+?oS-zxGDE_51}@a6LK{;T*)3vYM^_Tl!L5wa@$YHjXAv6FU;s#QzR z4%%`l?Bg#wp^$XFQPraxs?le&xTq2a?;AjgPoLCA{W^uA*MBw~wfD3eCwr3ZGrev5 zem};pHg-w=s<>z73(>g1>zuv1rEgT2is=t&f6+Y*nxoC$dkT%{vf?E(RtojPU%roT z%u4rR4@B_@Ta>IQ@+s1Xi_D7V2}5}x1G5b6AYbH{p<*hR$SMj3E6Sz3_*+cgm1ES~ zDv(>*bnf?)%m`dBopfRhKqeLj+vGSg2GzrKGV}?Mkc!tMY%j(tu{OmO<3-+mIeJFP z6@i8k3&$+&?HQoKx^|`K=Iw+3T8On7hH+bURTX#PLnXe8S_!F<=KRK7GhlKn1|WeH zApBW@SW>e=&kr}ZsVT{xRw>nL=~$1p&M$m5T!mjCeBJNPSJPT=;%*m?lI(8<((aCr+*_}`Q6?>` z^;T~G)ckg}YtO$zAVUARBk2?O!qdCJjV!5ozq<)3|LfaIshY0pldo5YI`p2lJ=;nF ziTyT*I`FjJ#ND#jPP4r?uiUv|?nf3bXS_AL1t%K|5yLL-B()FsR{eULIc)Z5T8=Bn zKHnP85j2hXj2Ihw-|YvtRPI)`H2PQH?vAf}2{~eInCt=0UiB;?zD_PY_MHgq%D?U2 zL3 z%LVIieR=IS4#eM^K&ZK2MdNt&m#IA!HYl^;bNM=p!(Hq8Ro8fCaPgh*7w=CICp~hw zBwKk9Dz0&hG*l-%A=KYlyY~%u%hZLOQD5zL9jYYf@;ZK8x?rKYZziI;k4lIbl&RM{ z{q8x1dJvrC_di_38_xE=O<23(H8VEfjq0kWXajV!E&>`CBI%F<{kmC~0p7p9Fp80) zs)N^=`W|A9_FU%d&tGnn%ziR=xts8Y`&|ZZzrvBucYX+GQlFpGh=@9NaaUD$b$&*M zS#RMBt>mKFdTm&(F6B1=+ITD6HggdMXpG0K>>`S}f;A)RYVEor+@`E)OKc5=uuANYd}oC_>q zLr53&f=UbuL`y)yO5hR@b3zFaIyg#m-shr7QJy*+N;K3-KivMkhr|M#;mAE=waKri zfJOM~(4@Aw#TIS(PGI<{*Id9dl1f%p-hdI+Bf;t@Y8u<5QFV(m?bWt!p#9h{3bM4c zrn^HEBY<|`Q5vS$4@c~lJSpUrv5jIXBdXf+wc9ZN*>%!6&E557*WE?ss{ep-OWvJi zf4{MK+Q6Qj-Oj1-bPKl8_>TKSLXIo$_wxG94Xkh54L1Y ze@N1^9)$tTcck&KaQt+*^W6&jtF-lV_{`Lj-WMtdY<_ltn~E7$r*TcGCjmwo4_p*^ z4L_?TQ5Gs!Z+`ZHuLv6gRg&UI8H+YQTf-=J&aLK%lL*SoKBVth5a}O^O(DEkqgUY*TVZWVpLS);ezX0%EOxUZvdp zVm*1^J7)#xt>NTZ7Kc?(DA+LDUuM$`5g44lidh># ztlTpjK$u*-H2@RBG8*ax*(k;*BG-iCXYl7L(TkQ|cFGo5=aJxu(YA!WwW?YvMMS&^ z{4z;%Haltaj$K=`7?41A@e|~PmijSeo}XawrK{Bq_dLkp80)DMol;|-*)ok)Bi=F%Q%gje zVrFoR_Bds17snBa>Otc&T&k6r#kNdIG9q8i>Jn4p7)Ee?@3s0#{e(k%O|pndMzbr$ z!Oz)$LOqDDJ7_|?Vp-69Xy8R{`Ahy`o0z7x32Y%1Bez%+G10&!nESX3$5wSh;h=i* z8K0y{kBv5SC7h~PieKcbR}TJ?<>Atn$ULEFf zVSJOYo2r^oSz3A%!`xMz&V&-TC+vN4lfIye5q&VB>&3FXnwsE_9#I(H_|JXB>O;c# zPH5{bxkIsGtyD|N z2EU!|YtdOs+^0LXMt4*995wAB)g|&zIdRCxqQ5b)2ni%g?QLD_U5%las~SoTphq+P ztm@R2NR@VlLoTou*H*XCl?;o=Y!l!az+OilAwSKjKozkEF4=^n$v``$HRe8 zF;>?Epu0AbONmOprK(nG(Sad*`1~_{YP!yIno3>fq{^>{Z1y;AYbMmjl4qmTjNjzF z!S=q*UiLmcoTGOJXAjPZAASlOoHx79ViKL_zMe$9ZyCJDi9>+J2pQGQO`8ZC+@8%Q zi9leL|B7M~ZF%gzWL0M}p;agh8ru}i6Ix(^5C0QdhXF$5Z&-#M0cq(Q=f|NvXbOsF zw}1)Qh_VZ(bG-fd66XEXYc^?P7Pc+^6t>~8fvXrseH#D;gymNS z%XgNUBBQ|Wxbi+HomaQ_jqf%?E9bh^t%gk3CN3Qatzn-MF;0$*>68f!qwa+KEs)2+MdV z0fYq+ic79ph`&lctVz$tp9`cHwE`r_xr~Q$k>ksk55HfF;)oxZrWNE#b6kn5PRV$% z&~MCGP%UMk${d&wHGZ3EHE{v}%+> ztU-rvEPasuT3OdTjRNxiFV_}dJ6KT9r{K|Z1zYlYjjF<(j9LF;H-B!k+%qdcm0Y|f z04nG5Gn5y?G7%a@u9<_MKt8NUU&5bTBlnxn)(~@Q_n7Sdjv_||M51ui60IQTkpi7s zWK{ns)GMXPjGqJp^U=gQiNEFA;>~gVR>jfBajOKBO6&nO7uBNOM)cwQxw`a9 z{J9`{QAR0*tchK2LeP*RbUR{#Hrl!YYKy{hthfh7dddBP8 z;m`*Vma)(}@?kal>3_S`*#Jl&p&b*JlFstP+>soki3_2#e5uyMc?gA?<f_J0xKXqo@-c6d-02fEUqK~w@+ERj2zv7>SrSm(L|HWl3=qVfc z-#GJZ2-$+AH-f2gat|<98KQuM&cIRv`PWhD46=ad%m=Zgfw5YB1|n7tOr4l3`MdWb z=Lu3^>t_jmqxbwsF7KN^o>D9$UP(m8uS`q$=wzi0qs(RLoW%Pma=+Trhw?MlPLJ;% z1(fTTf+!N$G@vpG)}_Fm`2w$M$%|$sA}FXzDWMnyRdUsU@+Pq9K%>~eIlu%qgknJn zsHCQ($Ng$4#CkY6J83 zA87lQ4D$#SF#X14a{#0>=Y&}Ec`b~<{_CE&hW0!&w@Uci(^a$xneqw{u2xb<@ zae_+OG2?#iU9CxSOwlK3OkGOq!cjUP-ttW7^{1-Lp3`<1<<0F6k11E9(Y@IYV^t7`+$9fW*RPys@! zDH#@wWe8*}=HfeAQivVCW4NbD;N2BV;_*^g^-;Cj^+z6Wv4fY#sFNWGcTx`Z&YeQh zbz9p$Q;WniiM^}>)+xYDBkh?b#HoCje z<`zgh-G445;PheC&=(*#s*!QHQL69S%4S~FR27oyINdGz&N*UNT%vvgeK%WAN70ndSEs4_d^)+cFQa%?=bz9dV&RhaB8!EGbN!2@ z{@`YW@Oo{PoA|oRXQM%@6nfOn%;G_;JhQ-#9`k+4X(^f|r+^19ml=rfRgX4t`JW$k zkzHF?ceR{O3Q>Fis`^BIHEy!tx$PY-aX4ubEpafZ6@Bxo%7WT^w~9vMwB3YT;xw+y z$aC8w`evz$n^^J_qgZHx;ZJU2cKSH&9?et17{fF7`3Z~8*@e{PAeqtO=^e=dby^OU zb+UNpxq(XNHXJ6?U8!^Nz{2LmkD{TdMS5be%}*UP4JkD_g~_)bOrk|g98cC$Uk#cp zcyGscQG0KPb!B>OV@69HO*%*4Y*dj@dv8`{iJ$hGNO^CQL}z%_UEkwY5lW#9@_2PI zMmv^t5}?uCbKN3_K1md$zxd)2tiOs^skQ5t z|GXrv+QgUBo+G=A9OJdNy%~a2SlV6PoFm*On}B#H>G&>q@VWHcTEq zUT!%THr{Lr0oZWp6Aq__j|W-~0BpGQqjO!;FxP0l}XMgA1bI<+VzWe@>`OQ4fAjo ziI6G0hYwH|GxclCqcOQ)4=0P70oD~lku-X|@QkRL zJTM2)?uFhPXgyxJHQX0aJ~dJhov};fRAI*K6Cahmi=Rxf&N|d0;I%sC2(+Wo zdkt?PEEk2h5SPb;V`S{;^wiPJ?vElH?l-!IQhpKuKy!L2S<-{;%q6OB>Kn z$KR#Y0btLxe<~7-#sKM17!Z?0022go>pz2+B4@I}OHniOb^TC&KzRvR95SO^w+ThE z=wZV3B4-l8<-k4NIwK&P4Bbu_RK9o9CLVGwB_FPA#(*cY9$%b@Tz1zJ&=CrvAQ!|I z;PHROw-bm{CyM@IylhjDG9={*HZG068N$4};h&NG8 zP_S5rdyNsFO}d@9?c0xc532J@C5~b;%R=src37@OGNzJcmvEl1H$`OsKo%R#HJL9m z=pr^bvuXv%;bf4Rw{?y(d#ZJZGIk7l=I{*2j7A+5(2hyZ4em=&{sGJ(v!_s}1x2#! zaY2*UqJF(q^Y@WkeHoj%4Dyb4Vnadis2Z(b26~J*ozC^Ts4pzH&SY-;Pvd0MI?j`i zzqp8SVI z$07?aKe=8=QVa4Q%w@v%)u#&+RLBKS+B<(;U(WhY-h`CKW}bzK zmj3p(|IuOw+fv@W+@L;>lYc{j5Jcpo6bK-*NQRE{bq?R_QEjqk#wdwy)jCosvOLmr zvb9aR-zr)GV|blq>u%lSoN@CV+#Y{;sMok03bw&r0t?zKE-1{~3mK9!i*(F_Y&iw(Tc|RS=F=;8cOzWJ zL3xA74e}G#C?C<{ynD~pO-G3u95bWJnZ1hczGv;FqrRP9-9g+U;)XbjBzNte9AEtI zmo1hn0N(@=x9x8biTIi13Zm3ku<%q$c6v4Pqx-alo%@yzJ00pnHot5y2}5KTp`m51 ze(5W>KBL-ro4N+w20iAqvT(h<#96Fzt-06Sb$SCDVXyb`Xl* z>-0kI2Cw+a@|^}!M(hI&N-P^BDnn37)hLgCu8w1RdE7oYy?mmqYIG)COV4Ug zT@-uFYo{=o7$2UFkv*ZArSLalrmUERX$q(=IO1OZ2Z!@+SZ(ggI1xCM~s5FVf~lpgvJ6tNvzEbT7q zE>))^G|R?2@4noA%bvcf;nfOH++X2Hneb>S7xewc{Zdb3rNUM*l|=Mc)V>NFtD>yF zT;M9%Ci$u_4n*Ro&mXs>Q)*FEQX;pUq!=Drm#HRxZkw}sl{R=3RE|11u`F`qWI<-( zd(YB^L25vEq0g6+SAWrUn>?soV7Yn;_?&%pl&T^57fK_kx{wwHI&BqJfMblvfe)a# za3c6~I295kQG+32_C=0nwm%@MoY8+tJ&>kQO?BnJ8_mp5^g9+3^QtNJn4r z{S7>@SbH(SFFVV(QXR=uDjVC)|AxqLPegh>g@R2VCzjVmf_-f5QfO==Y-^o_L5qmI zD6nW(>*?r9vux#`(F~Z4(LuMX;s0}(=0ihtjv9h&1YBg6d@A<+M3oUHE2>$v>%n@r z=a?yJ$^9yjr0k2oGt4#X0dHf!eB~JSg{@=E-p%icc=zTHJU0Q=yguaCanuDMW`6gbu#QSpF=bI19`cAawcyrH5 z4l_*FkE+ywD-{V~#p!;}eBnB@M-`ho(vs-u=1V%)n_RJlsM=u9B z;G#jKf%-sEpdSzbB>@Gq$hf~sq8$cM0tg|BKxQBzloyIgx664<2=bC@un<6gsB(x{ z=BBKSU$;@vi1AESo!;Z*$ZR%xijJvHeIq@HgxuDbqO&2}C4{m*rF|6|s!d;13u1W! zcF1V){Znv0C~y;YbIyTzmg@L~rA!^TkuZ4904oC1G$5fT#5v(n2W~@5vlTgdR2%i8 zxzC5jbKVu4cPUSXYGcaw&ShWOO=@%s25Y}FqJ?rmDWT%fr=VySsU$iPnqk}xNGt>c z$Oy%UV!%n^XaJ<_#c_#XgZ(VIzCgWTjjfpzfo5KNwL0>r$%sK^Oo;CXoI?%o*asX* z^IIe&3=;yLncbCKsqbbAO9G1?R{qugCj`_uUR5u$ovTu>x8EIMM778O)gsmD65s5| zdlAG)fnjV<>w%<2N!B^nx!{rHEb0i&@6O$+N}dVUYZ zXS*n!PlkCwZR@t`S+~?Y+m~)Zvxl)%b%EB7YC_$9lCr$Aj>gB;}7W*F`1f@G%5ag@OTG3)ZiTak^=~0pXnmfHHwl6)`S7Xs_vi3it<8S$k zWWn+DAT-3`kNYyUJlnumF9?H4jT)Xuk*5n6l4mMzPQ2LF;d8(d?ECE;stVRjeo{ZN z_3u>wV>M9}d=w%=SSuB2N6KOIM$x4#Lm+$tL9P3MTE)e`+{iS|C)U7jE-qIab(G(H z!!8l^ezveGv!8XW3tkau>160YdtA2j|M?4Se=%BbEzSQ|@8s7CD(xj1nt} zY`OtP5raSDwXZgWr95}Np8W2sPScowreeqcG6odTS}f6XSM%0!s2pH<2b?ZqiN4<3 z+QmZR{bWvFm-s2&s$&e!y2^g<(Qat4UH^yd=l!A$%ziMJFgN8@)4<4@nZbAP1-2}@ zI&Tnk+&_IKywW65ZNFAs8sBud8CVrZ=PuGkPTa`wX6q#Q207`y-oFzr+QQ=n{6pJ# zMd)ugRhfpwq35o7!k`5Zj{`$>($jz}WVd;qH z-OQp!CGnqX03MVMO*IwR<{4%3OxNPCANh_PbK~9TDbLLt5erx=p-@@qA?5!)s!z)A zx$6-o2t>_-{E=Y?S36f}_goo<&Gj}4=4G6nS!vuh3iez%IgJ`PIlal%ZmUAc(B{ZK zwNH?Tu#``Z3*Od9w0b4h!#|gyzzvoV=PJh92+P;9{ME+w=TnXj_HpSx8Sr?aH?`ID zE~Dlfu(8qtr#I*LvFWR=o@Kof0Y(SCH8u;{nbs)oV?9^V$wU01PZv-zSd^nTJJxV>1|M0CHTa47WKct$wG#vf(UiI*uBbh>_UdL zX4kLGDfD6|v3JYtRKGPBJC!JQ3sxoRc`Mc@Y>Y>LU;qFfq!$v!Bj43JQLWM%cTrMr zjn12Tr%&dYJ~J_GDN#9Paxzu1bN1#N5H)0y)3Hdvg(4;c z`b)I2qQEiW5SBmjK{<$?pAFkPQ{mM{L<|=fa$FC?s&lM0Vu`!Au85U#if+C<72Ncu z^}p+tjww7ab;18FZ^}@Y-E&z9j8EDbv40v+_|pGJKr#3MFxi0Vs)TP$ZaEEh+E)~; z@cF3kI?68*v<4zjS%j1zs#(PYFaS$SoCIf{9u=Y3V>y^qXCOfdshMmvP@*wQiw=_N2cU+OxUwiO* z4)E_$!rNL^v10hw9|~`A>z8w>CA4&($Au`ToI@pm7Ue!K6{sUyFjMJKae-uY{(3Xp zPLm@bu&(iGYu53HkNPO5cCJK{;8C3b>gZ02QL^@L1O+0IGHEIY zF)%ZLTX{rJ?V(<~*es9Ye(w`>8Nqs{Tn+P;I)^cVpYg=EbF#8O?de64qJ*F7n@u{^ z-~U3MsdvwH_e9YeATL8BZovO72zA%}Mp z)JNY1lSLbu_W6QVYRnN&t9ErN9p}1co;SEued&@$Ul5rc?>c)}w)%&{cID|>V3it=ajNyfCC z$LnYFnf68_n=$s!E!OiN~qR7S7kN~mF z-HyZeVPyt;!ilQPsG8ew*< z5)pmF>TWJ>!Z(bM>8d;xtSY-M1R8vAC(xO`a(1t~g#rH*lj%~H)-h_saPd>9!_-Tn zHK5I>;bCw0FGIwqu?j5TAc`N-AMZY4fBo4yk#zK_PuW+b>^pAP@+SU?iwVM?7my_u$idl@@1&-6FR@aLy> zkK%MUfeCkMxN|>0q+2V4+q}PSdGn%BX=RJ4uRig8Y!vr(uNvvQ-$vzEDfa^jX{U-n z&?9{*+NjbI^yj`5?NlESdB1$K+NhEd>-)$iZ*dmeODfMuy?#sX2K&!`?~SAN*vneS zE5~Z)RsyeX_qfDVINOUOy3ggxE7Faoc;LO&VscUb_hQf?P1I89PfG^D=YkqXG z_&tM9Y*hofw?`G6W_PLPQ~{LZYwz3w-M4S zCIrE=k^1UD$quLxNx&gGGlqKHnML|eE9wT_iyVUxkOx0A+85vT#r+0J5}5|Rz??1? zwlNHbq{_Ebj+KJD)n~r;ZB*|O^uK)V+o?n=2bT8$KH_GTa%)KWb}IBrxlQC?JC$6e z+$xf#or<|qZUZUgC(dbv6RFn`o()ZLA;prt)Zy{@JgG^}loLFIElYW15B<0@w+yIn zvGNT8;j1`vi?nJIdpD|GTW=iX$~pUXc~5!!Jju@{?|d^B#^|m=p?%L!ptN*WKhW`C zrnVbamKSxoHvBr3et1RPaz?B$B3mhLIMY`c&s!;OIX|ov{A$g*;Y?oH*!RKXq`Ed+ zb@M(DHCXA#N;V!S3#|WSHq&5!Y1F?zP+y@fy zi~}O%#r9-4Pw{>35=`)~x^GhHtO0jR74ItCr@7x8*lr1sZH6Jg&LCt=RqNE#@DF~` zMtEYlL9@HFz(w55^Dp{QMQ3O>G!tC(m1+J(1*&KZ&8Eu$7dfMfDX5|a>TMdh2p3fh zL=}yo*}XmNYj*#V^AL^NJiBDqZuF$YGpES zDz?Xp)jX1+TK`J@oQ#!ARm3QBzF)S1`V{;X-~&v@^$}U}I7Zw`{R&@Yi)$$h^c=CC z#wMaaIJL_%I^Of{9}&IcWgM#0j*2|WEpHT!oUM8EfqCxzTH^kz2a2Abo-&HQg(Oh2 z$YN9OXXg{-TcAg(p=!K9)kxZZ^&VA29#x|lRYPm&IYAlfwHE3%8|rm3>H|uipCF7u zO9V9*;UZQ(Vk}@o@hkK8l$IdLF%6B`_F}62`j&X(x9i20k}1fyah2!x#Ae4hh>x#% zZF%)MZGrhq%=GNy9V%$Cqj;ml7GwH1v9+a|w$ z{(yZA-<=+J5FpD{4)}S$wQrPy7uNL`MrV8^vBkzPv1wPdpU?eC{^ew$f*9#;joZUV z^Kp-*DbX)}Jdp5t_lD|w;@`|liP$isNjS(Hs7qWmz46Jqo@x$z;}VqpW`y`l>WIR= z6=4i-c;jGE+*wQ9g)Dk};rp@XAyq`$~mlbh&KgaXEboAM4SxA^yO03+|$WES?l;pzM3HX(QT;(|Smtrxl zY&7aLG(xRuz$)#!D&O~<9+^oZ7wx;*UR)%9l^W3H6FBumOp@wT3VQLz16!YWdQ_ig zBrfc!nEsjnFOUDH?A@L$2Tp=s;=}p|s1_SF@)C5tl|19*lQj`HpU76evUcYl=zizj z(xxd{x!@-;=D%}{T1`IE65cLGE+(_|(n_g%$TEG&^_zd7aT9<&v)m zz{8<=%oiS?@G^RGNtY7{^u>7b%8|@qhBMBx2r*K ztxo!^5-s2U&~y_#W7+=x_Q7HH0IoMCu-&P`7R?TKoO}28G=TfDN?4bBmcW z$zf!-l0gfhB3GlC7+1$#u3KuQ*<%XoyPLtATT<{Sw)>T+xbDp{IjL`fHK(IwjGaf9 zn{gB`%S^WZXsM=+)N5h=$K3uY8(_$J7)=nvVCz!4gOyaF!_a-$mz77Mg zIXpn}?C~lZ?3wy>IBjjTx+m9;YE}0XZl9~!tIp+PwMIUvYt**c1w-LaYDw>OPO>u1 z(PUyjO%8a_66$VzJf|hW+od45;lAndOt-x;W9%wP(6j1CtI@XWP!3vC5tA1EW_j1n z-St&{KV|`2?DO$80~%7?!UYVeNNL6V~WBg-9%j=cBQX$X`CGrXe0}|)A`g{2T0wO zOO68X!rsF}68jbLz}l3tJ*mB)$w~U;|J*ja@sDk@8K`Zu8{B>;q2bONfzzc6;i@^< zjs-cT-o@ijERt)Y_gzm74?DR#mPzCtw`Je*+O^pDwpnKcFPQoWMzg05Q>eXfrEd~% z_^#zzJTLxcj7jRFAjLx77`D;3Yyq{KGGk4TJ!w-TLJS9dG83=Rb(VgbPs9%e8l_(? z(A{7@KCUc&H$2TMOG)1XLJPyu9DAplN8gtG(nkF~lt20+_<@g+0;4^ftthtnk2P<+ z4_&$n_m6gLeOY(Qhe91d4n_5S7ivQuLGM;FmOiMyu9i4jFXwhN|2)YGSp-9vPrRoV zjJ`+AFLjg~_oO#<6{Gi+st!y3XXnle(2qNnP1rErx#h+&GI?vLmb_~=OZ`bLnPFEE zU6a4(5C;9F&;Ns?L()j-7Zqhi2alCM|DTsVreUN0rZuqpZQzTn7 zZqp)c-(tAd_v$Rw_2kYv-d~Q}p~O%)Z>1K>cB`jq(67lpsSh}hxV55w`TQEr)N7-^ zj(CV7Olf?}+F-b?jJ^R@GktK<@zX{U6 zV7H{;5D^P0B!Z99W#|LbW$LyW%sMw~5+SXXGIfUxW==Exbn|+^W|_KL2u2imMFDjP z#8-vh#SJhZn;lxa@xEOLh)|`Y@)3yuvh$R*nRdvHIg7bC3i)u7*N#6&U+JhENTs$7 zzgc;qPm$R9Jljw9X1YrTUJyK-{YiF?Ynpk5lTfXz$tQKe)tX{cs!vcaAuMX{uVDF40$|?6X!~%?yOIq`y`8nrkAsohTIX z>2g2VYRLKgz8NdyFhce5n1*;gAApYonl8iF2;qqW!xef%te62f8AR}wLY9U{MAHYi`Vr&Q)>)MHbrXFI3q5 ztk!#}6Mps%;M*y3KN$(fk661BuU_pXCQeigi&Eg?WQtil=I&z<_F{u*Tm+o+W*!|E04s~4loWs*>fG@^>DAZJAIE;v(g4!eTjwyDqm=UB(Xgyj=Z+H!pdzq`Mc$#rnJH484ViK&-tCxAG%|=#}Al17L znRCe+ewC#g)LsyNl@sRVFFU$m#vRA`AghpyI3^eT#|QOb+F>d{zt#iMX^~n8%RfzuECaQ>!9YqE@=LrLeMVb zUImFIf|sBepNHxT(IOGv+%*(CR?UU;O=?}m)eL~>Z5{CtK#A`Y`g}#SJXyCRd=R{K z`>$r2NqW5`dm@etXEG2Td~Hl7Dq)p<1-mIA)}TxQ81Uw3L>0wjJ$Jm5-(s8hVMAVUgnzm}Ig-e! z;{h8Kjk46bjz)6={9dC33=gCi~KoFiJr!$uSZl)kVHz~+4>r~$Dg-1MzJ_Vc(yA~BkEbIMCSRQfqivOb^q}cJ;k4^OtYs%qZIWv6KbFBao{9yT(TpYH zN!cE6LpAzd;M0!)PvZ9s7}qpvDr|vudL(ZP1CLkvGVQGr#~fu5trH0a=9Za6XD#G1_oqPp=DKpEaaNR;VmWiMp0$SLa|W!%&59+T zIiMMmMiW)CH61S4!)osjI9t4m{nM3Sv83uEbrlu&fW{fg#?+eF>|_8oh^+so@jW#- z_^m8`ZsAe)udP89jRGiul6zj(C7Iz$Wk|g^SVvKyc4$*TcLu}+1vTo^<;ANs?s{k3 zc-!SUnzwT3XR?fnrn;$LmV}l051^Cg&$#$WJ=&Upm zv=VPzty8GhPZ=7!*;0rENm2l%VtR8yuK5%3evvQCuys@Vv}<_Ot`~g@Gs!%7^Bf(g zEM=Fvi}lGo*zz15rW~SM%0a2*9-?^_+`DJ8OI^jHWFE|UhxSv3(JiGQFW%k3%d80u zuih|$oUedz@32Vn?Tg9W5`9X{Z+oE?F3ZKSYb17o(d`G%=}A2cj)JgrCLe|MbcwD= z^(^duRq^0|P*fi*K=G1272_Ndi5xZ~KK38HWjS9jG5^GsaRK%0O{5RYSCVOSHIINn zdDfb*FNIs2IB4mOU^TlBK9e75MMXfYC&z9G`>6+ioNUS(T1|08w-kUHd3I-Hm->qr z$UK@sjl8=Ud5%s~l+i7DATOTXaoMH5;#M*b!MxZkF4B{PC+l=n2e)5E*W&TdV!h>4 zqrGE_r`_^+nx7do2dU+fXjU#`np@J?o*my)2oIo0Z&Dc?koVGbr0P#5KAe3qX3yim>QK^ zua0rKQh$~!@6%43ifboIx18j(L|AR4a2v$T+DdNe;HQzKoWp1UN4X?i$cFH?g&(bi6F zgK2s^pH&&wuHeC463a9_lrNI0L7cd2tG1-f`jdw^-Zwo7Q^6vUML1hOQ)#zUa{_)W zz3wTUdJiAHGRIV34keWQ>-Q)UAKZeDC0l`Q)Ew&(E(#H3DD9t zk-@7ZfKFAWaz>87$E^mu7G=Vk!^}uGUbScfT}uKK8GO556HLPeFzad(x_}k3AVU8p zEo&@#71E}fPQC>K!pEzvd*5rP#0oKB`igh3g*qmTiW7&M@e$?#rX_ZWh~ih)b&6&V>Cyot7sYL-mXDDNTTKfD-a zSh+%SriN%DjJxEFX}T+4BtwHaao1kWP#IRL;K5rmII{c1)56fcJKsP9W=L8%=VIq( z?9NP>@fr`tRs0jAFi1x45a8AEvdwc-+LqSgvVC5{1-2{<&2D--jqB+7Bg&+PB7V_B zt}&J*MPE)Mi|0u)rj7B<41nj!v^8TX%v(`p-#Ukr|l{ETP9?2V4cyk;ASDhU)q z*gR`rVJjE61^gB@2y@U_LXlx#NpJNglctp#hcc|-U#?C5e1=A|LOpK@Mxw2g8l^HU zPr-wyWSnWbFTW*2LohM2Z8WA2$@7BPWw5|kmSWoh$)}o+2+^Qf`|1dg>P2pDxMcBn}_V3s6z+9)XH{FPX$;_Usku{4b@@-l6T~r^eeyZa59`AK z_ia3?sq&wThCBS-a!OJ9`Wu@3k9TnKjUU$@h`k`38$KXsRVAGpo+#{WBCYhc$V-eo ze5Nrm*enj2EyNz`(O&(S0(%KB%Vfcu6UqMk)v|w`Lu40dvFDjVSL2ZwF*u{bM5iy9 z9&ImRxO`&tbNwK?1=akeu>iSrh<#!1&Fl$yUqRR-sXF(@zAZVmLWF8gMB>HgeJg+6 zp^b$2G5vUFbOdo;49;=NYo40;GGhz7D3+ROq8K`T`j$$wpUjj)kxv_}zWSNfJ#3XPQgQkspe7u2*eNb1sFqC^ilx(aLuQ!AR!*X6IrqbyW%un9seZZyWR#2ppbt6^ z1+m)#%XNU3@j4n6^#?C;ruQel?z*1f*fkB(PF?`RsN$W&j8uG8E#;CHL^R~s)-{`< zXVV7*Z~IES@b@PD;krXDFo=wj?FW0?#4-DtW|fpV3(QP=o5XVantGLn>Y)Y!UB|=& z`lif8pug}~ex6MfY6rT&{e*J{|vCe9+u62v6+fI4)P++G~+ zX)G^VrZO+bzFOsIz+HnL%C(o&j&Rb3{G24)?`i(6KAq2~I22X;id?Z>G(n`f!#Hi7 zSCrIrCoeR$33I{EbQmPkEqGhndg+rz<`}x)#G}H}g3CztiEzDZ{53aGm+#FNu ztc7;wzExs;ALF|Eu(E+aG@Iu8CNTa2$kQYaVvwk}EWgmmnRkFJOtJ_3_zIs zS3|@cs0laHpB>!=ZxcXyA>*_eQtl7nj6iK%!`JSQ;2c0V z$RUm1D|a0DBVZh^xYRE!I3}wnfE&U2_xvf9h*){lm*ZkcNL>;DgsPKKf=>z z?Uw`Iw4~~Og>^LOLZAwoC8s!PGV2P@YCq}WTfP5ETITZ{js?YpaMHE}Z<9gEA&#^e zuieq%521b#DjMI=?PpMHh!KtNhiz==IQv)gt_P0E1!$Erk7K@CuI& z_1JEogabCi!!xOx!PMEDMc?zvBG7~?_VgBo{L$3oJx&i^nD$Ja_LU?`D7k`bCv%dx z$zc4Zd{48xGPZEXXjn}Jx@C~l`>t7GyY(OVmkF)nwiq_PWM81QnD&dw?HRbPtUnJy- zGD}=afgL%^zMi(l9jd(iT{VFAY-!TIgD!0lpG$g=>oa>+rLOnOSZdG?x}z4@OjzOQBnRJ0zoo}Dy_)h=???uSb}i@TzQ{G zmd^8iNw5hDVm3Kj8`b@FQ`0k8Ya6be?Pa{8@L-i?H&>}EL3d}gQA{1h0S_nq3;Pon zm-pju0Dl)`w(eAO&IDY}b5ZQ9U4*}5+$fuj<)M|-*2>*DtP(cQR_vQMFlKIaz;ufE zJ$11X~N{;F+ej3Sz#=#)U~*l+T1uG&qG~EjrbT5?QjK{ z6lzc8N(e2(@38U;5jP=!QahB)5i)l$Ogf4h%kSC`BgEQV=y{pu^k~4ub0QYYO7Ni9 z#1k=2D`w$!(2dGfU|D$`^rJFaONdHL@h^19mCh$2CbGJt1xt1|*zjYOf=)5E;;3BE zUOjlaFL1m%NPJi5t$s_;i^GG4>L7eQnY$COsO-?mq(>X#^AX6VmXU3nY?cX`z}WbU zCw4iH<(kAw4!tELss^@|x{5F))Q_8ktuZ>jH*r(7sNY@9zUf%=4XYF$r|OIeb|Mth zKF$s%#^~4*@_YH5!kWla<2W%mH+f|X`dvG=HvFbgjCwqB_R$wAXT+KESjY4T3+3Iq z_fM?`{$WF219_u3E=m8Cf(pmZXtg#W^MPVLIoB7OhEAG2*6NklHR~b^z}@7)@I4Qw zGIB=0lMxBSkkV3Bsvav44rph!crfNC3Ou!D-f??JX|Fo2b(PA&SCm1VfuD@FImL-NRmBDzYbIqwj>L z3FDx-RIfH4dn}EWloZ+k#l8s3Ibu09CaK@Nr@`cU@o*=)(hpXEsPwOto8uRSjk$1bqPR=%(NUJFf8o&{|kin zaY!&V;Z3j0Sy_(oIK_6i%S>c5>s6-f&52I)yOUmgC@;e&pA8SVLPBduM3Q?MJ^Fe~ z^QK{xJDJ>uBaptz>+9rkOgpLhz2g%rIcuV{Y%aS@!?#2khE*dUQ``hxKzaL24JwL1 zoV-=Pr#gK>$JGmacV~6PR zqZKJ#FyF1Kv}~QBl3#2*v!j2ne9a?-WhcFHBDEx*c$0}6DZD3>^(sC1AIKcDdj@{1 zCFqW%mhw$e>yZ^6C=cGk==g`kUO5SEZ51Jl8&SY)wzET9;9_I>wCx-+ll%TX~qSf_qOx^pkJSRg^4 zb?CRhgCCHS^sC0?J_C|!>R&h|seK&1-aRsVQjq$a8^OPL3c_MsWA)J{2>pw`e+WJz zy!rH94o%XC)4DF@#Jq(J zKa_;le2JU4{7j8psJCJ8a!5c;C_LUe6ML~E*F_`@knFxtE(xmmPCD>`x_VtnQtc^Q z;Nu-RMQrj7ibjkawhPrROtclY3#qO+jKH;~V|vd=AE;j%TmWWPlGa5f%?RZsVb5-_ zLr{>ZLtFBx`C%#4(l-S2W5L>Hh|_Fs8{a|iT_2nUs|m|2E%R6p?xse6y5r>26$ zy8VmrKJ+|QHqACPT{g`wRGFTguUn12jIUdd-kz^pi~a|hX%PS;rV8;CJ;r(JDRYeT z%v0hR=jo?{G2g|amt&^fjtVcn2CAP+aolxBODb$zotovDo)fT2v~L>Mhkmq=mI-k= zz3Xd%Y&59TR~<|_ACGI%H7RslwYO&l4RiTnQ^!t z^^|LHx5Y>asD3{jum90?1ViQ|e;9u7ki{t|eP=EJZYpC<{fe~O==WfovhA9}ffQT% zH{Gqz&qsFjo3*($0dhsS*;4L*BLoTkO20=@9JX|*M?)*c?#z_D;PGTOE^9VV{XM?l zPdB?e46~&MSM&1JKytfF?Su0A+Ydoq%di73uo9MtBiLXF_$xIqVw+HWF<1$u?EzVH z^lIHnG$J3i|Bm-e8TWFZ`Z}i&n0&^^7{n^BvV-!!AjRz5M8-1$sNdm7G)t-QGCjaj zyNm`ry?V+gr`)MrSqxJZ4tQb7o)kmofT{A?^9fuS$P3}5X$jrNfZ9PEX<9yjpTjAE zn2>>Ko2uk?o0o! zW5OOm)V^BSN=9$uK1E5ti+XtHU+WW|^+PArw5MFCEBmsAb=&KsdPeRvgar&7N!JZr z{jXU2Yb&W2A1pPzCU-OyP=;%O?5$(jD}6eis@f!NF)Fm-X9+C;P?65 z`<72iHYt3>RkW~F5+hUZm{shsMw0QDZ9UHKSH;6{NF>ZILRWdi zxJWw8F2YRDBfI6MFI8gB&oZ^@18%}%nQp>v3NnnMycH53EdRtCE_vP-I{c6;;)nU9 zjGTax?+adfKksLZR?xW$m}29-jj#ZbT`LN3`{Qn2gG-7Y(lBBYBQx(0tNdYtB+F(( z1IN#M(8A71Bup+KtN39PBpoIfk*nlk*{NTw;XVp6>&qJX2hu#V*^zaI@>t*(hu ze3Gq5Od1DLsU+}o(ypKJ{%0Rn&VPTI3E1_5EA2G4ehRR!h-+t*6Sd=OFA} z*OBttX1?nwb^$GV%4^H_8$h+R9jJNAB-;lc18kw{7o{SSFVkwEbYhk=J_#jSzd_O| zjJ)3!*0LrsGVzXBB@9E5ESq>oty1)eViA)pn_s+Jr4M^UB4K_Jyvp|fk@gPIm440M zV9>E`p4jRb9otFAwylmjPCB-2r(;_uwr$&-{NH=;%zNkiX05q1Yn`+AdCq>Gwd&cZ zs$bRKRl#=Hu1VlT>}Fiwqj>aUegbTeSa_W&K9tO34!fxYBagpPbpp4`6~v5FcG0f+RMLq zbq`F%X7e6rI*PVKM&@?fE*-s7pFzs} zjl^rD3f5#vGgPl|hR?M)V+UX_;E_ua(DZHN}GipH{!nC?+6u7LZ6F@3&J zMksehvgsOh-wytwE_^00oEzoGYO@;th$s}gsVft|S#s6_y%C`%JH^Sg2Sx>38^lD!7DpAph4Md39w0BMTZ3P<)u7puj z*TXRUGL#02K`Qc=4#WTka_3an6UWS*_Ph2G5C4Zbt7_*1Nl`O~zGOcSxiyo2m}*4s zj0u}i5ejBTw@&cxnhEE`e^d!vR1%@>Y##kqwql~Y?z6DFD89wwHob20r38&W)jnvt=`Glfely7( zO#p_i7mw+tiyU}fY{6|_ez^!FL$l@xWl+mILPh!n11J{+-;}OO{U_1As8j3O5B;rdivz-qL7_{&6v~VD`dd49L>Kn1{0y};b6}N6ep&3f z=gszMPJM-66WujM)O$ImO!2hjH*nd`3cjhmQLs@dQ`xSE(X^_g?WIP#FVDNab^7Iw zU_m{BB8>HJ8x%Jlz~8aqy(-^Ija+o2YmJKYwXlV8qUy7`-b2fGSah5ps(UX@#T5SujaRa-m|U*lB01qC5M^Ca0b z3G1VHRqnruzNP^}FDJpfHBlWp<#uaTt<2taK%{E5L6w_7Z(2QRt5o?M(`=+l^u{2) z-r&xIhe#-Pc`k5(RCOtyjFH+{9(DB+m(}R$?de2>-u2mWEh+e^8N!bP#3-(Vc7=l( z5z0PGq~-UhWaVG|py=E?;F9#s=SF?{pAT2oZzBCeJCUPvyMohmI|a4xDIaeeJzZ&* zntkW7D)z1IHFemM5RdJ{1-jKLS3p|wh`f2*taxXFGQctSf8rbEs~~?y46nj1D~L_1 zr~d`n!20~W3isE;cq`hPaJd3M(JrRA+vwdqO`v}>DgP6NwdmrOS(KJ^DpK2X%qF}T zW*p$D+wU?`Fisfn2zd3#(tottu#24UneEV9)=x-JHpJ2Z4|FSa%2N9Z^ zseP~T9+$OtKX-Y*Vfy=L?Bsd-nEA=buI+SZT5_n>z1qa|z_T@+1G}UB_R;K2Z-Ob? zuXZ%RD_8=PJ5$zpd(^6n?(WZo*e*5mJO3K{H61UPL~CFqiSr2(^8CN-=7S0rM%x|^ zUcmd_!IZ3RE72~+t4ha@uQ{CX@*h}(>*EXkef3e4uV2^4zcea5;pmDwW~r;6FTZ96 zWsdhg%pGn|ZH>lzv}_op?re+gX14wBWc4s3`Y7Z3^*qVLqG`EV^SE``QZNfhdFdqz z0#UvwkOvruNbrQLe6hOF-qu1CW^HVg!Ns>Hng16Q0ED75S@UogT773iB^Ft;(aKu! z6qRL1LrFdMoE>seL~n-qxoP=*FN5h9LXq2s#=?`F%CJ&C+1ztJ@w*y$;Bt8*SZx9;cGTWjmU~Ma(nb636$DojJfGFj<80+B_%Mjyv>BxYs z_gzR&wm|RcU}mjrBUpc6W7q!fYQ%fFt4(ky1`%y3htaBO$iG&<^RiMc@3kP0HvEB? z(m<~s#nDTH`QMxz@R3Vu?utk=o!EjaSO8v_S0&5gXUVdM0b2Bj!i*W+HB8qDC^>h` zhk?uV8b&eXhXH#Fy$IrSX_7DEw$I@BCU*~lj_veR?Brkl%Pt<~Rh;aCr-On&ly8MT zb8EYOzwqhj-H2v(yW$5;+VgEj;HLrt%%|YLS?1v*pXA+FP(BU59ScMdzl)cW98#9P zAXO_oMLSA)I%oxcUI{~gZa>OCApxL24Vv$W#9}`3k!`Q0?LPvt)DH!|SKNaE#wk%B zP9b0Vdbqj~$BPfP=s!zG|DoWan=?}=<`O+2=wp^q_Pr7)X*Ujf)4^;+wv1AcwuPTl zT}tR?XvC_AoGIk|KY{`Rd57FD!`uR82E3z`{HF)1@BGgGs{AB^1vbAlDlQLLR&)%_ zTQ46TM%=7$U)AW@7^GB?BFZz{L<6dAP6?zmm#0nP#gTV9Icq=&3B| z&hjFR^djnY705TWA29m9y;Z*L*&1p0p|m{{_5bR;++eyEeXRRTs9@A!OB7|*_&O|- zN~bxTS4gJ`%xj=q8P2Pu8!?%d!^EgHXADQIJ)#Pit39F%XQ(~;a{AW}ckJAwO}*Hx z=S#|qo|;^BRlQ;xolcYKT6vd$dawIPR$@gd0kn{o_1&|#;fc0bt@tQ4g+B^ygzOm)lK0Kke1n}rNdMQ$Clw(g(p@Iri9>suI+g-daQPvS+WwmW z=NrmSc_-jB=+7Hd1Z1%FYP_a0;BJq=*jX=KV4}Kv5g54()!;9cEGBTj4P6^wsf&%> zpat978}N0$ZRz4PmE2V>zo|AtC|UTqG$el9>QxWb>1N^SE63_3s+V)tQ`=mJRFKj{36u!!}OInE6s$Oda zmO`J;@7I2O7`AH|CuOdUJS(?-uQ{3N03Kf1@0AZ~It#XB2#{e9i=JKmJ%26+#V>RM zkTlq?MbB-6v583HUi)n;JRtsk1OTpmFuk^Hy=iF8>;H>rZWE*w-1^TOR9{o=a1D5W z;(U-xMidpC1)s>C$)`8sRh(LLJX&733Y+Ave`RLRS-#ONnsC-D(45wAi>_9Tv=?!U z<~%=`q`kaA3Bl#8iUh8R>t!(Mo3UVg!=y9CHK;;iIVZ!)=(ceK!sI90D-utdEYN6M z1x9inHeH3b_sqp-Kki-rx(UyE^Opa}rTkoq_uYBJdRWWi@_Qs_B=O^$SCjRbmFsRX z$y8iy_mppMVIvVUEaE#(V)CnT81=*Uc?bz_IvHkI|FxLoz^rPOssz{&3jn{Qd3^7a?e6LMaEbJLQwM7WxIZv ztTS@3_gX1^^E11X(4IzPtO7H|sMvaS0r+gaUo(}{FFGVc80?kcUoP(=f)yO4_g*$e zaNmeQG{d{^WWP`nHw^n@a;BO#wWIg=yH%pt{vu&Ksnuucz-pPkD?x3Z-eu(gsQyr7 z1@ZVdL~;91+y-7#1yU0fl?vh_mHVL`Xd2z1IGk0K zhK6m!lUI&BbtuGP+ zKTR~`^eFbGGSI6y0NGW9R_2zf4gN2qDW{*yNizQ7VExXw&IsLd{XUpJ{a=sonGjJn z@;)2!U8g|CUpJ3@&mmcFfWun}`<-tWzs(c_m^dGdyPyM%z8TnmrCiRqx#0}RcSDzh z7=({iHU>`blOqA8@6{5SJ0 zr=U7c8?=Q<1aXB-sea$&j|P^XxxJH~3;&%4X<8!=1x*|PFC6V|S7{B481m1-tI$Mh z#^}1*G{3fRW?F#GC*s$+qQ=@HOal|akvcrg*UO$;#;%X2rqAcM^JqQaSNY`4y;^#y z-72iij#1+|l+PrSK)`EwJqN`Z<3h$OGIzSnYaTc`jB+H?e@^EhF?GwpL^1WRwI;S? z8K@07V-w0n`O>d)P@-_R3s5F-zfuWQ;cge9Xh$o0Tx7ycujaFrJRK$tW$TvImjq6A zS=tuxJdo(sl5iP_AL5)cCh=@_1`ybX1U$YLTlEnd#_UyTEbQq}6o*g7AVaCk>cG_C z1~JVdGA}}Z&0q7tLNRrVzy`4;^T0~6;qSd)C?D6L8U+z|y97lJce?_G0e8C$#TK_m zBl2dByWCG}fjvMOrQFYRfn9j-tM2`rTu!AIkHS;D(>nm)%bU|&*-nKXm$`iHYAQ>;);xwcN~6_rSEp^ij=i|48EGB?{w^ng0+1d{*tBd zb?l0S)oTnso2Bn??24S#Yy2yVb>HI{h-zH+ca_p8x=37wLKm5v5>Qp^p?H5K1jE9Y!Y3sxDBM(r{VJMEm_&NJMGg4<O&ZZ=!(1#*m4DH}CWNhZQJNoRqnuE?2d| zOTCWF21kh@B+;tDO&QIizM^^_S;sN~4X=Civg$3w9EDS!Ar=Mu|34o`KmU0@q4`JQ z+=o@9pvDf6pqpT8omfL~5|{_s_iKU!)CMny%zT-UsX+~M8+K`pdtPR>BS5rsv%X8W zzRR$_%ap#$lD^B9zRQum%ay*%lfDaxfeW;O3$lR=wt)+=feV#^3zLBhr-9240~ZMc z7Xaw%YD;!x(of#TxCHy5tknFv{nbAkit1`ykH)4;|?d&LK) zf(_(UtTs+&s3So3mj66xz&uI7ykNk*LBPCUz3TcHUFD+I7dOx3UPv&jo zUW|Nit;Tx|BKmN4OGNnV>G0l+NAH54Dpa{UT|W_HSFo3Al%NFf!9INb3w-DTLF6|F zGUCP$Yj7x^o96R<>p7)X$M%wDd1V&O7RzPn{AeF>+*Fkvt}P)3g1uWCKd0x)X#uaA zqLLkj$W;LGON5$9JpF8rZhpJp-iD!PXaNj}7+-I87LVf0)iwt5`Ud@PH*A2swu3%A zv%WB=`AvBVhd-HIZR3n|IxFj!Dif$$`{bie5}_ zoW5(Ei@-|qCw%Pb{VN;OfJk+!SKgw`XgQfyj8J~7z^EZOYVei>!7532EXQwv9f-JL z3@u1hKQS{{R6nvQ4-mS#R`xewU2++cR|+45+Tlk*KWH2(l#CSqkFSfue$b?T6jMIS zm~KdE7XP-fPL|H*^cJYh5)PXj-RPdbLFYc2B*dVbGEu>qZeBsc?_^*r^%Zx1i-v(?k|U}oaQ@dt7@$@H z9#y7h7*9Wz!)x*(w`N$6&6f7N_#mk$sB!`K9iO@}4?Ct#b`WFModSM^IE<^S+#gM@ zk(1ePE)lXn2ahc}$-v92veYHKFllU)=~2K#C?Hd#nf9+Csdf<*2JvztHYJOA4WT)j z(7=@myI}s~-`^1&-Ppc_KO#7OVB3K(ZGh;tgSLkXs!Ksq1czcV56eJ)T|q;5u=EQOH=u#@NHg*uchE!p4}vP6S3M8bm09MkwZnD+-1y zZvRpA`=dzmN3kbN(I89_G)yr!R53Ld8^Z@1!yUUJrVpr}KjQNfwX=au?>trXs@dQg+#z(?05ksi1Tx}z5Q4K2&J$PVGNe2Js@qr!&K-g#*Z1hgd|fICQIeq0k>xEhg#a`mI2~{Bwq-7{XP!?(cS=QjKR5d(b4YLlFadg?)wu}puUetk?saacRw*c zr2Ym-*IQVC-dEKQ>hbRe6Mc6RFgls6vN+giV=!G zi)AQ&?wZYoSDIilKo~3BJ(gP+GQ`JzOZCK^peYY0FN^7&e_*v4rRDE6gG;+S0g=+s z&{lKYCt9|@u;H!~Ti^~pAHnvqhv5@uW3JwAXtKyhre8TSwB>eXbtAUrjeo!F*jL`F z_-CajclJxG2Ah@V8D}4Mol0fF^+&>jKEs|{4;0|-?d*U*5(DF~l3KcYXuUV5ukH!G z+n!&YZ(G8i>@aw`g+T%*r?ln)j;)0WB5`5_XaOoz%oo#^!Y4t47lwW_NI)ckg3CvR zicku%-j}r>j>Z+UxUM{djF7@-qH*Xh=?DF%5p}5Hj5-E2MBz-@^eN32T;gk&As=UL z7}8W6a~&a5#lx^t_Y>yeU!&lfC*0u6mR|@)@plNw3$E{F?N}5`)Fu%;N}bObS{{eA zjSY`pCJi{1LDr0CA1|XfH1YDrE7HwwmmnQl$4AVibMm@^)%~Cys8DxO_*2sO+&@c^zY~N@71r1%v3F#C-iw3fTxkGE=cf>Tnhq0PUC~L4oXu zyM^eSBRPcgAxC^lON>4>1jW&j48G?w zpTMb5O$}2e?XYKW(6$y*oS1_7I=Hp8$yaacYL-+bh?PY(ElSz7FxQq(UzLdt*k_TI z-~Vdq*U(f!GNO&)y7M>qd)a^uIo$h|vn@St8#!NBy0DOo=}cG%Vtz2m%mbOVB>tDU z_=)$z$kt);!PKcdg%tD;MO57%7|kkh$uu__h=~XNwRL%Ou3Q`yLE#w9>Ir1<6zl*@ z1d56yFuKiw+FibNl0wkYzWogK>p+*V`jT*y$!&eV#yqZuefgD}%HNYZqz->~?9}-| zfuGV^eANT=iw$RxbALvdWU9Q}l{FW}flOshAJ}h`BiIdmnvq+DQwy-vvJ&oZ{w9Hn zg$d8}M<%7`Z&YJT`BEo6YXKe{w%Nt-5=jS=Uol8vBewj)C+yppux&lm5Fsuh#1Ou1 zV2G@=C&M#B%N)v@1K@_h*QLhYTzifZ0}FFG$OpFWU8TC!3FWs3fAEg!BjeDVI?xPiW_z^fcPR87+%Q zEb~vm?~Z9(es!D8oUu{{QAdJFl$12E$^7QG4IyFbr79d`=U7Ny_oP-0rfj9JQ74%s zh#u@foLV9wEpbN=pLpos7qEi|lW0SND`ooEM>%vQ!#r-&KVGJq!zG$l17JukWq^c` z;2uXAT`pzONT>Lig^2z5fO`q;`>vd3`c4y%u1%ZocnaV$x9Ut1fS~~0B{=#7cx=3$*etCXom7$zF7M{Rv?x343PHH)F`+D?gcx zSw&Jv!sZw2sFng!PDnkS$b^@(_3v&)bgEdMgG5siJ2wcztZ7UbaoTKy8TvFnqb$uI zHfC9NR2XgGBPJvYPOulg7r7D%R=|drLVX~oA802Vhd?pmpGN`TeE?zQr~_L^lG_vX zD~adn9+WIv1;$&m(`1Cv$cASes&S6H{y6RY&F6;ACP{xD1t@;e<=I-BPWn? z2R;k*qZ*Od9Qz~Q715D)KYuHXO)CjCoermJQ@=Dc@9ozQB?*Yu^b2P>|u@{==1g6 z`Smp~UsiOsKxfCtwCC$w^lQhg-B5D;^I`}a>!&gYUwigM_2lhh)8TH5?bFZq?+4<3Zn*zT0{Mx@{(g-De z%}J7z<{D(u`3p=S>7C3q*y%M)%*=(XcBO4Y8F5nn!l6)j;2D}!WyP@frvxqC<&dz` zvx%6e3XbAyCdtADr+nxMXQ&+T-&=sl!XbcYnohBVB$>?mgQ{4FJJrZFYI|UpdSWL# zf2U{IjpMZ-3QdL{$aYrR9z!#eQ~E63T-Jpk%OQ!x%81b;D=2#`{-CN{?o&FgdS;S~ zsAGSR!7+$f&-g;(&;dgg*Yf0jQ)p|j4iSn_fyHuuH{&Lk*sfDISD=}UCUQBPtu34N<`*H z$iPQvu^n-c(F)gd4fYW&ZIFPXQgTo(MkZX)4PC{H>S?kT_|*dt zc4f{+LcGeY%sT0`>)7#FQXENb(uH`^L7s)_qrKclKczU5nO&M1X2$tmTUW&{*bV2O zejX2I^~(KI=>Ag(9v7KwNMW6wW>mL1*%nyWQZ}k_vzsg|P726=5I%Nh$VE?fd3|2b z>UEBdv%YDb>&B7NK*q$&$|CdF&uH-zc4c=dn*WU01%>kt_L<$dxqo}gB)u(>f%4e< zs0rZ_r`53Y^z!)p<;B_6#nPfbl++Xp zh)Xf>tC${Ze!T5#T2$MVVVi)OH(nBu;bIrk747gP*d*-g=*(^}jT|-P4F!f`PdE9O zHaBs7^k&&GQvuzV&d$C8B)`wfccoRn&Q8Nsg|e`UPpFTH)ni2zcAQZ4;3=u}2SkuD z;>`%w#NA^GWa&i}7eAU=(nQr6BU7tNcq%%gd}8(b9aFKXv9by8iUMv!Sk1qb!6XBJWF%wHPuDxYL75VqAJ5u3*T{J$y^CUHE^kSK$UT80z~% z{}{)_sxJxiL;p-+FrPg+FE3M0d#EoAV35!X%plTS5GyxSrN*N9;OJ#yi6{!|%UVLG zS2Qp`-*af-MrQfDoR&+F2XD*i&!!FLnhQO%1z(`ZB?072F=VjESHaWd(MU25aFD>) zSI^?C4;CZm3M!Fa_rwQN3&A8X)38qB{M@q*f-IqE#=apdS+J@X7WJD5qK01|5is1T zMX8O13H0Pg(fid1);b@EnQT1m9ut4kmmQa>|Lv}a{B*b?cyC#(LU$c*A>#)pD~>M< z6zDKp?r6o!%a2JQ_fdc|su*I*DSAf1!y(e}sqXchQPsXw1}pqarC*5ha8v zKc{a}xcUew^=4UE=G{geQQD+4hb=);ydjd1SxvL>txZBhgg|;0JW*AYvsr^k{pe*( z?S8R+Y*)Y@q?r?5{j#CM+s|$u{!=6m%B`5*7w1a|UXn_P+A&UOstI+IMz5p|*8=TK zg&sk@iG_`@^TMi~BBgQ-O=B*t!=li8b<5b4#xF{ePP)0k$dfd|HDW=f8S8d-?$yfx z$qO!~6GQ7(6QKlwYrD{Xu?vzGf1o?hdaroJG>x+otobzZ^`fINQxBNWCVR}rfM1b{ zBy}a{Sh_G!hZPll<9I+rROYTdLFG%#{eFl>Bq}RrP-llWWy+`(w@8R^dbi!xnsps= zT%+5@_51Sie?CNi)aZSV%O4koNe-vg|6~ zLcPs505*m>t@Doj-h$+TD7MON1LToBkz_*>(6s~{yt7;WNyC%T3jA9`Xv_=2+nGqn z6id0O!Xb0omsreqSy)^01wgQp2MM}-J z?d?DD0BTm5Cl~k4xcJ>Zt)5(&ZBJ`jxVxRPLy61>iN49prAUpJ+uF2%hmcE4hq6f9 z{Gv^JW32QfLr2l9?rGsfdq+mrg))mf08TzL4E*M2t!}wlD|p$2es)>icfE)$$w`;n zIPL&E$xQIshN*Rr6L=q97er2VN2oOJ@)pn&cq333#0axOsIyX5&g(g@E&WQKt2rEe z_Y-{k8kni!#SKY+k)4yZB2py_Jo~*l934&<#3^$url^w$IfYE{e_lW&^EQUNNvqi$ z`o5;hN;`uYnPK84{;|Jhg?HT^Rmbg+vFaI?^>0T#pY?rkBbSB0n-X-P))Hn)LJL>c z-r+bjMVopEJ*w^?slw7gBjnB+XR#gc(t?5C!BNJ&;lN9tb<{E$=lt9$QXci{3(3D$9Bi7k=C0{lgA1SgFy)1aGW;>2UHKj z>P*})*q4KBm0@#!-7oDd^RoY;wXs|_-+nzIjjfE@+7=&a-1p-zP{mD{meN^>wzwud z^}>Hn?#ORWu3ymDP`^Lr6fy7wh5%+>nKLiwB4$DUC~QIA089Eps5Iv;_6inOypOiz zD)eV7t)_4;o~Ce+PK66r{U4#PFKpY=H}ioLDnIp#DA=|YO|@-N11xNmIg#F!917TY zlT90yknR$74?sj%BF72uGsJ{VQv~aA5_3z>TDw$TM?Dzj_u`lFRHAm6EYhT(oS=K~ zu?6igAcxTCBk8~WAaHXaWIT)_~fQb2qdx7JC#;r7x|K?i7Xmy6~5SF46ilV8g>Tt5AYW>$4+P|gW7 zybizkins^?moOQ59%2SSfi%xn-w%?P^kleDR2M+1jR#43BKMP%H#(HtF;e265>Nm_ znHRMIYyS0E6Pmq*2tgOtRbVJ4v`AnvniJ_Ces9u^yZMwhnbri&8iKq22cO9ZIDCVi}|-keKG(fV_VgSDSX+$&_ax7?;b}QE5Uhmp$Cre@Zb4I5|0j z4veX&3~t*mCY8bc^;R%k^vJ_77VvxM-Y+tkXqo}LW05em&V5z-8p6G;( zuRiQRb%gxw^bK|o9$P&JRB8gmF-#&l1N4Ybh>HvS>ofv27x?h^uaye^)qj`~8K8GR ze>CmE|I>ZLINg^J?Eo{lZ;nE^PL3n|-i{m2^iFXtZNDEv$cltkWz5szY~XTjsO#6^ zY-n?B=<96)8!M+8(N-RO(gmE}g5?f3Q-!R} zFTO!(I;wqltKItpTCFV?}a#JP2pgKc=w6|0A7*>*`PL#;|OX1?%<$GRb!IT4?fmrxCxpjrLY# zWND`rl?avu7;>SO^8xjL8Uvgor4jv^kbhLqjDZ%EJW2je)QSwFq+*&HoUvFDM3c8; z?rC;#z3dp1IiCM*h5@;oSF_{<;n=|N;~VP{Bh&NpWxQQz>R=FC8DgJ|Xcm$2-?4{g z@M*N`u9W%xx94D9kLh&e=d>%9$Z<39Zx8`d7iexrG||M)u#8dzS?#6z0P;{M zNcQt+$;CR#AkVo_?Jm&&U6kG)EaXnPFVY8zK|s*H;Iz9nqqDh*jmdwM%>TgTGmY_Z zJP!0OtQP@zC%3onyV2wuTa(uD>tx263HS|-XNo^rS>oF9(7?#K?vA2fY3ur;Ljq~xk*HlT&pyg1JJ>- z&E2M)ew!2#LlV7!Y|J5if`E+_Oxi-FN^C&fldiv^f~PkN3E&JNQOeh|v|WCiIaUZE zf@)K(>@Rul-`&1iq0pcsr3(U*l^cUBW*)*F&zVOmR8$}HXY%($tfsH1mOm*(_8~Qd zZF4?ZT4eOS)TfPZMop3=kAv z=5yQBE>%Y{H>UJ->RJfOqJtK5RD5fKiQrqeMB%Chsrj}0LNZN@%}y6{(Dk(X7NyU1 z6`Jm>6htH)n}ZpG8Z6yfRxpfvkD|~v-~$9Z;;1JTV-U*El$2%ENjdg4BQ99@L4JcN zkxA|V2jJzWplh4MskpZKSIK9gQSE|JDfF9Fs1c-8pQ`!&K8gT;8ktxM5_R;$)bW%a z+ztDIGL3JvIrALEj9Gl1A6Lh>AzggFcSp~CdbKSLG}5+Er@9$U}dR-q^ zC)XUubClP;{atU*qsd*L*WC9tJ8Kw>o!&P$$MIO3-uH*{(F7~2_yj3eBw>HAs5MQrB1v2}Yg!1q+5^t#lX_pair4??u_c3CaA5?!~DMWt4Y z{e@UFig3ktzikO?3!93yx?qs10C_xQ%~9J?T8^wSIY9V>8dk*F zGo#$~S2%*8yiGWCf02)6N2Vt1?gF_wAas^o*ioNciJ@NDFm$xngg9{JW_-h@y064) zp3ln|VDLo;lTT>r{@|!NgW{PImk9HBg3fT`Jr+mb9&w5)Mlz9Kf2eO?jOYTT54c0_ zaRs|z;dc>V33d9}?gcGV+r9ioC@yb~vgF(U?z564T#P~tv_qUeM`tv6DV!`-bfn35xluWH>T{&xrjZOuwLFV$AY+QB z=z^`NhbV-uYZj=xoT@(UtUj%9lANn4=CBXTWP^d>zW=Z#+OZ!ZxFQ&Ail1<3gi&0d z+8$8_JCQ2(E1tD96;#2Hk(D^wS$VnD!NbzUqO3!-fF2OxTo=hS!{m>(O~*5y^r|t~ zni?|4BHv=zg6GJ35{ZOjB}z)!>_9aBIiQjX*NTrg$dw)9cauCL*wX1q!jDRr`(1ueqD{gO(`W z0oj>n*qx;ZU=h5It(YZ82z#erGT0L{sqbNpAz%sqti8{xTHnnK%mIeNz`D~1+C)Oio7_U$?E zaPS@``s^_lS&eqNzCU$!imUakbkOkfbL>wxE@GT~*Hx4P!; zXVJ%|^&vK5m&tp3RB^ej$z8P-@Cy@6>sfbRtSc`gQb7_p(-wcRguq*U6svv68Hw8; ze-ApZ6ubG%id^`S#GZRhFEjL55&LFovxl^6_wju#usZ7gd7xGZruB*)@JtTB82zU< zwM)IK#p7x*3F^DFfc-c!VL)Q722@hZl0INjaiwfUO?&pic%?6&2kW^1d%$Mn>B5@Z zqN(|$^$?<-q+adb1d*?{hmZaQ{tuU8m+_YR*%v{r6$mKef;+G%t4VO*UB*7BBem;f zX-`|7ymH!B!wGpl!O7=eGr#_hOKOhgtz~jzQSwD+lHxSQMP$FcC6%U-m=Ef~V%P^L`vWcX>iC zfC7H=JT+X~--g`HJLiZ94?AsMgHx-Ii`5#G?1>_?=!E;&SaLtm)%>-$Xl=Ze3STX_ z4#%<9mznyg5y@qmAVzUkCh>;1o&|!NLH>?baS1A+f%-Y%Okk16?T9YPHLS+)QKIfl zQp%go*lC5S*ig3JuM!#MR}Y$?r73>B2vIB)N$hF>PSNk0kuosoe~p}>We$-AiZt_a zdwYsI)&S*`vVw8bkziL1cB^Xj*pFxJHKdzkqAi5W3yS9l5z=+5T@?uv@@2 z$BfC)gU@88+3jUb1US6fUIPz^vE-#TTju6rl!&U$18cQf=hv2hjP%&09<-OKD0`4p1~A8Au|6 z&1_ib>Af#UOSF>>R8QB)J6JDN9Z!DmP&q-46>a3%szG7nzo+n#8~w|3oU3+F z9GYm+-HLlVJ$6xzc01xI%c+GOVXS*4AxFK?L6b{bIKvUnxhz>y2G%r{A$&YVCqfPU zNh*c(+hcV|gr}?znc3AuB-N>)INX>Xnb9(sY|D^^!J5-5g#x*2@~3l1r=bndGd7)) zKuggD{!|$e>Am;znI+=5q?mxpajpJis3M&$Z^63@wibVu(mtrZ&=F8Ar=y`2N;&bxX-2 zbEl0|i-{tTc#c>!eT`p{kahdzR3c2H?9rG|(~VV@-GT%c(^&&FNMlJcGb}i&b}akZ zst>jVv`SLR=UUo0Umn0`+OGYAP&xvLk53SJTQRe|kDs|+Df(bF;d>G=>QNW99EHlP zI`+XjJaz22P;W37wW{a};hE)csAx4>%CyC*Ji@qNi;TS%xomdNm}uNRYv4WmG?9k? z4#rvOZ(In8R^_U{1m=&OyZL#``aO@^T!|8gwo-bJ2~DU{cDn_b4}epeJaf=%?`Qf- zz>vxx6oz^;$oie=g;r;urS%1Tv*&W%8$GW)1N?N4$@0FcdWx%Hs$=PwgctMj@l&*H>gI_)=1Oj~ot+Gtej#2}(m{&6Gkc?q_ifq3x*kfjyaZaY~PyM`}ex{Hp*KL_tSGX@XG8{#*z9xW-SxU8JmiT@NNx3wz zBgd0GO%<%z;pE=Ozl!BctFCP-)( zPWWf##X@Hn>6VV8d@sOT?{%nP=F+dlr_puqaMeQQVEHdi2JUNeVPTGE2L0dN{5Z^~ ztKwFU@QW4!WZ{QFUpHmkAzHYZ2_MSLD#9jrUH~hKX14LvirIAYFFKW6lTsmv{sTPK z*Vr{`Xqq&}gOrLQXv)4BnJ565$#X`=MQa3L28SK`(sH5dUUIP0lS5v}cn+)Ku%T8n zn@qUf*8)1m0ci%=e!WRvEVi)sCbDBncN;@lWZjB|bfJLaG656c^g>01_k-Mh05AF$ zBJuAXnVy|__(PhQl7d$ZGSB4o*p1TFu?C7n&3I~9OI#+S#jc)pHSf^F{y@K|ai*FC z`A$MSU2@DKOWXu&(%5avH{}~*jvI-r7Y1V_ege-F8ym`czA=kslIIX4Nx{d>Pn=sB zE|hXYiyz87tauVk963=Sd_q^v4WQlGY;KKZja$MFJE&M-h$7BUbQ~ghZO?|Gm19aHzU3T|qJ4{>d_N4?leizDh(CGYG z>T*-={p(7kbS!)PKh!pad`tcaht3Uz(wj?b+tPRr2d8*?NbW5AR*cz7ofM?e*n6(s+)|_gA%$ zF0o%$YxsH~8d^XnqTR!i*dO)pnNd}BRw@m%21ZkUSZ!j9@s{q`#E8~w$TKDgmJI2H zbY`={t2H%)?A8^kiQ-6=2GhcxM$^JpRD2_;0!qU{JzFVT$y5S1TRVnNj5gqS51In9 z8>jNw5w2D_a*&m_LnjS)8+)bmr*f3?^1)S&^T&FzJT-7@zh$e=g7Lr_E_Mg~JZl3w zoHlq?CyQ%i@jY@bFb4`iRutFnUG?6VX6KTo6&rpw*&%8M_!KX){L*|a-OThl!Rdg$ z=*ZysI@?<`&C`VPykTVCmSp4^vb2M&C?ZW#@_XnTX$(3k%)T?x%ah%gX86A*Sc1I} zPuhb60r^c00)qaZx}&*)qlvMyv!jKr*}qiC3{6SLT~5q(HIeo05KIgW#YRXZF4Ywb zspVSMRb};wkrGu15h2tSvRqOLp84`lVZDOMDe>d@J8&u`7XAX2yIupl-65il?5FJ} zI}0Is77i&h%1Yg~Rnk;Z?w`wd1^4>~&7X~;MaUX-sF}d{1);PByJCTltDY498V**< zN)5%aO*d|eG@wdKW7a48^=2fzY5CL&?NpR(|xRcH}ZZ)7{q{3qQMk0D~i7$9b zy4*4y#P+ckG-k|z3M;oErpSe*(Vt+G5>8c6t~a@~p5F%AaoXrPRs|mcJNTY;jts%d zn8^nm)0F^!OL_|;-yMJENEII$M&PNMbNt8L{5%>46*CHamizQ)^iHFJ!frYE8_ z8_RUI)-$!Fbfz#jx=>2!r!OObi%}$j37lxWfv+WdROV^zDM406X^)ib_vq#;_=x~a z;!~S#-6}A!Pp&Gf0^c!nrUVnCl%4~muQcWuUvt;pczXqu3Qd!R0Jswcb8FDByy>VB z*c4&>Q9vr0+?rEO5(Ym3Wen3c-g6CqqAl3r`>{ycMR!B2%Gsmm%}B{P(aHLIL|PAZ zG>i&OLh6UkVDAZ=_?3jPTiuK$Scc|PJDkDMTeZ@-H_ZgvGa2l7!qu7xDU7VTE*rx| zv-h3Dek%Kl(|Oz5=;mVQ|H0ThhF8`$ZNf1-NvETZZFX$iwr%d%>Dabyqhp&pb~?80 z$^ASt-|@}7&wI@LskPQ#e`?jPb=6tts;Vo}R(LFN{+Wppe^6x) zsc|6Y`(w^$zCraH&y*$j#Vs9W37`lW+x}=Kj71s7yIN(a@%jgPm(AuJh34bkrVeYg zyWIhipx1ByL4FI9)5dL%^TxN+UE5R9{ACA&wDacL1KKKiig@yjUK4h>*G?=HlW{|Yd*0h%75L2dG2r9jI3m~?=51J!144nTGI z)NLCHlAYNx8sFKo&m6| z^)-LblIDbwBvC?S+uy>|b|+_2Sw523qlxB9u=?1lx5+r%D#Xa#c$_U=)=8Uz`>dW< zLSvM8YE#Rpr=YZvZ~TyKqagj!=UJt6%fv%%PVeVQlP`vCyk91Y2-_KuPUEn4*t0sh z;4IA_`uaV25{{ci6*&O*fYFd84a9(PIlF*h)?zG+^Gu_*N?R!SZY<0)CevQs16D4J z!Z;`BfwD8o0Go*$!%Yn8;!$LoG~JtdQ9mI{nU={071iSKcG;i)H}RkPDy*R`N70%o z>-7k8WB*5j3k8(8KK11a-h~d*oHQB!U&iwFzQX9uWa6}A+<*77ey?@~R_q{VH0zGF zUagR?hci(0`e+=XVh$#da7&Vw>q9;5TtroLoxA}jm?IE4(s6)v+N*qIZYJy)WZ=f( zgWvqAGGJ@Ee+$^Mp9t`o0Vsdqx4HzKod`m$u;r07ukqR=LIUP)rA?=dt<`tBrayvljf&Z;}=WHG`RG*O+ckCZ1=PO5e9a zVv1dvFjE{t=P-}7hsjO;bt8xCrvV~>EJYwm)GAGWW(4ZbOY34$Ze(2D6K?^FI;`iG z*BzasRYKgjy*G6AT4BwkjPaMVQ$i!F*wy-I+Mb={6}YeViRN57+z|Y}1R%c=8xMky zi=b=>Id8xTap9aSiTORf^+w9}e%b%_xM`?U5!g&9X>YJ1Da9_@E#Lt{u1)k}+&Yh$ zzh?cC4P{8Az^ahXgxV95x7ekTwI~%b<@>ZW-+(U?U0FR*T<=7hBp?xtJP^Mzxo4Bj z@yUupO3?{Xbazrx_;eo_-M(SVERklVrT2PLVsU*sl%(x}&&C6wxxx1pFHa%cPFds* z!#a?qaU$1B)#5OVzLpV{cFjvn)%wethxD)|NE*&Rcq|!p%Rk|cu($0zeubPj!w5h# zWBL8j`@y9Z$!|VTNyGj^`g=>1@AvDO>Cli3Yt`>ZGnn|&VPU%Dzt*NB!G`PuAa{IH z&!H}XglGCEw2_obd*Vi!5JzSg)*7C#zH|BE_!FL0aQ+D9dJMGX9+Vla;erwvgKgSv z_5pES?!n_78mB0A4gshAN!=}AJk-1bD9_T

+gyg4|YFIo;ZOwNpRw60$Scn&J}G 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�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

Q)Lz*ie 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 DIN 18599-10 16.0 16.0 16.0 16.0 16.0 16.0 20.0 20.0 20.0 20.0 20.0 20.0 20.0 20.0 20.0 20.0 20.0 - 20.0 20.0 20.0 20.0 20.0 20.0 0.0 + 20.0 20.0 20.0 20.0 20.0 20.0 20.0 @@ -117,7 +117,7 @@ 16.0 16.0 16.0 16.0 16.0 16.0 20.0 20.0 20.0 20.0 20.0 20.0 20.0 20.0 20.0 20.0 20.0 - 20.0 20.0 20.0 20.0 20.0 20.0 0.0 + 20.0 20.0 20.0 20.0 20.0 20.0 20.0 diff --git a/helpers/constants.py b/helpers/constants.py index b55cf955..684a367f 100644 --- a/helpers/constants.py +++ b/helpers/constants.py @@ -11,6 +11,7 @@ KELVIN = 273.15 HOUR_TO_MINUTES = 60 METERS_TO_FEET = 3.28084 BTU_H_TO_WATTS = 0.29307107 +KILO_WATTS_HOUR_TO_JULES = 3600000 # time SECOND = 'second' diff --git a/imports/construction/ca_physics_parameters.py b/imports/construction/ca_physics_parameters.py index d04c81ea..5b2ff50c 100644 --- a/imports/construction/ca_physics_parameters.py +++ b/imports/construction/ca_physics_parameters.py @@ -25,23 +25,25 @@ class CaPhysicsParameters(NrelPhysicsInterface): city = self._city # it is assumed that all buildings have the same archetypes' keys for building in city.buildings: - archetype = self._search_archetype(ConstructionHelper.nrcan_from_function(building.function), - building.year_of_construction) - if archetype is None: + try: + archetype = self._search_archetype(ConstructionHelper.nrcan_from_function(building.function), + building.year_of_construction) + except KeyError: sys.stderr.write(f'Building {building.name} has unknown archetype for building function: ' f'{ConstructionHelper.nrcan_from_function(building.function)} ' f'and building year of construction: {building.year_of_construction}\n') - continue + return # if building has no thermal zones defined from geometry, one thermal zone per storey is assigned - if building.thermal_zones is None: - self._create_storeys(building, archetype) - thermal_zones = [] - for storey in building.storeys: - thermal_zones.append(storey.thermal_zone) - building.thermal_zones = thermal_zones + if len(building.internal_zones) == 1: + if building.internal_zones[0].thermal_zones is None: + self._create_storeys(building, archetype) + thermal_zones = [] + for storey in building.storeys: + thermal_zones.append(storey.thermal_zone) + building.internal_zones[0].thermal_zones = thermal_zones - self._assign_values(building, archetype) + self._assign_values(building.internal_zones, archetype) def _search_archetype(self, function, year_of_construction): for building_archetype in self._building_archetypes: @@ -54,31 +56,32 @@ class CaPhysicsParameters(NrelPhysicsInterface): return building_archetype return None - def _assign_values(self, building, archetype): - for thermal_zone in building.thermal_zones: - thermal_zone.additional_thermal_bridge_u_value = archetype.additional_thermal_bridge_u_value - thermal_zone.effective_thermal_capacity = archetype.effective_thermal_capacity - thermal_zone.indirectly_heated_area_ratio = archetype.indirectly_heated_area_ratio - thermal_zone.infiltration_rate_system_on = archetype.infiltration_rate_system_on - thermal_zone.infiltration_rate_system_off = archetype.infiltration_rate_system_off - for thermal_boundary in thermal_zone.thermal_boundaries: - construction_type = ConstructionHelper.nrcan_construction_types[thermal_boundary.type] - thermal_boundary_archetype = self._search_construction_in_archetype(archetype, construction_type) - thermal_boundary.u_value = thermal_boundary_archetype.overall_u_value - thermal_boundary.outside_solar_absorptance = thermal_boundary_archetype.outside_solar_absorptance - thermal_boundary.construction_name = thermal_boundary_archetype.construction_name - try: - thermal_boundary.window_ratio = thermal_boundary_archetype.window_ratio - except ValueError: - # This is the normal operation way when the windows are defined in the geometry - continue - if thermal_boundary.thermal_openings is not None: - for thermal_opening in thermal_boundary.thermal_openings: - if thermal_boundary_archetype.thermal_opening_archetype is not None: - thermal_opening_archetype = thermal_boundary_archetype.thermal_opening_archetype - thermal_opening.frame_ratio = thermal_opening_archetype.frame_ratio - thermal_opening.g_value = thermal_opening_archetype.g_value - thermal_opening.overall_u_value = thermal_opening_archetype.overall_u_value + def _assign_values(self, internal_zones, archetype): + for internal_zone in internal_zones: + for thermal_zone in internal_zone.thermal_zones: + thermal_zone.additional_thermal_bridge_u_value = archetype.additional_thermal_bridge_u_value + thermal_zone.effective_thermal_capacity = archetype.effective_thermal_capacity + thermal_zone.indirectly_heated_area_ratio = archetype.indirectly_heated_area_ratio + thermal_zone.infiltration_rate_system_on = archetype.infiltration_rate_system_on + thermal_zone.infiltration_rate_system_off = archetype.infiltration_rate_system_off + for thermal_boundary in thermal_zone.thermal_boundaries: + construction_type = ConstructionHelper.nrcan_construction_types[thermal_boundary.type] + thermal_boundary_archetype = self._search_construction_in_archetype(archetype, construction_type) + thermal_boundary.u_value = thermal_boundary_archetype.overall_u_value + thermal_boundary.outside_solar_absorptance = thermal_boundary_archetype.outside_solar_absorptance + thermal_boundary.construction_name = thermal_boundary_archetype.construction_name + try: + thermal_boundary.window_ratio = thermal_boundary_archetype.window_ratio + except ValueError: + # This is the normal operation way when the windows are defined in the geometry + continue + if thermal_boundary.thermal_openings is not None: + for thermal_opening in thermal_boundary.thermal_openings: + if thermal_boundary_archetype.thermal_opening_archetype is not None: + thermal_opening_archetype = thermal_boundary_archetype.thermal_opening_archetype + thermal_opening.frame_ratio = thermal_opening_archetype.frame_ratio + thermal_opening.g_value = thermal_opening_archetype.g_value + thermal_opening.overall_u_value = thermal_opening_archetype.overall_u_value @staticmethod def _create_storeys(building, archetype): diff --git a/imports/construction/helpers/construction_helper.py b/imports/construction/helpers/construction_helper.py index 0cf05326..32402984 100644 --- a/imports/construction/helpers/construction_helper.py +++ b/imports/construction/helpers/construction_helper.py @@ -12,7 +12,7 @@ class ConstructionHelper: Construction helper """ # NREL - function_to_nrel = { + _function_to_nrel = { cte.RESIDENTIAL: 'residential', cte.SFH: 'single family house', cte.MFH: 'multifamily house', @@ -27,12 +27,12 @@ class ConstructionHelper: cte.OFFICE: 'office', cte.LARGE_OFFICE: 'large office' } - nrel_function_default_value = 'residential' - nrel_standards = { + + _nrel_standards = { 'ASHRAE Std189': 1, 'ASHRAE 90.1_2004': 2 } - reference_city_to_nrel_climate_zone = { + _reference_city_to_nrel_climate_zone = { 'Miami': 'ASHRAE_2004:1A', 'Houston': 'ASHRAE_2004:2A', 'Phoenix': 'ASHRAE_2004:2B', @@ -51,6 +51,7 @@ class ConstructionHelper: 'Fairbanks': 'ASHRAE_2004:8A' } nrel_window_types = [cte.WINDOW, cte.DOOR, cte.SKYLIGHT] + nrel_construction_types = { cte.WALL: 'exterior wall', cte.INTERIOR_WALL: 'interior wall', @@ -62,7 +63,7 @@ class ConstructionHelper: } # NRCAN - function_to_nrcan = { + _function_to_nrcan = { cte.RESIDENTIAL: 'residential', cte.SFH: 'single family house', cte.MFH: 'multifamily house', @@ -78,8 +79,9 @@ class ConstructionHelper: cte.LARGE_OFFICE: 'large office', cte.OFFICE_WORKSHOP: 'residential' } - nrcan_function_default_value = 'residential' + nrcan_window_types = [cte.WINDOW] + nrcan_construction_types = { cte.WALL: 'wall', cte.GROUND_WALL: 'basement_wall', @@ -97,10 +99,9 @@ class ConstructionHelper: :return: str """ try: - return ConstructionHelper.function_to_nrel[function] + return ConstructionHelper._function_to_nrel[function] except KeyError: - sys.stderr.write('Error: keyword not found. Returned default NREL function "residential"\n') - return ConstructionHelper.nrel_function_default_value + sys.stderr.write('Error: keyword not found.\n') @staticmethod def yoc_to_nrel_standard(year_of_construction): @@ -136,7 +137,7 @@ class ConstructionHelper: :return: str """ reference_city = ConstructionHelper.city_to_reference_city(city) - return ConstructionHelper.reference_city_to_nrel_climate_zone[reference_city] + return ConstructionHelper._reference_city_to_nrel_climate_zone[reference_city] @staticmethod def nrcan_from_function(function): @@ -146,7 +147,6 @@ class ConstructionHelper: :return: str """ try: - return ConstructionHelper.function_to_nrcan[function] + return ConstructionHelper._function_to_nrcan[function] except KeyError: - sys.stderr.write('Error: keyword not found. Returned default NRCAN function "residential"\n') - return ConstructionHelper.nrcan_function_default_value + sys.stderr.write('Error: keyword not found.\n') diff --git a/imports/construction/us_physics_parameters.py b/imports/construction/us_physics_parameters.py index 6e19e96d..de79697b 100644 --- a/imports/construction/us_physics_parameters.py +++ b/imports/construction/us_physics_parameters.py @@ -33,23 +33,25 @@ class UsPhysicsParameters(NrelPhysicsInterface): building_type = ConstructionHelper.nrel_from_function(building.function) if building_type is None: return - archetype = self._search_archetype(building_type, - ConstructionHelper.yoc_to_nrel_standard(building.year_of_construction), - self._climate_zone) - if archetype is None: + try: + archetype = self._search_archetype(building_type, + ConstructionHelper.yoc_to_nrel_standard(building.year_of_construction), + self._climate_zone) + except KeyError: sys.stderr.write(f'Building {building.name} has unknown archetype for building function: {building.function} ' f'and building year of construction: {building.year_of_construction}\n') - continue + return # if building has no thermal zones defined from geometry, one thermal zone per storey is assigned - if building.thermal_zones is None: - self._create_storeys(building, archetype) - thermal_zones = [] - for storey in building.storeys: - thermal_zones.append(storey.thermal_zone) - building.thermal_zones = thermal_zones + if len(building.internal_zones) == 1: + if building.internal_zones[0].thermal_zones is None: + self._create_storeys(building, archetype) + thermal_zones = [] + for storey in building.storeys: + thermal_zones.append(storey.thermal_zone) + building.internal_zones[0].thermal_zones = thermal_zones - self._assign_values(building, archetype) + self._assign_values(building.internal_zones, archetype) def _search_archetype(self, building_type, standard, climate_zone): for building_archetype in self._building_archetypes: @@ -60,52 +62,53 @@ class UsPhysicsParameters(NrelPhysicsInterface): return building_archetype return None - def _assign_values(self, building, archetype): - for thermal_zone in building.thermal_zones: - thermal_zone.additional_thermal_bridge_u_value = archetype.additional_thermal_bridge_u_value - thermal_zone.effective_thermal_capacity = archetype.effective_thermal_capacity - thermal_zone.indirectly_heated_area_ratio = archetype.indirectly_heated_area_ratio - thermal_zone.infiltration_rate_system_on = archetype.infiltration_rate_system_on - thermal_zone.infiltration_rate_system_off = archetype.infiltration_rate_system_off - for thermal_boundary in thermal_zone.thermal_boundaries: - construction_type = ConstructionHelper.nrel_construction_types[thermal_boundary.type] - thermal_boundary_archetype = self._search_construction_in_archetype(archetype, construction_type) - thermal_boundary.outside_solar_absorptance = thermal_boundary_archetype.outside_solar_absorptance - thermal_boundary.outside_thermal_absorptance = thermal_boundary_archetype.outside_thermal_absorptance - thermal_boundary.outside_visible_absorptance = thermal_boundary_archetype.outside_visible_absorptance - thermal_boundary.construction_name = thermal_boundary_archetype.construction_name - try: - thermal_boundary.window_ratio = thermal_boundary_archetype.window_ratio - except ValueError: - # This is the normal operation way when the windows are defined in the geometry - continue - thermal_boundary.layers = [] - for layer_archetype in thermal_boundary_archetype.layers: - layer = Layer() - layer.thickness = layer_archetype.thickness - material = Material() - material.name = layer_archetype.name - material.no_mass = layer_archetype.no_mass - material.density = layer_archetype.density - material.conductivity = layer_archetype.conductivity - material.specific_heat = layer_archetype.specific_heat - material.solar_absorptance = layer_archetype.solar_absorptance - material.thermal_absorptance = layer_archetype.thermal_absorptance - material.visible_absorptance = layer_archetype.visible_absorptance - material.thermal_resistance = layer_archetype.thermal_resistance - layer.material = material - thermal_boundary.layers.append(layer) - for thermal_opening in thermal_boundary.thermal_openings: - if thermal_boundary_archetype.thermal_opening_archetype is not None: - thermal_opening_archetype = thermal_boundary_archetype.thermal_opening_archetype - thermal_opening.frame_ratio = thermal_opening_archetype.frame_ratio - thermal_opening.g_value = thermal_opening_archetype.g_value - thermal_opening.conductivity = thermal_opening_archetype.conductivity - thermal_opening.thickness = thermal_opening_archetype.thickness - thermal_opening.back_side_solar_transmittance_at_normal_incidence = \ - thermal_opening_archetype.back_side_solar_transmittance_at_normal_incidence - thermal_opening.front_side_solar_transmittance_at_normal_incidence = \ - thermal_opening_archetype.front_side_solar_transmittance_at_normal_incidence + def _assign_values(self, internal_zones, archetype): + for internal_zone in internal_zones: + for thermal_zone in internal_zone.thermal_zones: + thermal_zone.additional_thermal_bridge_u_value = archetype.additional_thermal_bridge_u_value + thermal_zone.effective_thermal_capacity = archetype.effective_thermal_capacity + thermal_zone.indirectly_heated_area_ratio = archetype.indirectly_heated_area_ratio + thermal_zone.infiltration_rate_system_on = archetype.infiltration_rate_system_on + thermal_zone.infiltration_rate_system_off = archetype.infiltration_rate_system_off + for thermal_boundary in thermal_zone.thermal_boundaries: + construction_type = ConstructionHelper.nrel_construction_types[thermal_boundary.type] + thermal_boundary_archetype = self._search_construction_in_archetype(archetype, construction_type) + thermal_boundary.outside_solar_absorptance = thermal_boundary_archetype.outside_solar_absorptance + thermal_boundary.outside_thermal_absorptance = thermal_boundary_archetype.outside_thermal_absorptance + thermal_boundary.outside_visible_absorptance = thermal_boundary_archetype.outside_visible_absorptance + thermal_boundary.construction_name = thermal_boundary_archetype.construction_name + try: + thermal_boundary.window_ratio = thermal_boundary_archetype.window_ratio + except ValueError: + # This is the normal operation way when the windows are defined in the geometry + continue + thermal_boundary.layers = [] + for layer_archetype in thermal_boundary_archetype.layers: + layer = Layer() + layer.thickness = layer_archetype.thickness + material = Material() + material.name = layer_archetype.name + material.no_mass = layer_archetype.no_mass + material.density = layer_archetype.density + material.conductivity = layer_archetype.conductivity + material.specific_heat = layer_archetype.specific_heat + material.solar_absorptance = layer_archetype.solar_absorptance + material.thermal_absorptance = layer_archetype.thermal_absorptance + material.visible_absorptance = layer_archetype.visible_absorptance + material.thermal_resistance = layer_archetype.thermal_resistance + layer.material = material + thermal_boundary.layers.append(layer) + for thermal_opening in thermal_boundary.thermal_openings: + if thermal_boundary_archetype.thermal_opening_archetype is not None: + thermal_opening_archetype = thermal_boundary_archetype.thermal_opening_archetype + thermal_opening.frame_ratio = thermal_opening_archetype.frame_ratio + thermal_opening.g_value = thermal_opening_archetype.g_value + thermal_opening.conductivity = thermal_opening_archetype.conductivity + thermal_opening.thickness = thermal_opening_archetype.thickness + thermal_opening.back_side_solar_transmittance_at_normal_incidence = \ + thermal_opening_archetype.back_side_solar_transmittance_at_normal_incidence + thermal_opening.front_side_solar_transmittance_at_normal_incidence = \ + thermal_opening_archetype.front_side_solar_transmittance_at_normal_incidence @staticmethod def _create_storeys(building, archetype): diff --git a/imports/geometry/helpers/geometry_helper.py b/imports/geometry/helpers/geometry_helper.py index 9c432e15..104792d5 100644 --- a/imports/geometry/helpers/geometry_helper.py +++ b/imports/geometry/helpers/geometry_helper.py @@ -12,229 +12,207 @@ class GeometryHelper: Geometry helper """ # function - pluto_to_function = { - 'A0': 'single family house', - 'A1': 'single family house', - 'A2': 'single family house', - 'A3': 'single family house', - 'A4': 'single family house', - 'A5': 'single family house', - 'A6': 'single family house', - 'A7': 'single family house', - 'A8': 'single family house', - 'A9': 'single family house', - 'B1': 'multifamily house', - 'B2': 'multifamily house', - 'B3': 'multifamily house', - 'B9': 'multifamily house', - 'C0': 'residential', - 'C1': 'residential', - 'C2': 'residential', - 'C3': 'residential', - 'C4': 'residential', - 'C5': 'residential', - 'C6': 'residential', - 'C7': 'residential', - 'C8': 'residential', - 'C9': 'residential', - 'D0': 'residential', - 'D1': 'residential', - 'D2': 'residential', - 'D3': 'residential', - 'D4': 'residential', - 'D5': 'residential', - 'D6': 'residential', - 'D7': 'residential', - 'D8': 'residential', - 'D9': 'residential', - 'E1': 'warehouse', - 'E3': 'warehouse', - 'E4': 'warehouse', - 'E5': 'warehouse', - 'E7': 'warehouse', - 'E9': 'warehouse', - 'F1': 'warehouse', - 'F2': 'warehouse', - 'F4': 'warehouse', - 'F5': 'warehouse', - 'F8': 'warehouse', - 'F9': 'warehouse', - 'G0': 'office', - 'G1': 'office', - 'G2': 'office', - 'G3': 'office', - 'G4': 'office', - 'G5': 'office', - 'G6': 'office', - 'G7': 'office', - 'G8': 'office', - 'G9': 'office', - 'H1': 'hotel', - 'H2': 'hotel', - 'H3': 'hotel', - 'H4': 'hotel', - 'H5': 'hotel', - 'H6': 'hotel', - 'H7': 'hotel', - 'H8': 'hotel', - 'H9': 'hotel', - 'HB': 'hotel', - 'HH': 'hotel', - 'HR': 'hotel', - 'HS': 'hotel', - 'I1': 'hospital', - 'I2': 'outpatient', - 'I3': 'outpatient', - 'I4': 'residential', - 'I5': 'outpatient', - 'I6': 'outpatient', - 'I7': 'outpatient', - 'I9': 'outpatient', - 'J1': 'large office', - 'J2': 'large office', - 'J3': 'large office', - 'J4': 'large office', - 'J5': 'large office', - 'J6': 'large office', - 'J7': 'large office', - 'J8': 'large office', - 'J9': 'large office', - 'K1': 'strip mall', - 'K2': 'strip mall', - 'K3': 'strip mall', - 'K4': 'residential', - 'K5': 'restaurant', - 'K6': 'commercial', - 'K7': 'commercial', - 'K8': 'commercial', - 'K9': 'commercial', - 'L1': 'residential', - 'L2': 'residential', - 'L3': 'residential', - 'L8': 'residential', - 'L9': 'residential', - 'M1': 'large office', - 'M2': 'large office', - 'M3': 'large office', - 'M4': 'large office', - 'M9': 'large office', - 'N1': 'residential', - 'N2': 'residential', - 'N3': 'residential', - 'N4': 'residential', - 'N9': 'residential', - 'O1': 'office', - 'O2': 'office', - 'O3': 'office', - 'O4': 'office', - 'O5': 'office', - 'O6': 'office', - 'O7': 'office', - 'O8': 'office', - 'O9': 'office', - 'P1': 'large office', - 'P2': 'hotel', - 'P3': 'office', - 'P4': 'office', - 'P5': 'office', - 'P6': 'office', - 'P7': 'large office', - 'P8': 'large office', - 'P9': 'office', - 'Q0': 'office', - 'Q1': 'office', - 'Q2': 'office', - 'Q3': 'office', - 'Q4': 'office', - 'Q5': 'office', - 'Q6': 'office', - 'Q7': 'office', - 'Q8': 'office', - 'Q9': 'office', - 'R0': 'residential', - 'R1': 'residential', - 'R2': 'residential', - 'R3': 'residential', - 'R4': 'residential', - 'R5': 'residential', - 'R6': 'residential', - 'R7': 'residential', - 'R8': 'residential', - 'R9': 'residential', - 'RA': 'residential', - 'RB': 'residential', - 'RC': 'residential', - 'RD': 'residential', - 'RG': 'residential', - 'RH': 'residential', - 'RI': 'residential', - 'RK': 'residential', - 'RM': 'residential', - 'RR': 'residential', - 'RS': 'residential', - 'RW': 'residential', - 'RX': 'residential', - 'RZ': 'residential', - 'S0': 'residential', - 'S1': 'residential', - 'S2': 'residential', - 'S3': 'residential', - 'S4': 'residential', - 'S5': 'residential', - 'S9': 'residential', - 'T1': 'na', - 'T2': 'na', - 'T9': 'na', - 'U0': 'warehouse', - 'U1': 'warehouse', - 'U2': 'warehouse', - 'U3': 'warehouse', - 'U4': 'warehouse', - 'U5': 'warehouse', - 'U6': 'warehouse', - 'U7': 'warehouse', - 'U8': 'warehouse', - 'U9': 'warehouse', - 'V0': 'na', - 'V1': 'na', - 'V2': 'na', - 'V3': 'na', - 'V4': 'na', - 'V5': 'na', - 'V6': 'na', - 'V7': 'na', - 'V8': 'na', - 'V9': 'na', - 'W1': 'primary school', - 'W2': 'primary school', - 'W3': 'secondary school', - 'W4': 'secondary school', - 'W5': 'secondary school', - 'W6': 'secondary school', - 'W7': 'secondary school', - 'W8': 'primary school', - 'W9': 'secondary school', - 'Y1': 'large office', - 'Y2': 'large office', - 'Y3': 'large office', - 'Y4': 'large office', - 'Y5': 'large office', - 'Y6': 'large office', - 'Y7': 'large office', - 'Y8': 'large office', - 'Y9': 'large office', - 'Z0': 'na', - 'Z1': 'large office', - 'Z2': 'na', - 'Z3': 'na', - 'Z4': 'na', - 'Z5': 'na', - 'Z6': 'na', - 'Z7': 'na', - 'Z8': 'na', - 'Z9': 'na' + _pluto_to_function = { + 'A0': cte.SFH, + 'A1': cte.SFH, + 'A2': cte.SFH, + 'A3': cte.SFH, + 'A4': cte.SFH, + 'A5': cte.SFH, + 'A6': cte.SFH, + 'A7': cte.SFH, + 'A8': cte.SFH, + 'A9': cte.SFH, + 'B1': cte.MFH, + 'B2': cte.MFH, + 'B3': cte.MFH, + 'B9': cte.MFH, + 'C0': cte.RESIDENTIAL, + 'C1': cte.RESIDENTIAL, + 'C2': cte.RESIDENTIAL, + 'C3': cte.RESIDENTIAL, + 'C4': cte.RESIDENTIAL, + 'C5': cte.RESIDENTIAL, + 'C6': cte.RESIDENTIAL, + 'C7': cte.RESIDENTIAL, + 'C8': cte.RESIDENTIAL, + 'C9': cte.RESIDENTIAL, + 'D0': cte.RESIDENTIAL, + 'D1': cte.RESIDENTIAL, + 'D2': cte.RESIDENTIAL, + 'D3': cte.RESIDENTIAL, + 'D4': cte.RESIDENTIAL, + 'D5': cte.RESIDENTIAL, + 'D6': cte.RESIDENTIAL, + 'D7': cte.RESIDENTIAL, + 'D8': cte.RESIDENTIAL, + 'D9': cte.RESIDENTIAL, + 'E1': cte.WAREHOUSE, + 'E3': cte.WAREHOUSE, + 'E4': cte.WAREHOUSE, + 'E5': cte.WAREHOUSE, + 'E7': cte.WAREHOUSE, + 'E9': cte.WAREHOUSE, + 'F1': cte.WAREHOUSE, + 'F2': cte.WAREHOUSE, + 'F4': cte.WAREHOUSE, + 'F5': cte.WAREHOUSE, + 'F8': cte.WAREHOUSE, + 'F9': cte.WAREHOUSE, + 'G0': cte.OFFICE, + 'G1': cte.OFFICE, + 'G2': cte.OFFICE, + 'G3': cte.OFFICE, + 'G4': cte.OFFICE, + 'G5': cte.OFFICE, + 'G6': cte.OFFICE, + 'G7': cte.OFFICE, + 'G8': cte.OFFICE, + 'G9': cte.OFFICE, + 'H1': cte.HOTEL, + 'H2': cte.HOTEL, + 'H3': cte.HOTEL, + 'H4': cte.HOTEL, + 'H5': cte.HOTEL, + 'H6': cte.HOTEL, + 'H7': cte.HOTEL, + 'H8': cte.HOTEL, + 'H9': cte.HOTEL, + 'HB': cte.HOTEL, + 'HH': cte.HOTEL, + 'HR': cte.HOTEL, + 'HS': cte.HOTEL, + 'I1': cte.HOSPITAL, + 'I2': cte.OUTPATIENT, + 'I3': cte.OUTPATIENT, + 'I4': cte.RESIDENTIAL, + 'I5': cte.OUTPATIENT, + 'I6': cte.OUTPATIENT, + 'I7': cte.OUTPATIENT, + 'I9': cte.OUTPATIENT, + 'J1': cte.LARGE_OFFICE, + 'J2': cte.LARGE_OFFICE, + 'J3': cte.LARGE_OFFICE, + 'J4': cte.LARGE_OFFICE, + 'J5': cte.LARGE_OFFICE, + 'J6': cte.LARGE_OFFICE, + 'J7': cte.LARGE_OFFICE, + 'J8': cte.LARGE_OFFICE, + 'J9': cte.LARGE_OFFICE, + 'K1': cte.STRIP_MALL, + 'K2': cte.STRIP_MALL, + 'K3': cte.STRIP_MALL, + 'K4': cte.RESIDENTIAL, + 'K5': cte.RESTAURANT, + 'K6': cte.COMMERCIAL, + 'K7': cte.COMMERCIAL, + 'K8': cte.COMMERCIAL, + 'K9': cte.COMMERCIAL, + 'L1': cte.RESIDENTIAL, + 'L2': cte.RESIDENTIAL, + 'L3': cte.RESIDENTIAL, + 'L8': cte.RESIDENTIAL, + 'L9': cte.RESIDENTIAL, + 'M1': cte.LARGE_OFFICE, + 'M2': cte.LARGE_OFFICE, + 'M3': cte.LARGE_OFFICE, + 'M4': cte.LARGE_OFFICE, + 'M9': cte.LARGE_OFFICE, + 'N1': cte.RESIDENTIAL, + 'N2': cte.RESIDENTIAL, + 'N3': cte.RESIDENTIAL, + 'N4': cte.RESIDENTIAL, + 'N9': cte.RESIDENTIAL, + 'O1': cte.OFFICE, + 'O2': cte.OFFICE, + 'O3': cte.OFFICE, + 'O4': cte.OFFICE, + 'O5': cte.OFFICE, + 'O6': cte.OFFICE, + 'O7': cte.OFFICE, + 'O8': cte.OFFICE, + 'O9': cte.OFFICE, + 'P1': cte.LARGE_OFFICE, + 'P2': cte.HOTEL, + 'P3': cte.OFFICE, + 'P4': cte.OFFICE, + 'P5': cte.OFFICE, + 'P6': cte.OFFICE, + 'P7': cte.LARGE_OFFICE, + 'P8': cte.LARGE_OFFICE, + 'P9': cte.OFFICE, + 'Q0': cte.OFFICE, + 'Q1': cte.OFFICE, + 'Q2': cte.OFFICE, + 'Q3': cte.OFFICE, + 'Q4': cte.OFFICE, + 'Q5': cte.OFFICE, + 'Q6': cte.OFFICE, + 'Q7': cte.OFFICE, + 'Q8': cte.OFFICE, + 'Q9': cte.OFFICE, + 'R0': cte.RESIDENTIAL, + 'R1': cte.RESIDENTIAL, + 'R2': cte.RESIDENTIAL, + 'R3': cte.RESIDENTIAL, + 'R4': cte.RESIDENTIAL, + 'R5': cte.RESIDENTIAL, + 'R6': cte.RESIDENTIAL, + 'R7': cte.RESIDENTIAL, + 'R8': cte.RESIDENTIAL, + 'R9': cte.RESIDENTIAL, + 'RA': cte.RESIDENTIAL, + 'RB': cte.RESIDENTIAL, + 'RC': cte.RESIDENTIAL, + 'RD': cte.RESIDENTIAL, + 'RG': cte.RESIDENTIAL, + 'RH': cte.RESIDENTIAL, + 'RI': cte.RESIDENTIAL, + 'RK': cte.RESIDENTIAL, + 'RM': cte.RESIDENTIAL, + 'RR': cte.RESIDENTIAL, + 'RS': cte.RESIDENTIAL, + 'RW': cte.RESIDENTIAL, + 'RX': cte.RESIDENTIAL, + 'RZ': cte.RESIDENTIAL, + 'S0': cte.RESIDENTIAL, + 'S1': cte.RESIDENTIAL, + 'S2': cte.RESIDENTIAL, + 'S3': cte.RESIDENTIAL, + 'S4': cte.RESIDENTIAL, + 'S5': cte.RESIDENTIAL, + 'S9': cte.RESIDENTIAL, + 'U0': cte.WAREHOUSE, + 'U1': cte.WAREHOUSE, + 'U2': cte.WAREHOUSE, + 'U3': cte.WAREHOUSE, + 'U4': cte.WAREHOUSE, + 'U5': cte.WAREHOUSE, + 'U6': cte.WAREHOUSE, + 'U7': cte.WAREHOUSE, + 'U8': cte.WAREHOUSE, + 'U9': cte.WAREHOUSE, + 'W1': cte.PRIMARY_SCHOOL, + 'W2': cte.PRIMARY_SCHOOL, + 'W3': cte.SECONDARY_SCHOOL, + 'W4': cte.SECONDARY_SCHOOL, + 'W5': cte.SECONDARY_SCHOOL, + 'W6': cte.SECONDARY_SCHOOL, + 'W7': cte.SECONDARY_SCHOOL, + 'W8': cte.PRIMARY_SCHOOL, + 'W9': cte.SECONDARY_SCHOOL, + 'Y1': cte.LARGE_OFFICE, + 'Y2': cte.LARGE_OFFICE, + 'Y3': cte.LARGE_OFFICE, + 'Y4': cte.LARGE_OFFICE, + 'Y5': cte.LARGE_OFFICE, + 'Y6': cte.LARGE_OFFICE, + 'Y7': cte.LARGE_OFFICE, + 'Y8': cte.LARGE_OFFICE, + 'Y9': cte.LARGE_OFFICE, + 'Z1': cte.LARGE_OFFICE } - hft_to_function = { + _hft_to_function = { 'residential': cte.RESIDENTIAL, 'single family house': cte.SFH, 'multifamily house': cte.MFH, @@ -251,25 +229,18 @@ class GeometryHelper: } # usage - function_to_usage = { - 'full service restaurant': 'restaurant', - 'highrise apartment': cte.RESIDENTIAL, - 'hospital': 'health care', - 'large hotel': 'hotel', - 'large office': 'office and administration', - 'medium office': 'office and administration', - 'midrise apartment': cte.RESIDENTIAL, - 'outpatient healthcare': 'health care', - 'primary school': 'education', - 'quick service restaurant': 'restaurant', - 'secondary school': 'education', - 'small hotel': 'hotel', - 'small office': 'office and administration', - 'stand alone retail': 'retail', - 'strip mall': 'hall', - 'supermarket': 'retail', - 'warehouse': 'industry', - 'residential': cte.RESIDENTIAL + _function_to_usage = { + cte.RESTAURANT: cte.RESTAURANT, + cte.RESIDENTIAL: cte.RESIDENTIAL, + cte.HOSPITAL: cte.HEALTH_CARE, + cte.HOTEL: cte.HOTEL, + cte.LARGE_OFFICE: cte.OFFICE_ADMINISTRATION, + cte.OFFICE: cte.OFFICE_ADMINISTRATION, + cte.PRIMARY_SCHOOL: cte.EDUCATION, + cte.SECONDARY_SCHOOL: cte.EDUCATION, + cte.RETAIL: cte.RETAIL, + cte.STRIP_MALL: cte.HALL, + cte.WAREHOUSE: cte.INDUSTRY } @staticmethod @@ -279,7 +250,7 @@ class GeometryHelper: :param building_hft_function: str :return: str """ - return GeometryHelper.hft_to_function[building_hft_function] + return GeometryHelper._hft_to_function[building_hft_function] @staticmethod def function_from_pluto(building_pluto_function): @@ -288,7 +259,7 @@ class GeometryHelper: :param building_pluto_function: str :return: str """ - return GeometryHelper.pluto_to_function[building_pluto_function] + return GeometryHelper._pluto_to_function[building_pluto_function] @staticmethod def usage_from_function(building_function): @@ -297,7 +268,7 @@ class GeometryHelper: :param building_function: str :return: str """ - return GeometryHelper.function_to_usage[building_function] + return GeometryHelper._function_to_usage[building_function] @staticmethod def to_points_matrix(points): diff --git a/imports/schedules/helpers/schedules_helper.py b/imports/schedules/helpers/schedules_helper.py index fa47b343..9aca6fab 100644 --- a/imports/schedules/helpers/schedules_helper.py +++ b/imports/schedules/helpers/schedules_helper.py @@ -12,7 +12,7 @@ class SchedulesHelper: """ Schedules helper """ - usage_to_comnet = { + _usage_to_comnet = { cte.RESIDENTIAL: 'C-12 Residential', cte.INDUSTRY: 'C-10 Warehouse', cte.OFFICE_ADMINISTRATION: 'C-5 Office', @@ -23,16 +23,15 @@ class SchedulesHelper: cte.RESTAURANT: 'C-7 Restaurant', cte.EDUCATION: 'C-9 School' } - comnet_default_value = 'C-12 Residential' - comnet_to_data_type = { + _comnet_to_data_type = { 'Fraction': cte.FRACTION, 'OnOff': cte.ON_OFF, 'Temperature': cte.TEMPERATURE } # usage - function_to_usage = { + _function_to_usage = { 'full service restaurant': cte.RESTAURANT, 'high-rise apartment': cte.RESIDENTIAL, 'hospital': cte.HEALTH_CARE, @@ -61,10 +60,9 @@ class SchedulesHelper: :return: str """ try: - return SchedulesHelper.usage_to_comnet[usage] + return SchedulesHelper._usage_to_comnet[usage] except KeyError: - sys.stderr.write('Error: keyword not found. Returned default Comnet schedules "residential"\n') - return SchedulesHelper.comnet_default_value + sys.stderr.write('Error: keyword not found.\n') @staticmethod def data_type_from_comnet(comnet_data_type): @@ -74,7 +72,7 @@ class SchedulesHelper: :return: str """ try: - return SchedulesHelper.comnet_to_data_type[comnet_data_type] + return SchedulesHelper._comnet_to_data_type[comnet_data_type] except KeyError: raise ValueError(f"Error: comnet data type keyword not found.") @@ -85,4 +83,4 @@ class SchedulesHelper: :param building_function: str :return: str """ - return SchedulesHelper.function_to_usage[building_function] + return SchedulesHelper._function_to_usage[building_function] diff --git a/imports/usage/ca_usage_parameters.py b/imports/usage/ca_usage_parameters.py index cea058f6..c9034b85 100644 --- a/imports/usage/ca_usage_parameters.py +++ b/imports/usage/ca_usage_parameters.py @@ -5,10 +5,12 @@ Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.mons """ import sys -from imports.geometry.helpers.geometry_helper import GeometryHelper as gh from imports.usage.hft_usage_interface import HftUsageInterface from city_model_structure.building_demand.usage_zone import UsageZone from city_model_structure.building_demand.internal_gains import InternalGains +from city_model_structure.building_demand.occupancy import Occupancy +from city_model_structure.building_demand.appliances import Appliances +from city_model_structure.building_demand.thermal_control import ThermalControl class CaUsageParameters(HftUsageInterface): @@ -18,9 +20,6 @@ class CaUsageParameters(HftUsageInterface): def __init__(self, city, base_path): super().__init__(base_path, 'ca_archetypes_reduced.xml') self._city = city - # todo: this is a wrong location for self._min_air_change -> re-think where to place this info - # and where it comes from - self._min_air_change = 0 def enrich_buildings(self): """ @@ -29,19 +28,20 @@ class CaUsageParameters(HftUsageInterface): """ city = self._city for building in city.buildings: - archetype = self._search_archetype(building.function) - if archetype is None: + try: + print(building.function) + archetype = self._search_archetype(building.function) + except KeyError: sys.stderr.write(f'Building {building.name} has unknown archetype for building function:' - f' {building.function}, that assigns building usage as ' - f'{gh.usage_from_function(building.function)}\n') - continue + f' {building.function}\n') + return for internal_zone in building.internal_zones: usage_zone = UsageZone() - usage_zone.usage = building.function - self._assign_values(usage_zone, archetype) - usage_zone.percentage = 1 - internal_zone.usage_zones = [usage_zone] + usage_zone.usage = building.function + usage_zone.percentage = 1 + self._assign_values_usage_zone(usage_zone, archetype) + internal_zone.usage_zones = [usage_zone] def _search_archetype(self, building_usage): for building_archetype in self._usage_archetypes: @@ -50,26 +50,30 @@ class CaUsageParameters(HftUsageInterface): return None @staticmethod - def _assign_values(usage_zone, archetype): + def _assign_values_usage_zone(usage_zone, archetype): # Due to the fact that python is not a typed language, the wrong object type is assigned to # usage_zone.internal_gains when writing usage_zone.internal_gains = archetype.internal_gains. # Therefore, this walk around has been done. - internal_gains = [] - for archetype_internal_gain in archetype.internal_gains: - internal_gain = InternalGains() - internal_gain.average_internal_gain = archetype_internal_gain.average_internal_gain - internal_gain.convective_fraction = archetype_internal_gain.convective_fraction - internal_gain.radiative_fraction = archetype_internal_gain.radiative_fraction - internal_gain.latent_fraction = archetype_internal_gain.latent_fraction - internal_gains.append(internal_gain) - usage_zone.internal_gains = internal_gains - usage_zone.heating_setpoint = archetype.heating_setpoint - usage_zone.heating_setback = archetype.heating_setback - usage_zone.cooling_setpoint = archetype.cooling_setpoint - usage_zone.occupancy_density = archetype.occupancy_density + usage_zone.mechanical_air_change = archetype.mechanical_air_change + _occupancy = Occupancy() + _occupancy.occupancy_density = archetype.occupancy.occupancy_density + usage_zone.occupancy = _occupancy usage_zone.hours_day = archetype.hours_day usage_zone.days_year = archetype.days_year - usage_zone.dhw_average_volume_pers_day = archetype.dhw_average_volume_pers_day - usage_zone.dhw_preparation_temperature = archetype.dhw_preparation_temperature - usage_zone.electrical_app_average_consumption_sqm_year = archetype.electrical_app_average_consumption_sqm_year - usage_zone.mechanical_air_change = archetype.mechanical_air_change + _appliances = Appliances() + _appliances.appliances_density = archetype.appliances.appliances_density + usage_zone.appliances = _appliances + _control = ThermalControl() + _control.mean_heating_set_point = archetype.thermal_control.mean_heating_set_point + _control.heating_set_back = archetype.thermal_control.heating_set_back + _control.mean_cooling_set_point = archetype.thermal_control.mean_cooling_set_point + usage_zone.thermal_control = _control + _internal_gains = [] + for archetype_internal_gain in archetype.not_detailed_source_mean_annual_internal_gains: + _internal_gain = InternalGains() + _internal_gain.average_internal_gain = archetype_internal_gain.average_internal_gain + _internal_gain.convective_fraction = archetype_internal_gain.convective_fraction + _internal_gain.radiative_fraction = archetype_internal_gain.radiative_fraction + _internal_gain.latent_fraction = archetype_internal_gain.latent_fraction + _internal_gains.append(_internal_gain) + usage_zone.not_detailed_source_mean_annual_internal_gains = _internal_gains diff --git a/imports/usage/comnet_usage_parameters.py b/imports/usage/comnet_usage_parameters.py index ed5d187e..851473da 100644 --- a/imports/usage/comnet_usage_parameters.py +++ b/imports/usage/comnet_usage_parameters.py @@ -3,6 +3,7 @@ ComnetUsageParameters model the usage properties SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2021 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ + import sys from typing import Dict import pandas as pd @@ -11,11 +12,13 @@ import helpers.constants as cte from helpers.configuration_helper import ConfigurationHelper as ch from imports.geometry.helpers.geometry_helper import GeometryHelper from imports.usage.helpers.usage_helper import UsageHelper +from imports.schedules.helpers.schedules_helper import SchedulesHelper from city_model_structure.building_demand.usage_zone import UsageZone from city_model_structure.building_demand.lighting import Lighting from city_model_structure.building_demand.occupancy import Occupancy from city_model_structure.building_demand.appliances import Appliances -from city_model_structure.building_demand.internal_gains import InternalGains +from city_model_structure.building_demand.thermal_control import ThermalControl +from city_model_structure.attributes.schedule import Schedule class ComnetUsageParameters: @@ -25,24 +28,19 @@ class ComnetUsageParameters: def __init__(self, city, base_path): self._city = city self._base_path = str(base_path / 'comnet_archetypes.xlsx') - self._usage_archetypes = [] - data = self._read_file() - for item in data['lighting']: - for usage in UsageHelper.usage_to_comnet: - comnet_usage = UsageHelper.usage_to_comnet[usage] - if comnet_usage == item: - usage_archetype = self._parse_zone_usage_type(comnet_usage, data) - self._usage_archetypes.append(usage_archetype) + self._data = self._read_file() + self._comnet_schedules_path = str(base_path / 'comnet_schedules_archetypes.xlsx') + self._xls = pd.ExcelFile(self._comnet_schedules_path) def _read_file(self) -> Dict: """ - reads xlsx file containing usage information into a dictionary + reads xlsx files containing usage information into a dictionary :return : Dict """ number_usage_types = 33 xl_file = pd.ExcelFile(self._base_path) file_data = pd.read_excel(xl_file, sheet_name="Modeling Data", skiprows=[0, 1, 2], - nrows=number_usage_types, usecols="A:Z") + nrows=number_usage_types, usecols="A:AB") lighting_data = {} plug_loads_data = {} @@ -50,6 +48,7 @@ class ComnetUsageParameters: ventilation_rate = {} water_heating = {} process_data = {} + schedules_key = {} for j in range(0, number_usage_types): usage_parameters = file_data.iloc[j] @@ -60,53 +59,130 @@ class ComnetUsageParameters: ventilation_rate[usage_type] = usage_parameters[20:21].values.tolist() water_heating[usage_type] = usage_parameters[23:24].values.tolist() process_data[usage_type] = usage_parameters[24:26].values.tolist() + schedules_key[usage_type] = usage_parameters[27:28].values.tolist() return {'lighting': lighting_data, 'plug loads': plug_loads_data, 'occupancy': occupancy_data, 'ventilation rate': ventilation_rate, 'water heating': water_heating, - 'process': process_data} + 'process': process_data, + 'schedules_key': schedules_key} @staticmethod - def _parse_zone_usage_type(usage, data): + def _parse_usage_type(comnet_usage, data, schedules_data): _usage_zone = UsageZone() - _usage_zone.usage = usage # lighting _lighting = Lighting() _lighting.latent_fraction = ch().comnet_lighting_latent _lighting.convective_fraction = ch().comnet_lighting_convective _lighting.radiative_fraction = ch().comnet_lighting_radiant - _lighting.average_internal_gain = data['lighting'][usage][4] + _lighting.lighting_density = data['lighting'][comnet_usage][4] # plug loads _appliances = None - if data['plug loads'][usage][0] != 'n.a.': + if data['plug loads'][comnet_usage][0] != 'n.a.': _appliances = Appliances() _appliances.latent_fraction = ch().comnet_plugs_latent _appliances.convective_fraction = ch().comnet_plugs_convective _appliances.radiative_fraction = ch().comnet_plugs_radiant - _appliances.average_internal_gain = data['plug loads'][usage][0] + _appliances.appliances_density = data['plug loads'][comnet_usage][0] # occupancy _occupancy = Occupancy() - _occupancy.occupancy_density = data['occupancy'][usage][0] - _occupancy.sensible_convective_internal_gain = data['occupancy'][usage][1] \ + _occupancy.occupancy_density = data['occupancy'][comnet_usage][0] + _occupancy.sensible_convective_internal_gain = data['occupancy'][comnet_usage][1] \ * ch().comnet_occupancy_sensible_convective - _occupancy.sensible_radiant_internal_gain = data['occupancy'][usage][1] * ch().comnet_occupancy_sensible_radiant - _occupancy.latent_internal_gain = data['occupancy'][usage][2] + _occupancy.sensible_radiative_internal_gain = data['occupancy'][comnet_usage][1] \ + * ch().comnet_occupancy_sensible_radiant + _occupancy.latent_internal_gain = data['occupancy'][comnet_usage][2] if _occupancy.occupancy_density <= 0: _usage_zone.mechanical_air_change = 0 else: - _usage_zone.mechanical_air_change = data['ventilation rate'][usage][0] / _occupancy.occupancy_density + _usage_zone.mechanical_air_change = data['ventilation rate'][comnet_usage][0] / _occupancy.occupancy_density + + schedules_usage = UsageHelper.schedules_key(data['schedules_key'][comnet_usage][0]) + + _extracted_data = pd.read_excel(schedules_data, sheet_name=schedules_usage, + skiprows=[0, 1, 2, 3], nrows=39, usecols="A:AA") + schedules = [] + number_of_schedule_types = 13 + schedules_per_schedule_type = 3 + day_types = dict({'week_day': 0, 'saturday': 1, 'sunday': 2}) + for schedule_types in range(0, number_of_schedule_types): + name = '' + data_type = '' + for schedule_day in range(0, schedules_per_schedule_type): + _schedule = Schedule() + _schedule.time_step = cte.HOUR + _schedule.time_range = cte.DAY + row_cells = _extracted_data.iloc[schedules_per_schedule_type * schedule_types + schedule_day] + if schedule_day == day_types['week_day']: + name = row_cells[0] + data_type = row_cells[1] + _schedule.day_types = [cte.MONDAY, cte.TUESDAY, cte.WEDNESDAY, cte.THURSDAY, cte.FRIDAY] + elif schedule_day == day_types['saturday']: + _schedule.day_types = [cte.SATURDAY] + else: + _schedule.day_types = [cte.SUNDAY] + _schedule.type = name + _schedule.data_type = SchedulesHelper.data_type_from_comnet(data_type) + if _schedule.data_type == cte.TEMPERATURE: + values = [] + for cell in row_cells[schedules_per_schedule_type:].to_numpy(): + values.append((float(cell) - 32.) * 5 / 9) + _schedule.values = values + else: + _schedule.values = row_cells[schedules_per_schedule_type:].to_numpy() + schedules.append(_schedule) + + schedules_types = dict({'Occupancy': 0, 'Lights': 3, 'Receptacle': 6, 'Infiltration': 9, 'HVAC Avail': 12, + 'ClgSetPt': 15, 'HtgSetPt': 18}) + + _schedules = [] + for pointer in range(0, 3): + _schedules.append(schedules[schedules_types['Occupancy']+pointer]) + _occupancy.occupancy_schedules = _schedules + _schedules = [] + for pointer in range(0, 3): + _schedules.append(schedules[schedules_types['Lights']+pointer]) + _lighting.schedules = _schedules + _schedules = [] + for pointer in range(0, 3): + _schedules.append(schedules[schedules_types['Receptacle']+pointer]) + _appliances.schedules = _schedules _usage_zone.occupancy = _occupancy _usage_zone.lighting = _lighting _usage_zone.appliances = _appliances + + _control = ThermalControl() + _schedules = [] + for pointer in range(0, 3): + _schedules.append(schedules[schedules_types['HtgSetPt']+pointer]) + _control.heating_set_point_schedules = _schedules + _schedules = [] + for pointer in range(0, 3): + _schedules.append(schedules[schedules_types['ClgSetPt']+pointer]) + _control.cooling_set_point_schedules = _schedules + _schedules = [] + for pointer in range(0, 3): + _schedules.append(schedules[schedules_types['HVAC Avail']+pointer]) + _control.hvac_availability_schedules = _schedules + _usage_zone.thermal_control = _control + return _usage_zone + def _search_archetypes(self, usage): + for item in self._data['lighting']: + comnet_usage = UsageHelper.comnet_from_usage(usage) + if comnet_usage == item: + usage_archetype = self._parse_usage_type(comnet_usage, self._data, self._xls) + return usage_archetype + return None, None + def enrich_buildings(self): """ Returns the city with the usage parameters assigned to the buildings @@ -115,41 +191,80 @@ class ComnetUsageParameters: city = self._city for building in city.buildings: usage = GeometryHelper.usage_from_function(building.function) - archetype = self._search_archetype(UsageHelper.comnet_from_usage(usage)) - if archetype is None: + try: + archetype_usage = self._search_archetypes(usage) + except KeyError: sys.stderr.write(f'Building {building.name} has unknown archetype for building function:' f' {building.function}, that assigns building usage as ' f'{GeometryHelper.usage_from_function(building.function)}\n') - continue + return for internal_zone in building.internal_zones: + if internal_zone.area is None: + raise Exception('Internal zone area not defined, ACH cannot be calculated') + if internal_zone.volume is None: + raise Exception('Internal zone volume not defined, ACH cannot be calculated') + if internal_zone.area <= 0: + raise Exception('Internal zone area is zero, ACH cannot be calculated') + if internal_zone.volume <= 0: + raise Exception('Internal zone volume is zero, ACH cannot be calculated') + volume_per_area = internal_zone.volume / internal_zone.area usage_zone = UsageZone() usage_zone.usage = usage - self._assign_values(usage_zone, archetype, volume_per_area) + self._assign_values_usage_zone(usage_zone, archetype_usage, volume_per_area) usage_zone.percentage = 1 + self._calculate_reduced_values_from_extended_library(usage_zone, archetype_usage) + internal_zone.usage_zones = [usage_zone] - def _search_archetype(self, building_usage): - for building_archetype in self._usage_archetypes: - if building_archetype.usage == building_usage: - return building_archetype - return None + @staticmethod + def _assign_values_usage_zone(usage_zone, archetype, volume_per_area): + # Due to the fact that python is not a typed language, the wrong object type is assigned to + # usage_zone.occupancy when writing usage_zone.occupancy = archetype.occupancy. + # Same happens for lighting and appliances. Therefore, this walk around has been done. + usage_zone.mechanical_air_change = archetype.mechanical_air_change * cte.METERS_TO_FEET ** 2 \ + * cte.HOUR_TO_MINUTES / cte.METERS_TO_FEET ** 3 / volume_per_area + _occupancy = Occupancy() + _occupancy.occupancy_density = archetype.occupancy.occupancy_density * cte.METERS_TO_FEET**2 + _occupancy.sensible_radiative_internal_gain = archetype.occupancy.sensible_radiative_internal_gain + _occupancy.latent_internal_gain = archetype.occupancy.latent_internal_gain + _occupancy.sensible_convective_internal_gain = archetype.occupancy.sensible_convective_internal_gain + _occupancy.occupancy_schedules = archetype.occupancy.occupancy_schedules + usage_zone.occupancy = _occupancy + _lighting = Lighting() + _lighting.lighting_density = archetype.lighting.lighting_density / cte.METERS_TO_FEET**2 + _lighting.convective_fraction = archetype.lighting.convective_fraction + _lighting.radiative_fraction = archetype.lighting.radiative_fraction + _lighting.latent_fraction = archetype.lighting.latent_fraction + _lighting.schedules = archetype.lighting.schedules + usage_zone.lighting = _lighting + _appliances = Appliances() + _appliances.appliances_density = archetype.appliances.appliances_density / cte.METERS_TO_FEET**2 + _appliances.convective_fraction = archetype.appliances.convective_fraction + _appliances.radiative_fraction = archetype.appliances.radiative_fraction + _appliances.latent_fraction = archetype.appliances.latent_fraction + _appliances.schedules = archetype.appliances.schedules + usage_zone.appliances = _appliances + _control = ThermalControl() + _control.cooling_set_point_schedules = archetype.thermal_control.cooling_set_point_schedules + _control.heating_set_point_schedules = archetype.thermal_control.heating_set_point_schedules + _control.hvac_availability_schedules = archetype.thermal_control.hvac_availability_schedules + usage_zone.thermal_control = _control @staticmethod - def _assign_values(usage_zone, archetype, volume_per_area): - # Due to the fact that python is not a typed language, the wrong object type is assigned to - # usage_zone.internal_gains when writing usage_zone.internal_gains = archetype.internal_gains. - # Therefore, this walk around has been done. - internal_gains = [] - for archetype_internal_gain in archetype.internal_gains: - internal_gain = InternalGains() - internal_gain.type = archetype_internal_gain.type - internal_gain.average_internal_gain = archetype_internal_gain.average_internal_gain - internal_gain.convective_fraction = archetype_internal_gain.convective_fraction - internal_gain.radiative_fraction = archetype_internal_gain.radiative_fraction - internal_gain.latent_fraction = archetype_internal_gain.latent_fraction - internal_gains.append(internal_gain) - usage_zone.internal_gains = internal_gains - usage_zone.occupancy_density = archetype.occupancy_density * cte.METERS_TO_FEET**2 - usage_zone.mechanical_air_change = archetype.mechanical_air_change * cte.METERS_TO_FEET**2 \ - * cte.HOUR_TO_MINUTES / cte.METERS_TO_FEET**3 / volume_per_area \ No newline at end of file + def _calculate_reduced_values_from_extended_library(usage_zone, archetype): + number_of_days_per_type = {'WD': 251, 'Sat': 52, 'Sun': 62} + total = 0 + for schedule in archetype.thermal_control.hvac_availability_schedules: + if schedule.day_types[0] == cte.SATURDAY: + for value in schedule.values: + total += value * number_of_days_per_type['Sat'] + elif schedule.day_types[0] == cte.SUNDAY: + for value in schedule.values: + total += value * number_of_days_per_type['Sun'] + else: + for value in schedule.values: + total += value * number_of_days_per_type['WD'] + + usage_zone.hours_day = total / 365 + usage_zone.days_year = 365 diff --git a/imports/usage/helpers/usage_helper.py b/imports/usage/helpers/usage_helper.py index 310b4888..f6257503 100644 --- a/imports/usage/helpers/usage_helper.py +++ b/imports/usage/helpers/usage_helper.py @@ -11,7 +11,7 @@ class UsageHelper: """ Usage helper class """ - usage_to_hft = { + _usage_to_hft = { cte.RESIDENTIAL: 'residential', cte.INDUSTRY: 'industry', cte.OFFICE_ADMINISTRATION: 'office and administration', @@ -20,9 +20,7 @@ class UsageHelper: cte.RETAIL: 'retail', cte.HALL: 'hall', cte.RESTAURANT: 'restaurant', - cte.EDUCATION: 'education' - } - hft_default_value = 'residential' + cte.EDUCATION: 'education'} @staticmethod def hft_from_usage(usage): @@ -32,12 +30,11 @@ class UsageHelper: :return: str """ try: - return UsageHelper.usage_to_hft[usage] + return UsageHelper._usage_to_hft[usage] except KeyError: - sys.stderr.write('Error: keyword not found. Returned default HfT usage "residential"\n') - return UsageHelper.hft_default_value + sys.stderr.write('Error: keyword not found.\n') - usage_to_comnet = { + _usage_to_comnet = { cte.RESIDENTIAL: 'BA Multifamily', cte.INDUSTRY: 'BA Manufacturing Facility', cte.OFFICE_ADMINISTRATION: 'BA Office', @@ -46,9 +43,23 @@ class UsageHelper: cte.RETAIL: 'BA Retail', cte.HALL: 'BA Town Hall', cte.RESTAURANT: 'BA Dining: Bar Lounge/Leisure', - cte.EDUCATION: 'BA School/University' - } - comnet_default_value = 'BA Multifamily' + cte.EDUCATION: 'BA School/University'} + + _comnet_schedules_key_to_comnet_schedules = { + 'C-1 Assembly': 'C-1 Assembly', + 'C-2 Public': 'C-2 Health', + 'C-3 Hotel Motel': 'C-3 Hotel', + 'C-4 Manufacturing': 'C-4 Manufacturing', + 'C-5 Office': 'C-5 Office', + 'C-6 Parking Garage': 'C-6 Parking', + 'C-7 Restaurant': 'C-7 Restaurant', + 'C-8 Retail': 'C-8 Retail', + 'C-9 Schools': 'C-9 School', + 'C-10 Warehouse': 'C-10 Warehouse', + 'C-11 Laboratory': 'C-11 Lab', + 'C-12 Residential': 'C-12 Residential', + 'C-13 Data Center': 'C-13 Data', + 'C-14 Gymnasium': 'C-14 Gymnasium'} @staticmethod def comnet_from_usage(usage): @@ -58,7 +69,19 @@ class UsageHelper: :return: str """ try: - return UsageHelper.usage_to_comnet[usage] + return UsageHelper._usage_to_comnet[usage] except KeyError: - sys.stderr.write('Error: keyword not found. Returned default Comnet usage "BA Multifamily"\n') - return UsageHelper.comnet_default_value + sys.stderr.write('Error: keyword not found.\n') + + @staticmethod + def schedules_key(usage): + """ + Get Comnet schedules key from the list found in the Comnet usage file + :param usage: str + :return: str + """ + try: + return UsageHelper._comnet_schedules_key_to_comnet_schedules[usage] + except KeyError: + sys.stderr.write('Error: Comnet keyword not found. An update of the Comnet files might have been ' + 'done changing the keywords.\n') diff --git a/imports/usage/hft_usage_interface.py b/imports/usage/hft_usage_interface.py index ed6dfefc..340d8a80 100644 --- a/imports/usage/hft_usage_interface.py +++ b/imports/usage/hft_usage_interface.py @@ -1,12 +1,17 @@ """ -Hft-based interface, it reads format defined within the CERC team based on that one used in SimStadt and developed by -the IAF team at hft-Stuttgart and enriches the city with usage parameters +Hft-based interface, it reads format defined within the CERC team (based on that one used in SimStadt and developed by +the IAF team at hft-Stuttgart) SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +Copyright © 2022 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ import xmltodict -from imports.usage.data_classes.usage_zone_archetype import UsageZoneArchetype as huza -from imports.usage.data_classes.hft_internal_gains_archetype import HftInternalGainsArchetype as higa +from city_model_structure.building_demand.usage_zone import UsageZone +from city_model_structure.building_demand.internal_gains import InternalGains +from city_model_structure.building_demand.occupancy import Occupancy +from city_model_structure.building_demand.appliances import Appliances +from city_model_structure.building_demand.thermal_control import ThermalControl +from city_model_structure.attributes.schedule import Schedule +import helpers.constants as cte class HftUsageInterface: @@ -28,116 +33,196 @@ class HftUsageInterface: usage = usage_zone_variant['id'] usage_archetype_variant = self._parse_zone_usage_variant(usage, usage_archetype, usage_zone_variant) self._usage_archetypes.append(usage_archetype_variant) + for usage in self._usage_archetypes: + print(usage.usage) @staticmethod def _parse_zone_usage_type(usage, zone_usage_type): - occupancy_density = zone_usage_type['occupancy']['occupancyDensity'] - hours_day = zone_usage_type['occupancy']['usageHoursPerDay'] - days_year = zone_usage_type['occupancy']['usageDaysPerYear'] - cooling_setpoint = zone_usage_type['endUses']['space_cooling']['coolingSetPointTemperature'] - heating_setpoint = zone_usage_type['endUses']['space_heating']['heatingSetPointTemperature'] - heating_setback = zone_usage_type['endUses']['space_heating']['heatingSetBackTemperature'] - mechanical_air_change = None - if 'ventilation' in zone_usage_type['endUses'] and zone_usage_type['endUses']['ventilation'] is not None: - mechanical_air_change = zone_usage_type['endUses']['ventilation']['mechanicalAirChangeRate'] - dhw_average_volume_pers_day = None - dhw_preparation_temperature = None - if 'domestic_hot_water' in zone_usage_type['endUses']: - # liters to cubic meters - dhw_average_volume_pers_day = float( - zone_usage_type['endUses']['domestic_hot_water']['averageVolumePerPersAndDay']) / 1000 - dhw_preparation_temperature = zone_usage_type['endUses']['domestic_hot_water']['preparationTemperature'] - electrical_app_average_consumption_sqm_year = None - if 'all_electrical_appliances' in zone_usage_type['endUses']: - if 'averageConsumptionPerSqmAndYear' in zone_usage_type['endUses']['all_electrical_appliances']: - # kWh to J - electrical_app_average_consumption_sqm_year = \ - float(zone_usage_type['endUses']['all_electrical_appliances']['averageConsumptionPerSqmAndYear']) / 3.6 + usage_zone_archetype = UsageZone() + usage_zone_archetype.usage = usage - # todo: for internal_gain in usage_zone_variant['schedules']['internGains']:???????????????? - # There are no more internal gains? How is it saved when more than one??? - internal_gains = [] - if 'internGains' in zone_usage_type['occupancy']: - latent_fraction = zone_usage_type['occupancy']['internGains']['latentFraction'] - convective_fraction = zone_usage_type['occupancy']['internGains']['convectiveFraction'] - average_internal_gain = zone_usage_type['occupancy']['internGains']['averageInternGainPerSqm'] - radiative_fraction = zone_usage_type['occupancy']['internGains']['radiantFraction'] - else: - latent_fraction = 0 - convective_fraction = 0 - average_internal_gain = 0 - radiative_fraction = 0 + if 'occupancy' in zone_usage_type: + _occupancy = Occupancy() + _occupancy.occupancy_density = zone_usage_type['occupancy']['occupancyDensity'] #todo: check units + usage_zone_archetype.hours_day = zone_usage_type['occupancy']['usageHoursPerDay'] + usage_zone_archetype.days_year = zone_usage_type['occupancy']['usageDaysPerYear'] + usage_zone_archetype.occupancy = _occupancy + + if 'internGains' in zone_usage_type['occupancy']: + _internal_gain = InternalGains() + _internal_gain.latent_fraction = zone_usage_type['occupancy']['internGains']['latentFraction'] + _internal_gain.convective_fraction = zone_usage_type['occupancy']['internGains']['convectiveFraction'] + _internal_gain.average_internal_gain = zone_usage_type['occupancy']['internGains']['averageInternGainPerSqm'] + _internal_gain.radiative_fraction = zone_usage_type['occupancy']['internGains']['radiantFraction'] + if 'load' in zone_usage_type['occupancy']['internGains']: + _schedule = Schedule() + _schedule.type = 'internal gains load' + _schedule.time_range = cte.DAY + _schedule.time_step = cte.HOUR + _schedule.data_type = cte.ANY_NUMBER + _schedule.day_types = [cte.MONDAY, cte.TUESDAY, cte.WEDNESDAY, cte.THURSDAY, cte.FRIDAY] + _values = zone_usage_type['occupancy']['internGains']['load']['weekDayProfile']['values'] + while ' ' in _values: + _values = _values.replace(' ', ' ') + _schedule.values = _values.split() + _internal_gain.schedules = [_schedule] + + usage_zone_archetype.not_detailed_source_mean_annual_internal_gains = [_internal_gain] + + if 'endUses' in zone_usage_type: + _thermal_control = ThermalControl() + if 'space_heating' in zone_usage_type['endUses']: + _thermal_control.mean_heating_set_point = \ + zone_usage_type['endUses']['space_heating']['heatingSetPointTemperature'] + _thermal_control.heating_set_back = zone_usage_type['endUses']['space_heating']['heatingSetBackTemperature'] + if 'schedule' in zone_usage_type['endUses']['space_heating']: + _schedule = Schedule() + _schedule.type = 'heating temperature' + _schedule.time_range = cte.DAY + _schedule.time_step = cte.HOUR + _schedule.data_type = cte.TEMPERATURE + _schedule.day_types = [cte.MONDAY, cte.TUESDAY, cte.WEDNESDAY, cte.THURSDAY, cte.FRIDAY] + _values = zone_usage_type['endUses']['space_heating']['schedule']['weekDayProfile']['values'] + while ' ' in _values: + _values = _values.replace(' ', ' ') + _schedule.values = _values.split() + _thermal_control.heating_set_point_schedules = [_schedule] + + if 'space_cooling' in zone_usage_type['endUses']: + _thermal_control.mean_cooling_set_point = \ + zone_usage_type['endUses']['space_cooling']['coolingSetPointTemperature'] + if 'schedule' in zone_usage_type['endUses']['space_cooling']: + _schedule = Schedule() + _schedule.type = 'cooling temperature' + _schedule.time_range = cte.DAY + _schedule.time_step = cte.HOUR + _schedule.data_type = cte.TEMPERATURE + _schedule.day_types = [cte.MONDAY, cte.TUESDAY, cte.WEDNESDAY, cte.THURSDAY, cte.FRIDAY] + _values = zone_usage_type['endUses']['space_cooling']['schedule']['weekDayProfile']['values'] + while ' ' in _values: + _values = _values.replace(' ', ' ') + _schedule.values = _values.split() + _thermal_control.cooling_set_point_schedules = [_schedule] + + usage_zone_archetype.thermal_control = _thermal_control + + if 'ventilation' in zone_usage_type['endUses'] and zone_usage_type['endUses']['ventilation'] is not None: + usage_zone_archetype.mechanical_air_change = \ + zone_usage_type['endUses']['ventilation']['mechanicalAirChangeRate'] + + # todo: not used or assigned anywhere + if 'domestic_hot_water' in zone_usage_type['endUses']: + # liters to cubic meters + dhw_average_volume_pers_day = float( + zone_usage_type['endUses']['domestic_hot_water']['averageVolumePerPersAndDay']) / 1000 + dhw_preparation_temperature = zone_usage_type['endUses']['domestic_hot_water']['preparationTemperature'] + + if 'all_electrical_appliances' in zone_usage_type['endUses']: + if 'averageConsumptionPerSqmAndYear' in zone_usage_type['endUses']['all_electrical_appliances']: + # kWh to J + usage_zone_archetype.electrical_app_average_consumption_sqm_year = \ + float(zone_usage_type['endUses']['all_electrical_appliances']['averageConsumptionPerSqmAndYear']) \ + * cte.KILO_WATTS_HOUR_TO_JULES + + if 'appliance' in zone_usage_type: + _appliances = Appliances() + _appliances.appliances_density = zone_usage_type['appliance']['#text'] #todo: check units + + usage_zone_archetype.appliances = _appliances - internal_gains.append(higa(average_internal_gain=average_internal_gain, convective_fraction=convective_fraction, - radiative_fraction=radiative_fraction, latent_fraction=latent_fraction)) - usage_zone_archetype = huza(usage=usage, internal_gains=internal_gains, heating_set_point=heating_setpoint, - heating_set_back=heating_setback, cooling_set_point=cooling_setpoint, - occupancy_density=occupancy_density, hours_day=hours_day, days_year=days_year, - dhw_average_volume_pers_day=dhw_average_volume_pers_day, - dhw_preparation_temperature=dhw_preparation_temperature, - electrical_app_average_consumption_sqm_year=electrical_app_average_consumption_sqm_year, - mechanical_air_change=mechanical_air_change) return usage_zone_archetype @staticmethod def _parse_zone_usage_variant(usage, usage_zone, usage_zone_variant): - # for the variants all is optional because it mimics the inheritance concept from OOP - occupancy_density = usage_zone.occupancy_density - hours_day = usage_zone.hours_day - days_year = usage_zone.days_year - cooling_setpoint = usage_zone.cooling_setpoint - heating_setpoint = usage_zone.heating_setpoint - heating_setback = usage_zone.heating_setback - mechanical_air_change = usage_zone.mechanical_air_change - dhw_average_volume_pers_day = usage_zone.dhw_average_volume_pers_day - dhw_preparation_temperature = usage_zone.dhw_preparation_temperature - electrical_app_average_consumption_sqm_year = usage_zone.electrical_app_average_consumption_sqm_year + # the variants mimic the inheritance concept from OOP + usage_zone_archetype = usage_zone + usage_zone_archetype.usage = usage - # todo: for internal_gain in usage_zone_variant['schedules']['internGains']:???????????????? - # There are no more internal gains? How is it saved when more than one??? - # for internal_gain in usage_zone.internal_gains: - internal_gains = usage_zone.not_detailed_source_mean_annual_internal_gains[0] - latent_fraction = internal_gains.latent_fraction - convective_fraction = internal_gains.convective_fraction - average_internal_gain = internal_gains.average_internal_gain - radiative_fraction = internal_gains.radiative_fraction + if 'occupancy' in usage_zone_variant: + _occupancy = Occupancy() + if 'occupancyDensity' in usage_zone_variant['occupancy']: + _occupancy.occupancy_density = usage_zone_variant['occupancy']['occupancyDensity'] # todo: check units + if 'usageHoursPerDay' in usage_zone_variant['occupancy']: + usage_zone_archetype.hours_day = usage_zone_variant['occupancy']['usageHoursPerDay'] + if 'usageDaysPerYear' in usage_zone_variant['occupancy']: + usage_zone_archetype.days_year = usage_zone_variant['occupancy']['usageDaysPerYear'] + usage_zone_archetype.occupancy = _occupancy + + if 'internGains' in usage_zone_variant['occupancy']: + _internal_gain = InternalGains() + if 'latentFraction' in usage_zone_variant['occupancy']['internGains']: + _internal_gain.latent_fraction = usage_zone_variant['occupancy']['internGains']['latentFraction'] + if 'convectiveFraction' in usage_zone_variant['occupancy']['internGains']: + _internal_gain.convective_fraction = usage_zone_variant['occupancy']['internGains']['convectiveFraction'] + if 'averageInternGainPerSqm' in usage_zone_variant['occupancy']['internGains']: + _internal_gain.average_internal_gain = \ + usage_zone_variant['occupancy']['internGains']['averageInternGainPerSqm'] + if 'radiantFraction' in usage_zone_variant['occupancy']['internGains']: + _internal_gain.radiative_fraction = usage_zone_variant['occupancy']['internGains']['radiantFraction'] + if 'load' in usage_zone_variant['occupancy']['internGains']: + _schedule = Schedule() + _schedule.type = 'internal gains load' + _schedule.time_range = cte.DAY + _schedule.time_step = cte.HOUR + _schedule.data_type = cte.ANY_NUMBER + _schedule.day_types = [cte.MONDAY, cte.TUESDAY, cte.WEDNESDAY, cte.THURSDAY, cte.FRIDAY] + _values = usage_zone_variant['occupancy']['internGains']['load']['weekDayProfile']['values'] + while ' ' in _values: + _values = _values.replace(' ', ' ') + _schedule.values = _values.split() + _internal_gain.schedules = [_schedule] + + usage_zone_archetype.not_detailed_source_mean_annual_internal_gains = [_internal_gain] + + if 'endUses' in usage_zone_variant: + _thermal_control = ThermalControl() + if 'space_heating' in usage_zone_variant['endUses']: + if 'heatingSetPointTemperature' in usage_zone_variant['endUses']['space_heating']: + _thermal_control.mean_heating_set_point = \ + usage_zone_variant['endUses']['space_heating']['heatingSetPointTemperature'] + if 'heatingSetBackTemperature' in usage_zone_variant['endUses']['space_heating']: + _thermal_control.heating_set_back = usage_zone_variant['endUses']['space_heating']['heatingSetBackTemperature'] + if 'schedule' in usage_zone_variant['endUses']['space_heating']: + _schedule = Schedule() + _schedule.type = 'heating temperature' + _schedule.time_range = cte.DAY + _schedule.time_step = cte.HOUR + _schedule.data_type = cte.TEMPERATURE + _schedule.day_types = [cte.MONDAY, cte.TUESDAY, cte.WEDNESDAY, cte.THURSDAY, cte.FRIDAY] + _values = usage_zone_variant['endUses']['space_heating']['schedule']['weekDayProfile']['values'] + while ' ' in _values: + _values = _values.replace(' ', ' ') + _schedule.values = _values.split() + _thermal_control.heating_set_point_schedules = [_schedule] + + if 'space_cooling' in usage_zone_variant['endUses'] and \ + usage_zone_variant['endUses']['space_cooling'] is not None: + if 'coolingSetPointTemperature' in usage_zone_variant['endUses']['space_cooling']: + _thermal_control.mean_cooling_set_point = \ + usage_zone_variant['endUses']['space_cooling']['coolingSetPointTemperature'] + if 'schedule' in usage_zone_variant['endUses']['space_cooling']: + _schedule = Schedule() + _schedule.type = 'cooling temperature' + _schedule.time_range = cte.DAY + _schedule.time_step = cte.HOUR + _schedule.data_type = cte.TEMPERATURE + _schedule.day_types = [cte.MONDAY, cte.TUESDAY, cte.WEDNESDAY, cte.THURSDAY, cte.FRIDAY] + _values = usage_zone_variant['endUses']['space_cooling']['schedule']['weekDayProfile']['values'] + while ' ' in _values: + _values = _values.replace(' ', ' ') + _schedule.values = _values.split() + _thermal_control.cooling_set_point_schedules = [_schedule] + + usage_zone_archetype.thermal_control = _thermal_control + + if 'ventilation' in usage_zone_variant['endUses'] and usage_zone_variant['endUses']['ventilation'] is not None: + usage_zone_archetype.mechanical_air_change = \ + usage_zone_variant['endUses']['ventilation']['mechanicalAirChangeRate'] + + if 'appliance' in usage_zone_variant: + _appliances = Appliances() + _appliances.appliances_density = usage_zone_variant['appliance']['#text'] # todo: check units + + usage_zone_archetype.appliances = _appliances - if 'space_cooling' in usage_zone_variant['endUses'] and usage_zone_variant['endUses']['space_cooling'] is not None: - if 'coolingSetPointTemperature' in usage_zone_variant['endUses']['space_cooling']: - cooling_setpoint = usage_zone_variant['endUses']['space_cooling']['coolingSetPointTemperature'] - if 'space_heating' in usage_zone_variant['endUses'] and usage_zone_variant['endUses']['space_heating'] is not None: - if 'heatingSetPointTemperature' in usage_zone_variant['endUses']['space_heating']: - heating_setpoint = usage_zone_variant['endUses']['space_heating']['heatingSetPointTemperature'] - if 'heatingSetBackTemperature' in usage_zone_variant['endUses']['space_heating']: - heating_setback = usage_zone_variant['endUses']['space_heating']['heatingSetBackTemperature'] - if 'ventilation' in usage_zone_variant['endUses'] and usage_zone_variant['endUses']['ventilation'] is not None: - if 'mechanicalAirChangeRate' in usage_zone_variant['endUses']['ventilation']: - mechanical_air_change = usage_zone_variant['endUses']['ventilation']['mechanicalAirChangeRate'] - # todo: for internal_gain in usage_zone_variant['schedules']['internGains']:???????????????? - # There are no more internal gains? How is it saved when more than one??? - if 'schedules' in usage_zone_variant: - if 'usageHoursPerDay' in usage_zone_variant['schedules']: - hours_day = usage_zone_variant['schedules']['usageHoursPerDay'] - if 'usageDaysPerYear' in usage_zone_variant['schedules']: - days_year = usage_zone_variant['schedules']['usageDaysPerYear'] - if 'internalGains' in usage_zone_variant['schedules'] and usage_zone_variant['schedules'][ - 'internGains'] is not None: - internal_gains = [] - if 'latentFraction' in usage_zone_variant['schedules']['internGains']: - latent_fraction = usage_zone_variant['schedules']['internGains']['latentFraction'] - if 'convectiveFraction' in usage_zone_variant['schedules']['internGains']: - convective_fraction = usage_zone_variant['schedules']['internGains']['convectiveFraction'] - if 'averageInternGainPerSqm' in usage_zone_variant['schedules']['internGains']: - average_internal_gain = usage_zone_variant['schedules']['internGains']['averageInternGainPerSqm'] - if 'radiantFraction' in usage_zone_variant['schedules']['internGains']: - radiative_fraction = usage_zone_variant['schedules']['internGains']['radiantFraction'] - internal_gains.append(higa(average_internal_gain=average_internal_gain, convective_fraction=convective_fraction, - radiative_fraction=radiative_fraction, latent_fraction=latent_fraction)) - usage_zone_archetype = huza(usage=usage, internal_gains=internal_gains, heating_set_point=heating_setpoint, - heating_set_back=heating_setback, cooling_set_point=cooling_setpoint, - occupancy_density=occupancy_density, hours_day=hours_day, days_year=days_year, - dhw_average_volume_pers_day=dhw_average_volume_pers_day, - dhw_preparation_temperature=dhw_preparation_temperature, - electrical_app_average_consumption_sqm_year=electrical_app_average_consumption_sqm_year, - mechanical_air_change=mechanical_air_change) return usage_zone_archetype diff --git a/imports/usage/hft_usage_parameters.py b/imports/usage/hft_usage_parameters.py index 7411891e..d95a2c3c 100644 --- a/imports/usage/hft_usage_parameters.py +++ b/imports/usage/hft_usage_parameters.py @@ -9,6 +9,9 @@ from imports.geometry.helpers.geometry_helper import GeometryHelper as gh from imports.usage.hft_usage_interface import HftUsageInterface from city_model_structure.building_demand.usage_zone import UsageZone from city_model_structure.building_demand.internal_gains import InternalGains +from city_model_structure.building_demand.occupancy import Occupancy +from city_model_structure.building_demand.appliances import Appliances +from city_model_structure.building_demand.thermal_control import ThermalControl class HftUsageParameters(HftUsageInterface): @@ -26,19 +29,21 @@ class HftUsageParameters(HftUsageInterface): """ city = self._city for building in city.buildings: - archetype = self._search_archetype(gh.usage_from_function(building.function)) - if archetype is None: + usage = gh.usage_from_function(building.function) + try: + archetype = self._search_archetype(usage) + except KeyError: sys.stderr.write(f'Building {building.name} has unknown archetype for building function:' f' {building.function}, that assigns building usage as ' f'{gh.usage_from_function(building.function)}\n') - continue + return for internal_zone in building.internal_zones: usage_zone = UsageZone() - usage_zone.usage = building.function + usage_zone.usage = building.function self._assign_values(usage_zone, archetype) - usage_zone.percentage = 1 - internal_zone.usage_zones = [usage_zone] + usage_zone.percentage = 1 + internal_zone.usage_zones = [usage_zone] def _search_archetype(self, building_usage): for building_archetype in self._usage_archetypes: @@ -51,22 +56,30 @@ class HftUsageParameters(HftUsageInterface): # Due to the fact that python is not a typed language, the wrong object type is assigned to # usage_zone.internal_gains when writing usage_zone.internal_gains = archetype.internal_gains. # Therefore, this walk around has been done. - internal_gains = [] - for archetype_internal_gain in archetype.internal_gains: - internal_gain = InternalGains() - internal_gain.average_internal_gain = archetype_internal_gain.average_internal_gain - internal_gain.convective_fraction = archetype_internal_gain.convective_fraction - internal_gain.radiative_fraction = archetype_internal_gain.radiative_fraction - internal_gain.latent_fraction = archetype_internal_gain.latent_fraction - internal_gains.append(internal_gain) - usage_zone.internal_gains = internal_gains - usage_zone.heating_setpoint = archetype.heating_setpoint - usage_zone.heating_setback = archetype.heating_setback - usage_zone.cooling_setpoint = archetype.cooling_setpoint - usage_zone.occupancy_density = archetype.occupancy_density - usage_zone.hours_day = archetype.hours_day - usage_zone.days_year = archetype.days_year - usage_zone.dhw_average_volume_pers_day = archetype.dhw_average_volume_pers_day - usage_zone.dhw_preparation_temperature = archetype.dhw_preparation_temperature - usage_zone.electrical_app_average_consumption_sqm_year = archetype.electrical_app_average_consumption_sqm_year + # Due to the fact that python is not a typed language, the wrong object type is assigned to + # usage_zone.occupancy when writing usage_zone.occupancy = archetype.occupancy. + # Same happens for lighting and appliances. Therefore, this walk around has been done. usage_zone.mechanical_air_change = archetype.mechanical_air_change + _occupancy = Occupancy() + _occupancy.occupancy_density = archetype.occupancy.occupancy_density + usage_zone.occupancy = _occupancy + _appliances = Appliances() + _appliances.appliances_density = archetype.appliances.appliances_density + usage_zone.appliances = _appliances + _control = ThermalControl() + _control.mean_heating_set_point = archetype.thermal_control.mean_heating_set_point + _control.heating_set_back = archetype.thermal_control.heating_set_back + _control.mean_cooling_set_point = archetype.thermal_control.mean_cooling_set_point + _control.cooling_set_point_schedules = archetype.thermal_control.cooling_set_point_schedules + _control.heating_set_point_schedules = archetype.thermal_control.heating_set_point_schedules + usage_zone.thermal_control = _control + _internal_gains = [] + for archetype_internal_gain in archetype.not_detailed_source_mean_annual_internal_gains: + _internal_gain = InternalGains() + _internal_gain.average_internal_gain = archetype_internal_gain.average_internal_gain + _internal_gain.convective_fraction = archetype_internal_gain.convective_fraction + _internal_gain.radiative_fraction = archetype_internal_gain.radiative_fraction + _internal_gain.latent_fraction = archetype_internal_gain.latent_fraction + _internal_gain.schedules = archetype_internal_gain.schedules + _internal_gains.append(_internal_gain) + usage_zone.not_detailed_source_mean_annual_internal_gains = _internal_gains diff --git a/unittests/test_construction_factory.py b/unittests/test_construction_factory.py index e0ece40a..df7e1edd 100644 --- a/unittests/test_construction_factory.py +++ b/unittests/test_construction_factory.py @@ -47,12 +47,13 @@ class TestConstructionFactory(TestCase): self.assertEqual(len(building.beam), 0, 'building beam is calculated') self.assertIsNotNone(building.lower_corner, 'building lower corner is none') self.assertEqual(len(building.sensors), 0, 'building sensors are assigned') - self.assertIsNotNone(building.geometrical_zones, 'no geometrical zones created') + self.assertIsNotNone(building.internal_zones, 'no internal zones created') self.assertIsNotNone(building.grounds, 'building grounds is none') self.assertIsNotNone(building.walls, 'building walls is none') self.assertIsNotNone(building.roofs, 'building roofs is none') - self.assertIsNone(building.usage_zones, 'usage zones are defined') - self.assertTrue(len(building.thermal_zones) > 0, 'thermal zones are not defined') + for internal_zone in building.internal_zones: + self.assertIsNone(internal_zone.usage_zones, 'usage zones are defined') + self.assertTrue(len(internal_zone.thermal_zones) > 0, 'thermal zones are not defined') self.assertIsNone(building.basement_heated, 'building basement_heated is not none') self.assertIsNone(building.attic_heated, 'building attic_heated is not none') self.assertIsNone(building.terrains, 'building terrains is not none') @@ -69,8 +70,8 @@ class TestConstructionFactory(TestCase): self.assertIsNone(building.households, 'building households is not none') self.assertFalse(building.is_conditioned, 'building is conditioned') - def _check_thermal_zones(self, building): - for thermal_zone in building.thermal_zones: + def _check_thermal_zones(self, internal_zone): + for thermal_zone in internal_zone.thermal_zones: self.assertIsNotNone(thermal_zone.id, 'thermal_zone id is none') self.assertIsNotNone(thermal_zone.floor_area, 'thermal_zone floor area is none') self.assertTrue(len(thermal_zone.thermal_boundaries) > 0, 'thermal_zone thermal_boundaries not defined') @@ -81,9 +82,10 @@ class TestConstructionFactory(TestCase): self.assertIsNotNone(thermal_zone.infiltration_rate_system_off, 'thermal_zone infiltration_rate_system_off is none') self.assertIsNotNone(thermal_zone.infiltration_rate_system_on, 'thermal_zone infiltration_rate_system_on is none') - self.assertIsNone(thermal_zone.usage_zones, 'thermal_zone usage_zones is not none') self.assertIsNotNone(thermal_zone.volume, 'thermal_zone volume is none') self.assertIsNone(thermal_zone.ordinate_number, 'thermal_zone ordinate number is not none') + self.assertIsNotNone(thermal_zone.view_factors_matrix, 'thermal_zone view factors matrix is none') + self.assertIsNone(thermal_zone.usage_zones, 'thermal_zone usage_zones is not none') self.assertIsNone(thermal_zone.thermal_control, 'thermal_zone thermal_control is not none') self.assertIsNone(thermal_zone.hvac_system, 'thermal_zone hvac_system is not none') @@ -164,27 +166,28 @@ class TestConstructionFactory(TestCase): file = 'one_building_in_kelowna.gml' city = self._get_citygml(file) for building in city.buildings: - building.function = GeometryHelper.hft_to_function[building.function] + building.function = GeometryHelper.function_from_hft(building.function) ConstructionFactory('nrcan', city).enrich() self._check_buildings(city) for building in city.buildings: - self._check_thermal_zones(building) - for thermal_zone in building.thermal_zones: - self._check_thermal_boundaries(thermal_zone) - for thermal_boundary in thermal_zone.thermal_boundaries: - self.assertIsNone(thermal_boundary.outside_thermal_absorptance, 'outside_thermal_absorptance is not none') - self.assertIsNone(thermal_boundary.outside_visible_absorptance, 'outside_visible_absorptance is not none') - self.assertIsNone(thermal_boundary.layers, 'layers is not none') + for internal_zone in building.internal_zones: + self._check_thermal_zones(internal_zone) + for thermal_zone in internal_zone.thermal_zones: + self._check_thermal_boundaries(thermal_zone) + for thermal_boundary in thermal_zone.thermal_boundaries: + self.assertIsNone(thermal_boundary.outside_thermal_absorptance, 'outside_thermal_absorptance is not none') + self.assertIsNone(thermal_boundary.outside_visible_absorptance, 'outside_visible_absorptance is not none') + self.assertIsNone(thermal_boundary.layers, 'layers is not none') - self._check_thermal_openings(thermal_boundary) - for thermal_opening in thermal_boundary.thermal_openings: - self.assertIsNone(thermal_opening.conductivity, 'thermal_opening conductivity is not none') - self.assertIsNone(thermal_opening.thickness, 'thermal opening thickness is not none') - self.assertIsNone(thermal_opening.front_side_solar_transmittance_at_normal_incidence, - 'thermal opening front_side_solar_transmittance_at_normal_incidence is not none') - self.assertIsNone(thermal_opening.back_side_solar_transmittance_at_normal_incidence, - 'thermal opening back_side_solar_transmittance_at_normal_incidence is not none') + self._check_thermal_openings(thermal_boundary) + for thermal_opening in thermal_boundary.thermal_openings: + self.assertIsNone(thermal_opening.conductivity, 'thermal_opening conductivity is not none') + self.assertIsNone(thermal_opening.thickness, 'thermal opening thickness is not none') + self.assertIsNone(thermal_opening.front_side_solar_transmittance_at_normal_incidence, + 'thermal opening front_side_solar_transmittance_at_normal_incidence is not none') + self.assertIsNone(thermal_opening.back_side_solar_transmittance_at_normal_incidence, + 'thermal opening back_side_solar_transmittance_at_normal_incidence is not none') def test_city_with_construction_extended_library(self): """ @@ -193,41 +196,39 @@ class TestConstructionFactory(TestCase): file = 'pluto_building.gml' city = self._get_citygml(file) for building in city.buildings: - building.function = GeometryHelper.pluto_to_function[building.function] + building.function = GeometryHelper.function_from_pluto(building.function) ConstructionFactory('nrel', city).enrich() self._check_buildings(city) for building in city.buildings: - self._check_thermal_zones(building) - for thermal_zone in building.thermal_zones: - self._check_thermal_boundaries(thermal_zone) - for thermal_boundary in thermal_zone.thermal_boundaries: - if thermal_boundary.type is not cte.GROUND: - self.assertIsNotNone(thermal_boundary.outside_thermal_absorptance, 'outside_thermal_absorptance is none') - self.assertIsNotNone(thermal_boundary.outside_visible_absorptance, 'outside_visible_absorptance is none') - else: - self.assertIsNone(thermal_boundary.outside_thermal_absorptance, 'outside_thermal_absorptance is not none') - self.assertIsNone(thermal_boundary.outside_visible_absorptance, 'outside_visible_absorptance is not none') - self.assertIsNotNone(thermal_boundary.layers, 'layers is none') + for internal_zone in building.internal_zones: + self._check_thermal_zones(internal_zone) + for thermal_zone in internal_zone.thermal_zones: + self._check_thermal_boundaries(thermal_zone) + for thermal_boundary in thermal_zone.thermal_boundaries: + if thermal_boundary.type is not cte.GROUND: + self.assertIsNotNone(thermal_boundary.outside_thermal_absorptance, 'outside_thermal_absorptance is none') + self.assertIsNotNone(thermal_boundary.outside_visible_absorptance, 'outside_visible_absorptance is none') + else: + self.assertIsNone(thermal_boundary.outside_thermal_absorptance, 'outside_thermal_absorptance is not none') + self.assertIsNone(thermal_boundary.outside_visible_absorptance, 'outside_visible_absorptance is not none') + self.assertIsNotNone(thermal_boundary.layers, 'layers is none') - self._check_thermal_openings(thermal_boundary) - for thermal_opening in thermal_boundary.thermal_openings: - self.assertIsNotNone(thermal_opening.conductivity, 'thermal_opening conductivity is none') - self.assertIsNotNone(thermal_opening.thickness, 'thermal opening thickness is none') - self.assertIsNotNone(thermal_opening.front_side_solar_transmittance_at_normal_incidence, - 'thermal opening front_side_solar_transmittance_at_normal_incidence is none') - self.assertIsNotNone(thermal_opening.back_side_solar_transmittance_at_normal_incidence, - 'thermal opening back_side_solar_transmittance_at_normal_incidence is none') + self._check_thermal_openings(thermal_boundary) + for thermal_opening in thermal_boundary.thermal_openings: + self.assertIsNotNone(thermal_opening.conductivity, 'thermal_opening conductivity is none') + self.assertIsNotNone(thermal_opening.thickness, 'thermal opening thickness is none') + self.assertIsNotNone(thermal_opening.front_side_solar_transmittance_at_normal_incidence, + 'thermal opening front_side_solar_transmittance_at_normal_incidence is none') + self.assertIsNotNone(thermal_opening.back_side_solar_transmittance_at_normal_incidence, + 'thermal opening back_side_solar_transmittance_at_normal_incidence is none') @staticmethod def _internal_function(function_format, original_function): if function_format == 'hft': - new_function = GeometryHelper.hft_to_function[original_function] + new_function = GeometryHelper.function_from_hft(original_function) elif function_format == 'pluto': - new_function = GeometryHelper.pluto_to_function[original_function] - elif function_format == 'alkis': - # todo: not implemented yet!! - raise NotImplementedError + new_function = GeometryHelper.function_from_pluto(original_function) else: raise Exception('Function key not recognized. Implemented only "hft" and "pluto"') return new_function diff --git a/unittests/test_enrichement.py b/unittests/test_enrichement.py new file mode 100644 index 00000000..7c5861d5 --- /dev/null +++ b/unittests/test_enrichement.py @@ -0,0 +1,145 @@ +""" +TestGeometryFactory test and validate the city model structure geometric parameters +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +""" +from pathlib import Path +from unittest import TestCase +from imports.geometry_factory import GeometryFactory +from imports.geometry.helpers.geometry_helper import GeometryHelper +from imports.usage_factory import UsageFactory +from imports.construction_factory import ConstructionFactory + + +class TestGeometryFactory(TestCase): + """ + Non-functional TestGeometryFactory + Load testing + """ + def setUp(self) -> None: + """ + Test setup + :return: None + """ + self._city = None + self._example_path = (Path(__file__).parent / 'tests_data').resolve() + + def _get_citygml(self, file): + file_path = (self._example_path / file).resolve() + self._city = GeometryFactory('citygml', file_path).city + self.assertIsNotNone(self._city, 'city is none') + return self._city + + def _check_buildings(self, city): + for building in city.buildings: + self.assertIsNotNone(building.internal_zones, 'no internal zones created') + for internal_zone in building.internal_zones: + self.assertIsNotNone(internal_zone.usage_zones, 'usage zones are not defined') + self.assertIsNotNone(internal_zone.thermal_zones, 'thermal zones are not defined') + #self.assertIsNotNone(building.basement_heated, 'building basement_heated is none') + #self.assertIsNotNone(building.attic_heated, 'building attic_heated is none') + self.assertIsNotNone(building.average_storey_height, 'building average_storey_height is none') + self.assertIsNotNone(building.storeys_above_ground, 'building storeys_above_ground is none') + self.assertTrue(building.is_conditioned, 'building is_conditioned is not conditioned') + + def _check_usage_zone(self, usage_zone): + self.assertIsNotNone(usage_zone.id, 'usage id is none') + + def _check_thermal_zones(self, thermal_zone): + self.assertIsNotNone(thermal_zone.id, 'thermal_zone id is none') + self.assertIsNone(thermal_zone.usage_zones, 'thermal_zone usage_zones is not none') + self.assertIsNone(thermal_zone.thermal_control, 'thermal_zone thermal_control is not none') + + @staticmethod + def _prepare_case_usage_first(city, input_key, construction_key, usage_key): + if input_key == 'pluto': + for building in city.buildings: + building.function = GeometryHelper.function_from_pluto(building.function) + elif input_key == 'hft': + for building in city.buildings: + building.function = GeometryHelper.function_from_hft(building.function) + UsageFactory(usage_key, city).enrich() + ConstructionFactory(construction_key, city).enrich() + + @staticmethod + def _prepare_case_construction_first(city, input_key, construction_key, usage_key): + if input_key == 'pluto': + for building in city.buildings: + building.function = GeometryHelper.function_from_pluto(building.function) + elif input_key == 'hft': + for building in city.buildings: + building.function = GeometryHelper.function_from_hft(building.function) + ConstructionFactory(construction_key, city).enrich() + UsageFactory(usage_key, city).enrich() + + def test_enrichment(self): + """ + Test enrichment of the city with different order and all possible combinations + :return: None + """ + file_1 = 'one_building_in_kelowna.gml' + file_2 = 'pluto_building.gml' + file_3 = 'C40_Final.gml' + _construction_keys = ['nrel', 'nrcan'] + _usage_keys = ['ca', 'comnet'] # todo: add 'hft' + + for construction_key in _construction_keys: + for usage_key in _usage_keys: + city = self._get_citygml(file_1) + self.assertTrue(len(city.buildings) == 1) + self._prepare_case_construction_first(city, 'hft', construction_key, usage_key) + self._check_buildings(city) + for building in city.buildings: + for internal_zone in building.internal_zones: + self.assertIsNot(len(internal_zone.usage_zones), 0, 'no building usage_zones defined') + for usage_zone in internal_zone.usage_zones: + self._check_usage_zone(usage_zone) + for thermal_zone in internal_zone.thermal_zones: + self._check_thermal_zones(thermal_zone) + + for construction_key in _construction_keys: + for usage_key in _usage_keys: + city = self._get_citygml(file_1) + self.assertTrue(len(city.buildings) == 1) + self._prepare_case_usage_first(city, 'hft', construction_key, usage_key) + self._check_buildings(city) + for building in city.buildings: + for internal_zone in building.internal_zones: + self.assertIsNot(len(internal_zone.usage_zones), 0, 'no building usage_zones defined') + for usage_zone in internal_zone.usage_zones: + self._check_usage_zone(usage_zone) + for thermal_zone in internal_zone.thermal_zones: + self._check_thermal_zones(thermal_zone) + + for construction_key in _construction_keys: + for usage_key in _usage_keys: + if usage_key != 'ca': + city = self._get_citygml(file_2) + self.assertTrue(len(city.buildings) == 1) + self._prepare_case_construction_first(city, 'pluto', construction_key, usage_key) + self._check_buildings(city) + for building in city.buildings: + for internal_zone in building.internal_zones: + self.assertIsNot(len(internal_zone.usage_zones), 0, 'no building usage_zones defined') + for usage_zone in internal_zone.usage_zones: + self._check_usage_zone(usage_zone) + for thermal_zone in internal_zone.thermal_zones: + self._check_thermal_zones(thermal_zone) + + for construction_key in _construction_keys: + for usage_key in _usage_keys: + if usage_key != 'ca': + city = self._get_citygml(file_2) + self.assertTrue(len(city.buildings) == 1) + self._prepare_case_usage_first(city, 'pluto', construction_key, usage_key) + self._check_buildings(city) + for building in city.buildings: + for internal_zone in building.internal_zones: + self.assertIsNot(len(internal_zone.usage_zones), 0, 'no building usage_zones defined') + for usage_zone in internal_zone.usage_zones: + self._check_usage_zone(usage_zone) + for thermal_zone in internal_zone.thermal_zones: + self._check_thermal_zones(thermal_zone) + + city = self._get_citygml(file_3) + self.assertTrue(len(city.buildings) == 10) diff --git a/unittests/test_geometry_factory.py b/unittests/test_geometry_factory.py index b9142591..db5ae4bb 100644 --- a/unittests/test_geometry_factory.py +++ b/unittests/test_geometry_factory.py @@ -51,7 +51,7 @@ class TestGeometryFactory(TestCase): self.assertEqual(len(building.beam), 0, 'building beam is calculated') self.assertIsNotNone(building.lower_corner, 'building lower corner is none') self.assertEqual(len(building.sensors), 0, 'building sensors are assigned') - self.assertIsNotNone(building.geometrical_zones, 'no geometrical zones created') + self.assertIsNotNone(building.internal_zones, 'no internal zones created') self.assertIsNotNone(building.grounds, 'building grounds is none') self.assertIsNotNone(building.walls, 'building walls is none') self.assertIsNotNone(building.roofs, 'building roofs is none') diff --git a/unittests/test_usage_factory.py b/unittests/test_usage_factory.py index 5385d1e0..bae8dc2c 100644 --- a/unittests/test_usage_factory.py +++ b/unittests/test_usage_factory.py @@ -8,8 +8,6 @@ from unittest import TestCase from imports.geometry_factory import GeometryFactory from imports.usage_factory import UsageFactory -from imports.schedules_factory import SchedulesFactory -from imports.construction_factory import ConstructionFactory from imports.geometry.helpers.geometry_helper import GeometryHelper @@ -48,34 +46,40 @@ class TestUsageFactory(TestCase): self.assertEqual(len(building.beam), 0, 'building beam is calculated') self.assertIsNotNone(building.lower_corner, 'building lower corner is none') self.assertEqual(len(building.sensors), 0, 'building sensors are assigned') - self.assertIsNotNone(building.geometrical_zones, 'no geometrical zones created') + self.assertIsNotNone(building.internal_zones, 'no internal zones created') self.assertIsNotNone(building.grounds, 'building grounds is none') self.assertIsNotNone(building.walls, 'building walls is none') self.assertIsNotNone(building.roofs, 'building roofs is none') - self.assertTrue(len(building.usage_zones) > 0, 'usage zones are not defined') - self.assertTrue(len(building.thermal_zones) > 0, 'thermal zones are not defined') - self.assertIsNotNone(building.basement_heated, 'building basement_heated is none') - self.assertIsNotNone(building.attic_heated, 'building attic_heated is none') + for internal_zone in building.internal_zones: + self.assertTrue(len(internal_zone.usage_zones) > 0, 'usage zones are not defined') + self.assertIsNone(building.thermal_zones, 'thermal zones are defined') + self.assertIsNone(building.basement_heated, 'building basement_heated is not none') + self.assertIsNone(building.attic_heated, 'building attic_heated is not none') self.assertIsNone(building.terrains, 'building terrains is not none') self.assertIsNotNone(building.year_of_construction, 'building year_of_construction is none') self.assertIsNotNone(building.function, 'building function is none') - self.assertIsNotNone(building.average_storey_height, 'building average_storey_height is none') - self.assertIsNotNone(building.storeys_above_ground, 'building storeys_above_ground is none') + self.assertIsNone(building.average_storey_height, 'building average_storey_height is not none') + self.assertIsNone(building.storeys_above_ground, 'building storeys_above_ground is not none') self.assertEqual(len(building.heating), 0, 'building heating is not none') self.assertEqual(len(building.cooling), 0, 'building cooling is not none') self.assertIsNotNone(building.eave_height, 'building eave height is none') - self.assertIsNotNone(building.storeys, 'building storeys are not defined') + self.assertIsNone(building.storeys, 'building storeys are defined') self.assertIsNotNone(building.roof_type, 'building roof type is none') self.assertIsNotNone(building.floor_area, 'building floor_area is none') self.assertIsNone(building.households, 'building households is not none') self.assertTrue(building.is_conditioned, 'building is not conditioned') - - def _check_hvac(self, thermal_zone): - self.assertIsNotNone(None, 'hvac') - - def _check_control(self, thermal_zone): - self.assertIsNotNone(None, 'control') + def _check_usage_zone(self, usage_zone): + self.assertIsNotNone(usage_zone.usage, 'usage is none') + self.assertIsNotNone(usage_zone.percentage, 'usage percentage is none') + self.assertIsNotNone(usage_zone.get_internal_gains, 'internal gains is none') + self.assertIsNotNone(usage_zone.hours_day, 'hours per day is none') + self.assertIsNotNone(usage_zone.days_year, 'days per year is none') + self.assertIsNotNone(usage_zone.mechanical_air_change, 'mechanical air change is none') + self.assertIsNotNone(usage_zone.thermal_control, 'thermal control is none') + self.assertIsNotNone(usage_zone.thermal_control.mean_heating_set_point, 'control heating set point is none') + self.assertIsNotNone(usage_zone.thermal_control.heating_set_back, 'control heating set back is none') + self.assertIsNotNone(usage_zone.thermal_control.mean_cooling_set_point, 'control cooling set point is none') def test_import_comnet(self): """ @@ -84,33 +88,45 @@ class TestUsageFactory(TestCase): file = 'pluto_building.gml' city = self._get_citygml(file) for building in city.buildings: - building.function = GeometryHelper.pluto_to_function[building.function] + building.function = GeometryHelper.function_from_pluto(building.function) UsageFactory('comnet', city).enrich() - SchedulesFactory('comnet', city).enrich() self._check_buildings(city) for building in city.buildings: for internal_zone in building.internal_zones: self.assertIsNot(len(internal_zone.usage_zones), 0, 'no building usage_zones defined') for usage_zone in internal_zone.usage_zones: - self._check_extended_usage(usage_zone) - - def test_import_hft(self): - """ - Enrich the city with the usage information from hft and verify it - """ - # todo: read schedules!! - file = 'pluto_building.gml' - city = self._get_citygml(file) - for building in city.buildings: - building.function = GeometryHelper.pluto_to_function[building.function] - - UsageFactory('hft', city).enrich() - for building in city.buildings: - for internal_zone in building.internal_zones: - self.assertIsNot(len(internal_zone.usage_zones), 0, 'no building usage_zones defined') - for usage_zone in internal_zone.usage_zones: - self._check_extended_usage(usage_zone) + self._check_usage_zone(usage_zone) + self.assertIsNotNone(usage_zone.thermal_control.heating_set_point_schedules, + 'control heating set point schedule is none') + self.assertIsNotNone(usage_zone.thermal_control.cooling_set_point_schedules, + 'control cooling set point schedule is none') + self.assertIsNotNone(usage_zone.occupancy, 'occupancy is none') + occupancy = usage_zone.occupancy + self.assertIsNotNone(occupancy.occupancy_density, 'occupancy density is none') + self.assertIsNotNone(occupancy.latent_internal_gain, 'occupancy latent internal gain is none') + self.assertIsNotNone(occupancy.sensible_convective_internal_gain, + 'occupancy sensible convective internal gain is none') + self.assertIsNotNone(occupancy.sensible_radiative_internal_gain, + 'occupancy sensible radiant internal gain is none') + self.assertIsNotNone(occupancy.occupancy_schedules, 'occupancy schedule is none') + self.assertIsNone(occupancy.occupants, 'occupancy density is not none') + self.assertIsNotNone(usage_zone.lighting, 'lighting is none') + lighting = usage_zone.lighting + self.assertIsNotNone(lighting.lighting_density, 'lighting density is none') + self.assertIsNotNone(lighting.latent_fraction, 'lighting latent fraction is none') + self.assertIsNotNone(lighting.convective_fraction, 'lighting convective fraction is none') + self.assertIsNotNone(lighting.radiative_fraction, 'lighting radiant fraction is none') + self.assertIsNotNone(lighting.schedules, 'lighting schedule is none') + self.assertIsNotNone(usage_zone.appliances, 'appliances is none') + appliances = usage_zone.appliances + self.assertIsNotNone(appliances.appliances_density, 'appliances density is none') + self.assertIsNotNone(appliances.latent_fraction, 'appliances latent fraction is none') + self.assertIsNotNone(appliances.convective_fraction, 'appliances convective fraction is none') + self.assertIsNotNone(appliances.radiative_fraction, 'appliances radiant fraction is none') + self.assertIsNotNone(appliances.schedules, 'appliances schedule is none') + self.assertIsNotNone(usage_zone.thermal_control.hvac_availability_schedules, + 'control hvac availability is none') def test_import_ca(self): """ @@ -119,34 +135,56 @@ class TestUsageFactory(TestCase): file = 'one_building_in_kelowna.gml' city = self._get_citygml(file) UsageFactory('ca', city).enrich() + self._check_buildings(city) for building in city.buildings: for internal_zone in building.internal_zones: self.assertIsNot(len(internal_zone.usage_zones), 0, 'no building usage_zones defined') for usage_zone in internal_zone.usage_zones: - self._check_reduced_usage(usage_zone) + self._check_usage_zone(usage_zone) + self.assertIsNotNone(usage_zone.not_detailed_source_mean_annual_internal_gains, + 'not detailed internal gains is none') - def _check_extended_usage(self, usage_zone): - self.assertIsNotNone(usage_zone.usage, 'usage is none') - self.assertIsNotNone(usage_zone.not_detailed_source_mean_annual_internal_gains, 'usage is none') - self.assertIsNotNone(usage_zone.cooling_setpoint, 'usage is none') - self.assertIsNotNone(usage_zone.heating_setback, 'usage is none') - self.assertIsNotNone(usage_zone.heating_setpoint, 'usage is none') - self.assertIsNotNone(usage_zone.occupancy_density, 'usage is none') - self.assertIsNotNone(usage_zone.hours_day, 'usage is none') - self.assertIsNotNone(usage_zone.days_year, 'usage is none') - self.assertIsNotNone(usage_zone.dhw_average_volume_pers_day, 'usage is none') - self.assertIsNotNone(usage_zone.dhw_preparation_temperature, 'usage is none') - self.assertIsNotNone(usage_zone.electrical_app_average_consumption_sqm_year, 'usage is none') - self.assertIsNotNone(usage_zone.is_heated, 'thermal_zone heated is none') - self.assertIsNotNone(usage_zone.is_cooled, 'thermal_zone cooled is none') + def test_import_hft(self): + """ + Enrich the city with the usage information from hft and verify it + """ + file = 'pluto_building.gml' + city = self._get_citygml(file) + for building in city.buildings: + building.function = GeometryHelper.function_from_pluto(building.function) - - def _check_reduced_usage(self, usage_zone): - self.assertIsNotNone(usage_zone.usage, 'usage is none') - self.assertIsNotNone(usage_zone.internal_gains, 'usage is none') - self.assertIsNotNone(usage_zone.cooling_setpoint, 'usage is none') - self.assertIsNotNone(usage_zone.heating_setback, 'usage is none') - self.assertIsNotNone(usage_zone.heating_setpoint, 'usage is none') - self.assertIsNotNone(usage_zone.occupancy_density, 'usage is none') - self.assertIsNotNone(usage_zone.hours_day, 'usage is none') - self.assertIsNotNone(usage_zone.days_year, 'usage is none') + UsageFactory('hft', city).enrich() + self._check_buildings(city) + for building in city.buildings: + for internal_zone in building.internal_zones: + self.assertIsNot(len(internal_zone.usage_zones), 0, 'no building usage_zones defined') + for usage_zone in internal_zone.usage_zones: + self._check_usage_zone(usage_zone) + self.assertIsNotNone(usage_zone.thermal_control.heating_set_point_schedules, + 'control heating set point schedule is none') + self.assertIsNotNone(usage_zone.thermal_control.cooling_set_point_schedules, + 'control cooling set point schedule is none') + self.assertIsNotNone(usage_zone.occupancy, 'occupancy is none') + occupancy = usage_zone.occupancy + self.assertIsNotNone(occupancy.occupancy_density, 'occupancy density is none') + self.assertIsNotNone(occupancy.latent_internal_gain, 'occupancy latent internal gain is none') + self.assertIsNotNone(occupancy.sensible_convective_internal_gain, + 'occupancy sensible convective internal gain is none') + self.assertIsNotNone(occupancy.sensible_radiative_internal_gain, + 'occupancy sensible radiant internal gain is none') + self.assertIsNotNone(occupancy.occupancy_schedules, 'occupancy schedule is none') + self.assertIsNone(occupancy.occupants, 'occupancy density is not none') + self.assertIsNotNone(usage_zone.lighting, 'lighting is none') + lighting = usage_zone.lighting + self.assertIsNotNone(lighting.lighting_density, 'lighting density is none') + self.assertIsNotNone(lighting.latent_fraction, 'lighting latent fraction is none') + self.assertIsNotNone(lighting.convective_fraction, 'lighting convective fraction is none') + self.assertIsNotNone(lighting.radiative_fraction, 'lighting radiant fraction is none') + self.assertIsNotNone(lighting.schedules, 'lighting schedule is none') + self.assertIsNotNone(usage_zone.appliances, 'appliances is none') + appliances = usage_zone.appliances + self.assertIsNotNone(appliances.appliances_density, 'appliances density is none') + self.assertIsNotNone(appliances.latent_fraction, 'appliances latent fraction is none') + self.assertIsNotNone(appliances.convective_fraction, 'appliances convective fraction is none') + self.assertIsNotNone(appliances.radiative_fraction, 'appliances radiant fraction is none') + self.assertIsNotNone(appliances.schedules, 'appliances schedule is none') From d170f3a2201bfd74d49ee07b3a67729569ed1f5e Mon Sep 17 00:00:00 2001 From: guille Date: Tue, 22 Mar 2022 12:30:05 -0400 Subject: [PATCH 04/19] correct comment in rhino handler --- imports/geometry_factory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imports/geometry_factory.py b/imports/geometry_factory.py index d24199bf..b594da08 100644 --- a/imports/geometry_factory.py +++ b/imports/geometry_factory.py @@ -46,7 +46,7 @@ class GeometryFactory: @property def _rhino(self) -> City: """ - Enrich the city by using OpenStreetMap information as data source + Enrich the city by using Rhino information as data source :return: City """ return Rhino(self._path).city From 6bca2dfda43803670bdf71d8cd169c344c29f751 Mon Sep 17 00:00:00 2001 From: guille Date: Wed, 23 Mar 2022 14:40:49 -0400 Subject: [PATCH 05/19] match pep8 --- imports/life_cycle_assessment/lca_vehicle.py | 1 + 1 file changed, 1 insertion(+) diff --git a/imports/life_cycle_assessment/lca_vehicle.py b/imports/life_cycle_assessment/lca_vehicle.py index 78efead7..2125be95 100644 --- a/imports/life_cycle_assessment/lca_vehicle.py +++ b/imports/life_cycle_assessment/lca_vehicle.py @@ -26,3 +26,4 @@ class LcaVehicle: vehicle['fuel_consumption_rate']['@unit'], vehicle['carbon_emission_factor']['#text'], vehicle['carbon_emission_factor']['@unit'])) + From 7488b6ba91ef646d3a56d45ac0adac273562abbb Mon Sep 17 00:00:00 2001 From: Pilar Date: Thu, 24 Mar 2022 16:51:01 -0400 Subject: [PATCH 06/19] Major changes with two objectives: homogenize the usage parameters regardless the data source, and completely uncouple geometry and construction library (before the geometry was modified after reading storey high from construction library), and construction and usage libraries (before the usage library could only be read after construction library, now ant order is accepted). --- city_model_structure/building.py | 17 -- helpers/constants.py | 60 ++++--- imports/construction/ca_physics_parameters.py | 21 +-- .../helpers/construction_helper.py | 65 ++++--- .../helpers/storeys_generation.py | 61 ++++--- .../construction/nrel_physics_interface.py | 50 ++++++ imports/construction/us_physics_parameters.py | 19 +-- .../helpers/sanam_customized_usage_helper.py | 2 +- .../sanam_customized_usage_parameters.py | 92 +++++----- imports/customized_imports_factory.py | 4 - imports/geometry/helpers/geometry_helper.py | 158 ++++++++++-------- imports/schedules/doe_idf.py | 31 ++-- imports/schedules/helpers/schedules_helper.py | 14 +- imports/schedules_factory.py | 15 +- imports/usage/ca_usage_parameters.py | 9 +- imports/usage/comnet_usage_parameters.py | 10 +- imports/usage/helpers/usage_helper.py | 59 +++++-- imports/usage/hft_usage_interface.py | 5 +- imports/usage/hft_usage_parameters.py | 29 ++-- recognized_functions_and_usages.md | 64 +++++++ unittests/test_construction_factory.py | 11 +- unittests/test_customized_imports_factory.py | 12 +- unittests/test_doe_idf.py | 31 ++-- unittests/test_enrichement.py | 78 +++++---- unittests/test_exports.py | 5 +- unittests/test_geometry_factory.py | 7 +- unittests/test_schedules_factory.py | 45 +++-- unittests/test_usage_factory.py | 40 ++--- 28 files changed, 603 insertions(+), 411 deletions(-) create mode 100644 recognized_functions_and_usages.md diff --git a/city_model_structure/building.py b/city_model_structure/building.py index a94e55a6..bab214a1 100644 --- a/city_model_structure/building.py +++ b/city_model_structure/building.py @@ -8,7 +8,6 @@ contributors: Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca from typing import List, Union import numpy as np from city_model_structure.building_demand.surface import Surface -from city_model_structure.building_demand.storey import Storey from city_model_structure.city_object import CityObject from city_model_structure.building_demand.household import Household from city_model_structure.building_demand.internal_zone import InternalZone @@ -265,22 +264,6 @@ class Building(CityObject): self._eave_height = max(self._eave_height, wall.upper_corner[2]) return self._eave_height - @property - def storeys(self) -> List[Storey]: - """ - Get building storeys - :return: [Storey] - """ - return self._storeys - - @storeys.setter - def storeys(self, value): - """ - Set building storeys - :param value: [Storey] - """ - self._storeys = value - @property def roof_type(self): """ diff --git a/helpers/constants.py b/helpers/constants.py index 684a367f..b4291102 100644 --- a/helpers/constants.py +++ b/helpers/constants.py @@ -58,31 +58,47 @@ WINDOW = 'Window' DOOR = 'Door' SKYLIGHT = 'Skylight' -# todo: homogenize function and usage!! -# function -RESIDENTIAL = 'residential' -SFH = 'single family house' -MFH = 'multifamily house' -HOTEL = 'hotel' -HOSPITAL = 'hospital' -OUTPATIENT = 'outpatient' -COMMERCIAL = 'commercial' -STRIP_MALL = 'strip mall' -WAREHOUSE = 'warehouse' +# functions and usages +SINGLE_FAMILY_HOUSE = 'single family house' +MULTI_FAMILY_HOUSE = 'multifamily house' +ROW_HOSE = 'row house' +MID_RISE_APARTMENT = 'mid rise apartment' +HIGH_RISE_APARTMENT = 'high rise apartment' +SMALL_OFFICE = 'small office' +MEDIUM_OFFICE = 'medium office' +LARGE_OFFICE = 'large office' PRIMARY_SCHOOL = 'primary school' SECONDARY_SCHOOL = 'secondary school' -OFFICE = 'office' -LARGE_OFFICE = 'large office' -OFFICE_WORKSHOP = 'office/workshop' - -# usage -INDUSTRY = 'industry' -OFFICE_ADMINISTRATION = 'office and administration' -HEALTH_CARE = 'health care' -RETAIL = 'retail' -HALL = 'hall' -RESTAURANT = 'restaurant' +STAND_ALONE_RETAIL = 'stand alone retail' +HOSPITAL = 'hospital' +OUT_PATIENT_HEALTH_CARE = 'out-patient health care' +STRIP_MALL = 'strip mall' +SUPERMARKET = 'supermarket' +WAREHOUSE = 'warehouse' +QUICK_SERVICE_RESTAURANT = 'quick service restaurant' +FULL_SERVICE_RESTAURANT = 'full service restaurant' +SMALL_HOTEL = 'small hotel' +LARGE_HOTEL = 'large hotel' +RESIDENTIAL = 'residential' EDUCATION = 'education' +SCHOOL_WITHOUT_SHOWER = 'school without shower' +SCHOOL_WITH_SHOWER = 'school with shower' +RETAIL_SHOP_WITHOUT_REFRIGERATED_FOOD = 'retail shop without refrigerated food' +RETAIL_SHOP_WITH_REFRIGERATED_FOOD = 'retail shop with refrigerated food' +HOTEL = 'hotel' +HOTEL_MEDIUM_CLASS = 'hotel medium class' +DORMITORY = 'dormitory' +INDUSTRY = 'industry' +RESTAURANT = 'restaurant' +HEALTH_CARE = 'health care' +RETIREMENT_HOME_OR_ORPHANAGE = 'retirement home or orphanage' +OFFICE_AND_ADMINISTRATION = 'office and administration' +EVENT_LOCATION = 'event location' +HALL = 'hall' +SPORTS_LOCATION = 'sports location' +LABOR = 'labor' +GREEN_HOUSE = 'green house' +NON_HEATED = 'non-heated' LIGHTING = 'Lights' OCCUPANCY = 'Occupancy' diff --git a/imports/construction/ca_physics_parameters.py b/imports/construction/ca_physics_parameters.py index 5b2ff50c..6f4120fc 100644 --- a/imports/construction/ca_physics_parameters.py +++ b/imports/construction/ca_physics_parameters.py @@ -6,7 +6,6 @@ Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.mons import sys from imports.construction.helpers.construction_helper import ConstructionHelper from imports.construction.nrel_physics_interface import NrelPhysicsInterface -from imports.construction.helpers.storeys_generation import StoreysGeneration class CaPhysicsParameters(NrelPhysicsInterface): @@ -26,11 +25,11 @@ class CaPhysicsParameters(NrelPhysicsInterface): # it is assumed that all buildings have the same archetypes' keys for building in city.buildings: try: - archetype = self._search_archetype(ConstructionHelper.nrcan_from_function(building.function), + archetype = self._search_archetype(ConstructionHelper.nrcan_from_libs_function(building.function), building.year_of_construction) except KeyError: sys.stderr.write(f'Building {building.name} has unknown archetype for building function: ' - f'{ConstructionHelper.nrcan_from_function(building.function)} ' + f'{ConstructionHelper.nrcan_from_libs_function(building.function)} ' f'and building year of construction: {building.year_of_construction}\n') return @@ -38,12 +37,11 @@ class CaPhysicsParameters(NrelPhysicsInterface): if len(building.internal_zones) == 1: if building.internal_zones[0].thermal_zones is None: self._create_storeys(building, archetype) - thermal_zones = [] - for storey in building.storeys: - thermal_zones.append(storey.thermal_zone) - building.internal_zones[0].thermal_zones = thermal_zones self._assign_values(building.internal_zones, archetype) + for internal_zone in building.internal_zones: + for thermal_zone in internal_zone.thermal_zones: + self._calculate_view_factors(thermal_zone) def _search_archetype(self, function, year_of_construction): for building_archetype in self._building_archetypes: @@ -82,12 +80,3 @@ class CaPhysicsParameters(NrelPhysicsInterface): thermal_opening.frame_ratio = thermal_opening_archetype.frame_ratio thermal_opening.g_value = thermal_opening_archetype.g_value thermal_opening.overall_u_value = thermal_opening_archetype.overall_u_value - - @staticmethod - def _create_storeys(building, archetype): - building.average_storey_height = archetype.average_storey_height - building.storeys_above_ground = archetype.storeys_above_ground - storeys_generation = StoreysGeneration(building) - storeys = storeys_generation.storeys - building.storeys = storeys - storeys_generation.assign_thermal_zones_delimited_by_thermal_boundaries() diff --git a/imports/construction/helpers/construction_helper.py b/imports/construction/helpers/construction_helper.py index 32402984..ba93d614 100644 --- a/imports/construction/helpers/construction_helper.py +++ b/imports/construction/helpers/construction_helper.py @@ -14,18 +14,26 @@ class ConstructionHelper: # NREL _function_to_nrel = { cte.RESIDENTIAL: 'residential', - cte.SFH: 'single family house', - cte.MFH: 'multifamily house', - cte.HOTEL: 'hotel', - cte.HOSPITAL: 'hospital', - cte.OUTPATIENT: 'outpatient', - cte.COMMERCIAL: 'commercial', - cte.STRIP_MALL: 'strip mall', - cte.WAREHOUSE: 'warehouse', + cte.SINGLE_FAMILY_HOUSE: 'residential', + cte.MULTI_FAMILY_HOUSE: 'residential', + cte.ROW_HOSE: 'residential', + cte.MID_RISE_APARTMENT: 'midrise apartment', + cte.HIGH_RISE_APARTMENT: 'high-rise apartment', + cte.SMALL_OFFICE: 'small office', + cte.MEDIUM_OFFICE: 'medium office', + cte.LARGE_OFFICE: 'large office', cte.PRIMARY_SCHOOL: 'primary school', cte.SECONDARY_SCHOOL: 'secondary school', - cte.OFFICE: 'office', - cte.LARGE_OFFICE: 'large office' + cte.STAND_ALONE_RETAIL: 'stand-alone retail', + cte.HOSPITAL: 'hospital', + cte.OUT_PATIENT_HEALTH_CARE: 'outpatient healthcare', + cte.STRIP_MALL: 'strip mall', + cte.SUPERMARKET: 'supermarket', + cte.WAREHOUSE: 'warehouse', + cte.QUICK_SERVICE_RESTAURANT: 'quick service restaurant', + cte.FULL_SERVICE_RESTAURANT: 'full service restaurant', + cte.SMALL_HOTEL: 'small hotel', + cte.LARGE_HOTEL: 'large hotel' } _nrel_standards = { @@ -65,19 +73,26 @@ class ConstructionHelper: # NRCAN _function_to_nrcan = { cte.RESIDENTIAL: 'residential', - cte.SFH: 'single family house', - cte.MFH: 'multifamily house', - cte.HOTEL: 'hotel', - cte.HOSPITAL: 'hospital', - cte.OUTPATIENT: 'outpatient', - cte.COMMERCIAL: 'commercial', - cte.STRIP_MALL: 'strip mall', - cte.WAREHOUSE: 'warehouse', - cte.PRIMARY_SCHOOL: 'primary school', - cte.SECONDARY_SCHOOL: 'secondary school', - cte.OFFICE: 'office', - cte.LARGE_OFFICE: 'large office', - cte.OFFICE_WORKSHOP: 'residential' + cte.SINGLE_FAMILY_HOUSE: 'residential', + cte.MULTI_FAMILY_HOUSE: 'residential', + cte.ROW_HOSE: 'residential', + cte.MID_RISE_APARTMENT: 'residential', + cte.HIGH_RISE_APARTMENT: 'residential', + cte.SMALL_OFFICE: cte.SMALL_OFFICE, + cte.MEDIUM_OFFICE: cte.MEDIUM_OFFICE, + cte.LARGE_OFFICE: cte.LARGE_OFFICE, + cte.PRIMARY_SCHOOL: cte.PRIMARY_SCHOOL, + cte.SECONDARY_SCHOOL: cte.SECONDARY_SCHOOL, + cte.STAND_ALONE_RETAIL: cte.STAND_ALONE_RETAIL, + cte.HOSPITAL: cte.HOSPITAL, + cte.OUT_PATIENT_HEALTH_CARE: cte.OUT_PATIENT_HEALTH_CARE, + cte.STRIP_MALL: cte.STRIP_MALL, + cte.SUPERMARKET: cte.SUPERMARKET, + cte.WAREHOUSE: cte.WAREHOUSE, + cte.QUICK_SERVICE_RESTAURANT: cte.QUICK_SERVICE_RESTAURANT, + cte.FULL_SERVICE_RESTAURANT: cte.FULL_SERVICE_RESTAURANT, + cte.SMALL_HOTEL: cte.SMALL_HOTEL, + cte.LARGE_HOTEL: cte.LARGE_HOTEL } nrcan_window_types = [cte.WINDOW] @@ -92,7 +107,7 @@ class ConstructionHelper: } @staticmethod - def nrel_from_function(function): + def nrel_from_libs_function(function): """ Get NREL function from the given internal function key :param function: str @@ -140,7 +155,7 @@ class ConstructionHelper: return ConstructionHelper._reference_city_to_nrel_climate_zone[reference_city] @staticmethod - def nrcan_from_function(function): + def nrcan_from_libs_function(function): """ Get NREL function from the given internal function key :param function: str diff --git a/imports/construction/helpers/storeys_generation.py b/imports/construction/helpers/storeys_generation.py index b67a820d..da0b4dcc 100644 --- a/imports/construction/helpers/storeys_generation.py +++ b/imports/construction/helpers/storeys_generation.py @@ -6,12 +6,14 @@ Copyright © 2022 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.mons import sys import math import numpy as np +from typing import List from helpers import constants as cte from city_model_structure.attributes.polygon import Polygon from city_model_structure.attributes.point import Point from city_model_structure.building_demand.storey import Storey from city_model_structure.building_demand.surface import Surface +from city_model_structure.building_demand.thermal_zone import ThermalZone class StoreysGeneration: @@ -20,14 +22,14 @@ class StoreysGeneration: """ def __init__(self, building, divide_in_storeys=False): self._building = building + self._thermal_zones = [] self._divide_in_storeys = divide_in_storeys - self._storeys = None self._floor_area = 0 for ground in building.grounds: self._floor_area += ground.perimeter_polygon.area @property - def storeys(self) -> [Storey]: + def thermal_zones(self) -> List[ThermalZone]: """ Get subsections of building trimesh by storey in case of no interiors defined :return: [Storey] @@ -37,7 +39,21 @@ class StoreysGeneration: self._building.storeys_above_ground) number_of_storeys = 1 if not self._divide_in_storeys or number_of_storeys == 1: - return [Storey('storey_0', self._building.surfaces, [None, None], self._building.volume, self._floor_area)] + storey = Storey('storey_0', self._building.surfaces, [None, None], self._building.volume, self._floor_area) + for thermal_boundary in storey.thermal_boundaries: + if thermal_boundary.type != cte.INTERIOR_WALL or thermal_boundary.type != cte.INTERIOR_SLAB: + # external thermal boundary -> only one thermal zone + thermal_zones = [storey.thermal_zone] + else: + # internal thermal boundary -> two thermal zones + grad = np.rad2deg(thermal_boundary.inclination) + if grad >= 170: + thermal_zones = [storey.thermal_zone, storey.neighbours[0]] + else: + thermal_zones = [storey.neighbours[1], storey.thermal_zone] + thermal_boundary.thermal_zones = thermal_zones + + return [storey.thermal_zone] if number_of_storeys == 0: raise Exception('Number of storeys cannot be 0') @@ -89,7 +105,25 @@ class StoreysGeneration: if volume < 0: raise Exception('Error in storeys creation, volume of last storey cannot be lower that 0') storeys.append(Storey(name, surfaces_child_last_storey, neighbours, volume, self._floor_area)) - return storeys + + for storey in storeys: + for thermal_boundary in storey.thermal_boundaries: + if thermal_boundary.type != cte.INTERIOR_WALL or thermal_boundary.type != cte.INTERIOR_SLAB: + # external thermal boundary -> only one thermal zone + thermal_zones = [storey.thermal_zone] + else: + # internal thermal boundary -> two thermal zones + grad = np.rad2deg(thermal_boundary.inclination) + if grad >= 170: + thermal_zones = [storey.thermal_zone, storey.neighbours[0]] + else: + thermal_zones = [storey.neighbours[1], storey.thermal_zone] + thermal_boundary.thermal_zones = thermal_zones + + for storey in storeys: + self._thermal_zones.append(storey.thermal_zone) + + return self._thermal_zones @staticmethod def _calculate_number_storeys_and_height(average_storey_height, eave_height, storeys_above_ground): @@ -139,22 +173,3 @@ class StoreysGeneration: for point in points: array_points.append(point.coordinates) return np.array(array_points) - - def assign_thermal_zones_delimited_by_thermal_boundaries(self): - """ - During storeys creation, the thermal boundaries and zones are also created. - It is afterwards needed to define which zones are delimited by each thermal boundary - """ - for storey in self._building.storeys: - for thermal_boundary in storey.thermal_boundaries: - if thermal_boundary.type != cte.INTERIOR_WALL or thermal_boundary.type != cte.INTERIOR_SLAB: - # external thermal boundary -> only one thermal zone - thermal_zones = [storey.thermal_zone] - else: - # internal thermal boundary -> two thermal zones - grad = np.rad2deg(thermal_boundary.inclination) - if grad >= 170: - thermal_zones = [storey.thermal_zone, storey.neighbours[0]] - else: - thermal_zones = [storey.neighbours[1], storey.thermal_zone] - thermal_boundary.thermal_zones = thermal_zones diff --git a/imports/construction/nrel_physics_interface.py b/imports/construction/nrel_physics_interface.py index 9065ca9e..6f08e536 100644 --- a/imports/construction/nrel_physics_interface.py +++ b/imports/construction/nrel_physics_interface.py @@ -10,6 +10,7 @@ from imports.construction.data_classes.building_achetype import BuildingArchetyp from imports.construction.data_classes.thermal_boundary_archetype import ThermalBoundaryArchetype as ntba from imports.construction.data_classes.thermal_opening_archetype import ThermalOpeningArchetype as ntoa from imports.construction.data_classes.layer_archetype import LayerArchetype as nla +from imports.construction.helpers.storeys_generation import StoreysGeneration class NrelPhysicsInterface: @@ -181,8 +182,57 @@ class NrelPhysicsInterface: return thermal_boundary raise Exception('Construction type not found') + # todo: verify windows + @staticmethod + def _calculate_view_factors(thermal_zone): + """ + Get thermal zone view factors matrix + :return: [[float]] + """ + total_area = 0 + for thermal_boundary in thermal_zone.thermal_boundaries: + total_area += thermal_boundary.opaque_area + for thermal_opening in thermal_boundary.thermal_openings: + total_area += thermal_opening.area + + view_factors_matrix = [] + for thermal_boundary_1 in thermal_zone.thermal_boundaries: + values = [] + for thermal_boundary_2 in thermal_zone.thermal_boundaries: + value = 0 + if thermal_boundary_1.id != thermal_boundary_2.id: + value = thermal_boundary_2.opaque_area / (total_area - thermal_boundary_1.opaque_area) + values.append(value) + for thermal_boundary in thermal_zone.thermal_boundaries: + for thermal_opening in thermal_boundary.thermal_openings: + value = thermal_opening.area / (total_area - thermal_boundary_1.opaque_area) + values.append(value) + view_factors_matrix.append(values) + + for thermal_boundary_1 in thermal_zone.thermal_boundaries: + values = [] + for thermal_opening_1 in thermal_boundary_1.thermal_openings: + for thermal_boundary_2 in thermal_zone.thermal_boundaries: + value = thermal_boundary_2.opaque_area / (total_area - thermal_opening_1.area) + values.append(value) + for thermal_boundary in thermal_zone.thermal_boundaries: + for thermal_opening_2 in thermal_boundary.thermal_openings: + value = 0 + if thermal_opening_1.id != thermal_opening_2.id: + value = thermal_opening_2.area / (total_area - thermal_opening_1.area) + values.append(value) + view_factors_matrix.append(values) + thermal_zone.view_factors_matrix = view_factors_matrix + def enrich_buildings(self): """ Raise not implemented error """ raise NotImplementedError + + @staticmethod + def _create_storeys(building, archetype): + building.average_storey_height = archetype.average_storey_height + building.storeys_above_ground = archetype.storeys_above_ground + thermal_zones = StoreysGeneration(building).thermal_zones + building.internal_zones[0].thermal_zones = thermal_zones diff --git a/imports/construction/us_physics_parameters.py b/imports/construction/us_physics_parameters.py index de79697b..7a23b318 100644 --- a/imports/construction/us_physics_parameters.py +++ b/imports/construction/us_physics_parameters.py @@ -10,7 +10,6 @@ from imports.construction.nrel_physics_interface import NrelPhysicsInterface from imports.construction.helpers.construction_helper import ConstructionHelper from city_model_structure.building_demand.layer import Layer from city_model_structure.building_demand.material import Material -from imports.construction.helpers.storeys_generation import StoreysGeneration class UsPhysicsParameters(NrelPhysicsInterface): @@ -30,7 +29,7 @@ class UsPhysicsParameters(NrelPhysicsInterface): city = self._city # it is assumed that all buildings have the same archetypes' keys for building in city.buildings: - building_type = ConstructionHelper.nrel_from_function(building.function) + building_type = ConstructionHelper.nrel_from_libs_function(building.function) if building_type is None: return try: @@ -46,12 +45,11 @@ class UsPhysicsParameters(NrelPhysicsInterface): if len(building.internal_zones) == 1: if building.internal_zones[0].thermal_zones is None: self._create_storeys(building, archetype) - thermal_zones = [] - for storey in building.storeys: - thermal_zones.append(storey.thermal_zone) - building.internal_zones[0].thermal_zones = thermal_zones self._assign_values(building.internal_zones, archetype) + for internal_zone in building.internal_zones: + for thermal_zone in internal_zone.thermal_zones: + self._calculate_view_factors(thermal_zone) def _search_archetype(self, building_type, standard, climate_zone): for building_archetype in self._building_archetypes: @@ -109,12 +107,3 @@ class UsPhysicsParameters(NrelPhysicsInterface): thermal_opening_archetype.back_side_solar_transmittance_at_normal_incidence thermal_opening.front_side_solar_transmittance_at_normal_incidence = \ thermal_opening_archetype.front_side_solar_transmittance_at_normal_incidence - - @staticmethod - def _create_storeys(building, archetype): - building.average_storey_height = archetype.average_storey_height - building.storeys_above_ground = archetype.storeys_above_ground - storeys_generation = StoreysGeneration(building) - storeys = storeys_generation.storeys - building.storeys = storeys - storeys_generation.assign_thermal_zones_delimited_by_thermal_boundaries() diff --git a/imports/customized_imports/helpers/sanam_customized_usage_helper.py b/imports/customized_imports/helpers/sanam_customized_usage_helper.py index a0d981b8..7bf73f65 100644 --- a/imports/customized_imports/helpers/sanam_customized_usage_helper.py +++ b/imports/customized_imports/helpers/sanam_customized_usage_helper.py @@ -14,7 +14,7 @@ class SanamCustomizedUsageHelper: usage_to_customized = { cte.RESIDENTIAL: 'residential', cte.INDUSTRY: 'manufacturing', - cte.OFFICE_ADMINISTRATION: 'office', + cte.OFFICE_AND_ADMINISTRATION: 'office', cte.HOTEL: 'hotel', cte.HEALTH_CARE: 'health', cte.RETAIL: 'retail', diff --git a/imports/customized_imports/sanam_customized_usage_parameters.py b/imports/customized_imports/sanam_customized_usage_parameters.py index 442ec9d3..ea3cac62 100644 --- a/imports/customized_imports/sanam_customized_usage_parameters.py +++ b/imports/customized_imports/sanam_customized_usage_parameters.py @@ -6,9 +6,11 @@ Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.mons import sys import xmltodict -from imports.geometry.helpers.geometry_helper import GeometryHelper as gh -from imports.usage.data_classes.usage_zone_archetype import UsageZoneArchetype as huza import helpers.constants as cte +from imports.usage.helpers.usage_helper import UsageHelper +from city_model_structure.building_demand.occupancy import Occupancy +from city_model_structure.building_demand.usage_zone import UsageZone +from imports.geometry.helpers.geometry_helper import GeometryHelper class SanamCustomizedUsageParameters: @@ -18,15 +20,10 @@ class SanamCustomizedUsageParameters: def __init__(self, city, base_path): file = 'ashrae_archetypes.xml' path = str(base_path / file) + self._city = city self._usage_archetypes = [] with open(path) as xml: self._archetypes = xmltodict.parse(xml.read(), force_list=('zoneUsageVariant', 'zoneUsageType')) - for zone_usage_type in self._archetypes['buildingUsageLibrary']['zoneUsageType']: - usage = zone_usage_type['id'] - usage_archetype = self._parse_zone_usage_type(usage, zone_usage_type) - self._usage_archetypes.append(usage_archetype) - - self._city = city def enrich_buildings(self): """ @@ -35,48 +32,59 @@ class SanamCustomizedUsageParameters: """ city = self._city for building in city.buildings: - archetype = self._search_archetype(building.function) # todo: building.function or other translation??????? - height = building.average_storey_height - if height is None: - raise Exception('Average storey height not defined, ACH cannot be calculated') - if height <= 0: - raise Exception('Average storey height is zero, ACH cannot be calculated') + libs_usage = GeometryHelper().libs_usage_from_libs_function(building.function) + comnet_usage = UsageHelper().comnet_from_libs_usage(libs_usage) + archetype = self._search_archetype(comnet_usage) if archetype is None: sys.stderr.write(f'Building {building.name} has unknown archetype for building function:' f' {building.function}, that assigns building usage as ' - f'{gh.usage_from_function(building.function)}\n') - continue - mix_usage = False - if not mix_usage: - # just one usage_zone - for usage_zone in building.usage_zones: - self._assign_values(usage_zone, archetype, height) + f'{libs_usage}\n') + return - def _search_archetype(self, building_usage): - for building_archetype in self._usage_archetypes: - if building_archetype.usage == building_usage: - return building_archetype + for internal_zone in building.internal_zones: + if internal_zone.area is None: + raise Exception('Internal zone area not defined, ACH cannot be calculated') + if internal_zone.volume is None: + raise Exception('Internal zone volume not defined, ACH cannot be calculated') + if internal_zone.area <= 0: + raise Exception('Internal zone area is zero, ACH cannot be calculated') + if internal_zone.volume <= 0: + raise Exception('Internal zone volume is zero, ACH cannot be calculated') + volume_per_area = internal_zone.volume / internal_zone.area + + usage_zone = UsageZone() + usage_zone.usage = libs_usage + self._assign_values(usage_zone, archetype, volume_per_area) + + def _search_archetype(self, libs_usage): + comnet_usage = UsageHelper.comnet_from_libs_usage(libs_usage) + for building_archetype in self._archetypes['buildingUsageLibrary']['zoneUsageType']: + if building_archetype['id'] == comnet_usage: + usage_archetype = self._parse_usage_type(self._archetypes) + return usage_archetype return None @staticmethod - def _assign_values(usage_zone, archetype, height): + def _assign_values(usage_zone, archetype, volume_per_area): usage_zone.usage = archetype.usage - # Due to the fact that python is not a typed language, the wrong object type is assigned to - # usage_zone.internal_gains when writing usage_zone.internal_gains = archetype.internal_gains. - # Therefore, this walk around has been done. - if archetype.occupancy_density is not None: - usage_zone.occupancy_density = archetype.occupancy_density - archetype_mechanical_air_change = float(archetype.mechanical_air_change) * float(usage_zone.occupancy_density) \ - * cte.HOUR_TO_MINUTES / cte.METERS_TO_FEET**3 / height - usage_zone.mechanical_air_change = archetype_mechanical_air_change + if archetype.occupancy.occupancy_density is not None: + if usage_zone.occupancy is None: + _occupancy = Occupancy() + usage_zone.occupancy = _occupancy + usage_zone.occupancy.occupancy_density = archetype.occupancy.occupancy_density + archetype_mechanical_air_change = float(archetype.mechanical_air_change) * \ + float(usage_zone.occupancy.occupancy_density) * cte.METERS_TO_FEET ** 2 \ + * cte.HOUR_TO_MINUTES / cte.METERS_TO_FEET ** 3 / volume_per_area + usage_zone.mechanical_air_change = archetype_mechanical_air_change @staticmethod - def _parse_zone_usage_type(usage, zone_usage_type): - mechanical_air_change = zone_usage_type['endUses']['ventilation']['minimumVentilationRate']['#text'] - if 'occupancy' in zone_usage_type: - occupancy_density = zone_usage_type['occupancy']['occupancyDensity']['#text'] - usage_zone_archetype = huza(usage=usage, occupancy_density=occupancy_density, - mechanical_air_change=mechanical_air_change) - else: - usage_zone_archetype = huza(usage=usage, mechanical_air_change=mechanical_air_change) + def _parse_usage_type(data): + usage_zone_archetype = UsageZone() + usage_zone_archetype.usage = data['id'] + usage_zone_archetype.mechanical_air_change = data['endUses']['ventilation']['minimumVentilationRate'][ + '#text'] + if 'occupancy' in data: + _occupancy = Occupancy() + _occupancy.occupancy_density = data['occupancy']['occupancyDensity']['#text'] + usage_zone_archetype.occupancy = _occupancy return usage_zone_archetype diff --git a/imports/customized_imports_factory.py b/imports/customized_imports_factory.py index bca8a87c..0a072f89 100644 --- a/imports/customized_imports_factory.py +++ b/imports/customized_imports_factory.py @@ -17,10 +17,6 @@ class CustomizedImportsFactory: self._importer_class = importer_class self._city = city self._base_path = base_path - for building in city.buildings: - if len(building.thermal_zones) == 0: - raise Exception('It seems that the customized imports factory is being called before the construction factory. ' - 'Please ensure that the construction factory is called first.') def enrich(self): """ diff --git a/imports/geometry/helpers/geometry_helper.py b/imports/geometry/helpers/geometry_helper.py index 104792d5..7307673a 100644 --- a/imports/geometry/helpers/geometry_helper.py +++ b/imports/geometry/helpers/geometry_helper.py @@ -13,20 +13,20 @@ class GeometryHelper: """ # function _pluto_to_function = { - 'A0': cte.SFH, - 'A1': cte.SFH, - 'A2': cte.SFH, - 'A3': cte.SFH, - 'A4': cte.SFH, - 'A5': cte.SFH, - 'A6': cte.SFH, - 'A7': cte.SFH, - 'A8': cte.SFH, - 'A9': cte.SFH, - 'B1': cte.MFH, - 'B2': cte.MFH, - 'B3': cte.MFH, - 'B9': cte.MFH, + 'A0': cte.SINGLE_FAMILY_HOUSE, + 'A1': cte.SINGLE_FAMILY_HOUSE, + 'A2': cte.SINGLE_FAMILY_HOUSE, + 'A3': cte.SINGLE_FAMILY_HOUSE, + 'A4': cte.SINGLE_FAMILY_HOUSE, + 'A5': cte.SINGLE_FAMILY_HOUSE, + 'A6': cte.SINGLE_FAMILY_HOUSE, + 'A7': cte.SINGLE_FAMILY_HOUSE, + 'A8': cte.SINGLE_FAMILY_HOUSE, + 'A9': cte.SINGLE_FAMILY_HOUSE, + 'B1': cte.MULTI_FAMILY_HOUSE, + 'B2': cte.MULTI_FAMILY_HOUSE, + 'B3': cte.MULTI_FAMILY_HOUSE, + 'B9': cte.MULTI_FAMILY_HOUSE, 'C0': cte.RESIDENTIAL, 'C1': cte.RESIDENTIAL, 'C2': cte.RESIDENTIAL, @@ -59,16 +59,16 @@ class GeometryHelper: 'F5': cte.WAREHOUSE, 'F8': cte.WAREHOUSE, 'F9': cte.WAREHOUSE, - 'G0': cte.OFFICE, - 'G1': cte.OFFICE, - 'G2': cte.OFFICE, - 'G3': cte.OFFICE, - 'G4': cte.OFFICE, - 'G5': cte.OFFICE, - 'G6': cte.OFFICE, - 'G7': cte.OFFICE, - 'G8': cte.OFFICE, - 'G9': cte.OFFICE, + 'G0': cte.SMALL_OFFICE, + 'G1': cte.SMALL_OFFICE, + 'G2': cte.SMALL_OFFICE, + 'G3': cte.SMALL_OFFICE, + 'G4': cte.SMALL_OFFICE, + 'G5': cte.SMALL_OFFICE, + 'G6': cte.SMALL_OFFICE, + 'G7': cte.SMALL_OFFICE, + 'G8': cte.SMALL_OFFICE, + 'G9': cte.SMALL_OFFICE, 'H1': cte.HOTEL, 'H2': cte.HOTEL, 'H3': cte.HOTEL, @@ -83,13 +83,13 @@ class GeometryHelper: 'HR': cte.HOTEL, 'HS': cte.HOTEL, 'I1': cte.HOSPITAL, - 'I2': cte.OUTPATIENT, - 'I3': cte.OUTPATIENT, + 'I2': cte.OUT_PATIENT_HEALTH_CARE, + 'I3': cte.OUT_PATIENT_HEALTH_CARE, 'I4': cte.RESIDENTIAL, - 'I5': cte.OUTPATIENT, - 'I6': cte.OUTPATIENT, - 'I7': cte.OUTPATIENT, - 'I9': cte.OUTPATIENT, + 'I5': cte.OUT_PATIENT_HEALTH_CARE, + 'I6': cte.OUT_PATIENT_HEALTH_CARE, + 'I7': cte.OUT_PATIENT_HEALTH_CARE, + 'I9': cte.OUT_PATIENT_HEALTH_CARE, 'J1': cte.LARGE_OFFICE, 'J2': cte.LARGE_OFFICE, 'J3': cte.LARGE_OFFICE, @@ -104,10 +104,10 @@ class GeometryHelper: 'K3': cte.STRIP_MALL, 'K4': cte.RESIDENTIAL, 'K5': cte.RESTAURANT, - 'K6': cte.COMMERCIAL, - 'K7': cte.COMMERCIAL, - 'K8': cte.COMMERCIAL, - 'K9': cte.COMMERCIAL, + 'K6': cte.SUPERMARKET, + 'K7': cte.SUPERMARKET, + 'K8': cte.SUPERMARKET, + 'K9': cte.SUPERMARKET, 'L1': cte.RESIDENTIAL, 'L2': cte.RESIDENTIAL, 'L3': cte.RESIDENTIAL, @@ -123,34 +123,34 @@ class GeometryHelper: 'N3': cte.RESIDENTIAL, 'N4': cte.RESIDENTIAL, 'N9': cte.RESIDENTIAL, - 'O1': cte.OFFICE, - 'O2': cte.OFFICE, - 'O3': cte.OFFICE, - 'O4': cte.OFFICE, - 'O5': cte.OFFICE, - 'O6': cte.OFFICE, - 'O7': cte.OFFICE, - 'O8': cte.OFFICE, - 'O9': cte.OFFICE, + 'O1': cte.SMALL_OFFICE, + 'O2': cte.SMALL_OFFICE, + 'O3': cte.SMALL_OFFICE, + 'O4': cte.SMALL_OFFICE, + 'O5': cte.SMALL_OFFICE, + 'O6': cte.SMALL_OFFICE, + 'O7': cte.SMALL_OFFICE, + 'O8': cte.SMALL_OFFICE, + 'O9': cte.SMALL_OFFICE, 'P1': cte.LARGE_OFFICE, 'P2': cte.HOTEL, - 'P3': cte.OFFICE, - 'P4': cte.OFFICE, - 'P5': cte.OFFICE, - 'P6': cte.OFFICE, + 'P3': cte.SMALL_OFFICE, + 'P4': cte.SMALL_OFFICE, + 'P5': cte.SMALL_OFFICE, + 'P6': cte.SMALL_OFFICE, 'P7': cte.LARGE_OFFICE, 'P8': cte.LARGE_OFFICE, - 'P9': cte.OFFICE, - 'Q0': cte.OFFICE, - 'Q1': cte.OFFICE, - 'Q2': cte.OFFICE, - 'Q3': cte.OFFICE, - 'Q4': cte.OFFICE, - 'Q5': cte.OFFICE, - 'Q6': cte.OFFICE, - 'Q7': cte.OFFICE, - 'Q8': cte.OFFICE, - 'Q9': cte.OFFICE, + 'P9': cte.SMALL_OFFICE, + 'Q0': cte.SMALL_OFFICE, + 'Q1': cte.SMALL_OFFICE, + 'Q2': cte.SMALL_OFFICE, + 'Q3': cte.SMALL_OFFICE, + 'Q4': cte.SMALL_OFFICE, + 'Q5': cte.SMALL_OFFICE, + 'Q6': cte.SMALL_OFFICE, + 'Q7': cte.SMALL_OFFICE, + 'Q8': cte.SMALL_OFFICE, + 'Q9': cte.SMALL_OFFICE, 'R0': cte.RESIDENTIAL, 'R1': cte.RESIDENTIAL, 'R2': cte.RESIDENTIAL, @@ -214,37 +214,47 @@ class GeometryHelper: } _hft_to_function = { 'residential': cte.RESIDENTIAL, - 'single family house': cte.SFH, - 'multifamily house': cte.MFH, + 'single family house': cte.SINGLE_FAMILY_HOUSE, + 'multifamily house': cte.MULTI_FAMILY_HOUSE, 'hotel': cte.HOTEL, 'hospital': cte.HOSPITAL, - 'outpatient': cte.OUTPATIENT, - 'commercial': cte.COMMERCIAL, + 'outpatient': cte.OUT_PATIENT_HEALTH_CARE, + 'commercial': cte.SUPERMARKET, 'strip mall': cte.STRIP_MALL, 'warehouse': cte.WAREHOUSE, 'primary school': cte.PRIMARY_SCHOOL, 'secondary school': cte.SECONDARY_SCHOOL, - 'office': cte.OFFICE, + 'office': cte.MEDIUM_OFFICE, 'large office': cte.LARGE_OFFICE } # usage _function_to_usage = { - cte.RESTAURANT: cte.RESTAURANT, cte.RESIDENTIAL: cte.RESIDENTIAL, - cte.HOSPITAL: cte.HEALTH_CARE, - cte.HOTEL: cte.HOTEL, - cte.LARGE_OFFICE: cte.OFFICE_ADMINISTRATION, - cte.OFFICE: cte.OFFICE_ADMINISTRATION, + cte.SINGLE_FAMILY_HOUSE: cte.SINGLE_FAMILY_HOUSE, + cte.MULTI_FAMILY_HOUSE: cte.MULTI_FAMILY_HOUSE, + cte.ROW_HOSE: cte.RESIDENTIAL, + cte.MID_RISE_APARTMENT: cte.RESIDENTIAL, + cte.HIGH_RISE_APARTMENT: cte.RESIDENTIAL, + cte.SMALL_OFFICE: cte.OFFICE_AND_ADMINISTRATION, + cte.MEDIUM_OFFICE: cte.OFFICE_AND_ADMINISTRATION, + cte.LARGE_OFFICE: cte.OFFICE_AND_ADMINISTRATION, cte.PRIMARY_SCHOOL: cte.EDUCATION, cte.SECONDARY_SCHOOL: cte.EDUCATION, - cte.RETAIL: cte.RETAIL, - cte.STRIP_MALL: cte.HALL, - cte.WAREHOUSE: cte.INDUSTRY + cte.STAND_ALONE_RETAIL: cte.RETAIL_SHOP_WITHOUT_REFRIGERATED_FOOD, + cte.HOSPITAL: cte.HEALTH_CARE, + cte.OUT_PATIENT_HEALTH_CARE: cte.HEALTH_CARE, + cte.STRIP_MALL: cte.RETAIL_SHOP_WITHOUT_REFRIGERATED_FOOD, + cte.SUPERMARKET: cte.RETAIL_SHOP_WITH_REFRIGERATED_FOOD, + cte.WAREHOUSE: cte.RETAIL_SHOP_WITHOUT_REFRIGERATED_FOOD, + cte.QUICK_SERVICE_RESTAURANT: cte.RESTAURANT, + cte.FULL_SERVICE_RESTAURANT: cte.RESTAURANT, + cte.SMALL_HOTEL: cte.HOTEL, + cte.LARGE_HOTEL: cte.HOTEL } @staticmethod - def function_from_hft(building_hft_function): + def libs_function_from_hft(building_hft_function): """ Get internal function from the given HfT function :param building_hft_function: str @@ -253,7 +263,7 @@ class GeometryHelper: return GeometryHelper._hft_to_function[building_hft_function] @staticmethod - def function_from_pluto(building_pluto_function): + def libs_function_from_pluto(building_pluto_function): """ Get internal function from the given pluto function :param building_pluto_function: str @@ -262,7 +272,7 @@ class GeometryHelper: return GeometryHelper._pluto_to_function[building_pluto_function] @staticmethod - def usage_from_function(building_function): + def libs_usage_from_libs_function(building_function): """ Get the internal usage for the given internal building function :param building_function: str diff --git a/imports/schedules/doe_idf.py b/imports/schedules/doe_idf.py index d8f88f4f..cbec5105 100644 --- a/imports/schedules/doe_idf.py +++ b/imports/schedules/doe_idf.py @@ -10,6 +10,8 @@ import parseidf import xmltodict from imports.schedules.helpers.schedules_helper import SchedulesHelper from city_model_structure.attributes.schedule import Schedule +from city_model_structure.building_demand.occupancy import Occupancy +from city_model_structure.building_demand.lighting import Lighting import helpers.constants as cte @@ -52,15 +54,16 @@ class DoeIdf: self._schedule_library = xmltodict.parse(xml.read()) for building in self._city.buildings: - for usage_zone in building.usage_zones: - for schedule_archetype in self._schedule_library['archetypes']['archetypes']: - function = schedule_archetype['@building_type'] - if SchedulesHelper.usage_from_function(function) == usage_zone.usage: - self._idf_schedules_path = (base_path / schedule_archetype['idf']['path']).resolve() - with open(self._idf_schedules_path, 'r') as file: - idf = parseidf.parse(file.read()) - self._load_schedule(idf, usage_zone) - break + for internal_zone in building.internal_zones: + for usage_zone in internal_zone.usage_zones: + for schedule_archetype in self._schedule_library['archetypes']['archetypes']: + function = schedule_archetype['@building_type'] + if SchedulesHelper.usage_from_function(function) == usage_zone.usage: + self._idf_schedules_path = (base_path / schedule_archetype['idf']['path']).resolve() + with open(self._idf_schedules_path, 'r') as file: + idf = parseidf.parse(file.read()) + self._load_schedule(idf, usage_zone) + break def _load_schedule(self, idf, usage_zone): schedules_day = {} @@ -131,4 +134,12 @@ class DoeIdf: continue schedules.append(schedule) - usage_zone.schedules = schedules + for schedule in schedules: + if schedule.type == cte.OCCUPANCY: + if usage_zone.occupancy is None: + usage_zone.occupancy = Occupancy() + usage_zone.occupancy.occupancy_schedules = [schedule] + elif schedule.type == cte.LIGHTING: + if usage_zone.lighting is None: + usage_zone.lighting = Lighting() + usage_zone.lighting.schedules = [schedule] diff --git a/imports/schedules/helpers/schedules_helper.py b/imports/schedules/helpers/schedules_helper.py index 9aca6fab..4d4881a5 100644 --- a/imports/schedules/helpers/schedules_helper.py +++ b/imports/schedules/helpers/schedules_helper.py @@ -15,10 +15,10 @@ class SchedulesHelper: _usage_to_comnet = { cte.RESIDENTIAL: 'C-12 Residential', cte.INDUSTRY: 'C-10 Warehouse', - cte.OFFICE_ADMINISTRATION: 'C-5 Office', + cte.OFFICE_AND_ADMINISTRATION: 'C-5 Office', cte.HOTEL: 'C-3 Hotel', cte.HEALTH_CARE: 'C-2 Health', - cte.RETAIL: 'C-8 Retail', + cte.RETAIL_SHOP_WITHOUT_REFRIGERATED_FOOD: 'C-8 Retail', cte.HALL: 'C-8 Retail', cte.RESTAURANT: 'C-7 Restaurant', cte.EDUCATION: 'C-9 School' @@ -36,18 +36,18 @@ class SchedulesHelper: 'high-rise apartment': cte.RESIDENTIAL, 'hospital': cte.HEALTH_CARE, 'large hotel': cte.HOTEL, - 'large office': cte.OFFICE_ADMINISTRATION, - 'medium office': cte.OFFICE_ADMINISTRATION, + 'large office': cte.OFFICE_AND_ADMINISTRATION, + 'medium office': cte.OFFICE_AND_ADMINISTRATION, 'midrise apartment': cte.RESIDENTIAL, 'outpatient healthcare': cte.HEALTH_CARE, 'primary school': cte.EDUCATION, 'quick service restaurant': cte.RESTAURANT, 'secondary school': cte.EDUCATION, 'small hotel': cte.HOTEL, - 'small office': cte.OFFICE_ADMINISTRATION, - 'stand-alone-retail': cte.RETAIL, + 'small office': cte.OFFICE_AND_ADMINISTRATION, + 'stand-alone-retail': cte.RETAIL_SHOP_WITHOUT_REFRIGERATED_FOOD, 'strip mall': cte.HALL, - 'supermarket': cte.RETAIL, + 'supermarket': cte.RETAIL_SHOP_WITH_REFRIGERATED_FOOD, 'warehouse': cte.INDUSTRY, 'residential': cte.RESIDENTIAL } diff --git a/imports/schedules_factory.py b/imports/schedules_factory.py index 8437593f..3842889a 100644 --- a/imports/schedules_factory.py +++ b/imports/schedules_factory.py @@ -6,7 +6,6 @@ Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@conc """ from pathlib import Path -from imports.schedules.comnet_schedules_parameters import ComnetSchedules from imports.schedules.doe_idf import DoeIdf @@ -19,15 +18,11 @@ class SchedulesFactory: self._city = city self._base_path = base_path for building in city.buildings: - if len(building.usage_zones) == 0: - raise Exception('It seems that the schedule factory is being called before the usage factory. ' - 'Please ensure that the usage factory is called first.') - - def _comnet(self): - """ - Enrich the city by using COMNET schedules as data source - """ - ComnetSchedules(self._city, self._base_path) + for internal_zone in building.internal_zones: + if len(internal_zone.usage_zones) == 0: + raise Exception('It seems that the schedule factory is being called before the usage factory. ' + 'Please ensure that the usage factory is called first as the usage zones must be ' + 'firstly generated.') def _doe_idf(self): """ diff --git a/imports/usage/ca_usage_parameters.py b/imports/usage/ca_usage_parameters.py index c9034b85..b2320fa6 100644 --- a/imports/usage/ca_usage_parameters.py +++ b/imports/usage/ca_usage_parameters.py @@ -5,7 +5,9 @@ Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.mons """ import sys +from imports.geometry.helpers.geometry_helper import GeometryHelper from imports.usage.hft_usage_interface import HftUsageInterface +from imports.usage.helpers.usage_helper import UsageHelper from city_model_structure.building_demand.usage_zone import UsageZone from city_model_structure.building_demand.internal_gains import InternalGains from city_model_structure.building_demand.occupancy import Occupancy @@ -28,9 +30,9 @@ class CaUsageParameters(HftUsageInterface): """ city = self._city for building in city.buildings: + usage = GeometryHelper().libs_usage_from_libs_function(building.function) try: - print(building.function) - archetype = self._search_archetype(building.function) + archetype = self._search_archetype(usage) except KeyError: sys.stderr.write(f'Building {building.name} has unknown archetype for building function:' f' {building.function}\n') @@ -43,7 +45,8 @@ class CaUsageParameters(HftUsageInterface): self._assign_values_usage_zone(usage_zone, archetype) internal_zone.usage_zones = [usage_zone] - def _search_archetype(self, building_usage): + def _search_archetype(self, libs_usage): + building_usage = UsageHelper().hft_from_libs_usage(libs_usage) for building_archetype in self._usage_archetypes: if building_archetype.usage == building_usage: return building_archetype diff --git a/imports/usage/comnet_usage_parameters.py b/imports/usage/comnet_usage_parameters.py index 851473da..5d159af7 100644 --- a/imports/usage/comnet_usage_parameters.py +++ b/imports/usage/comnet_usage_parameters.py @@ -175,13 +175,13 @@ class ComnetUsageParameters: return _usage_zone - def _search_archetypes(self, usage): + def _search_archetypes(self, libs_usage): for item in self._data['lighting']: - comnet_usage = UsageHelper.comnet_from_usage(usage) + comnet_usage = UsageHelper.comnet_from_libs_usage(libs_usage) if comnet_usage == item: usage_archetype = self._parse_usage_type(comnet_usage, self._data, self._xls) return usage_archetype - return None, None + return None def enrich_buildings(self): """ @@ -190,13 +190,13 @@ class ComnetUsageParameters: """ city = self._city for building in city.buildings: - usage = GeometryHelper.usage_from_function(building.function) + usage = GeometryHelper.libs_usage_from_libs_function(building.function) try: archetype_usage = self._search_archetypes(usage) except KeyError: sys.stderr.write(f'Building {building.name} has unknown archetype for building function:' f' {building.function}, that assigns building usage as ' - f'{GeometryHelper.usage_from_function(building.function)}\n') + f'{GeometryHelper.libs_usage_from_libs_function(building.function)}\n') return for internal_zone in building.internal_zones: diff --git a/imports/usage/helpers/usage_helper.py b/imports/usage/helpers/usage_helper.py index f6257503..98f873da 100644 --- a/imports/usage/helpers/usage_helper.py +++ b/imports/usage/helpers/usage_helper.py @@ -13,17 +13,30 @@ class UsageHelper: """ _usage_to_hft = { cte.RESIDENTIAL: 'residential', - cte.INDUSTRY: 'industry', - cte.OFFICE_ADMINISTRATION: 'office and administration', + cte.SINGLE_FAMILY_HOUSE: 'Single family house', + cte.MULTI_FAMILY_HOUSE: 'Multi-family house', + cte.EDUCATION: 'education', + cte.SCHOOL_WITHOUT_SHOWER: 'school without shower', + cte.SCHOOL_WITH_SHOWER: 'school with shower', + cte.RETAIL_SHOP_WITHOUT_REFRIGERATED_FOOD: 'retail', + cte.RETAIL_SHOP_WITH_REFRIGERATED_FOOD: 'retail shop / refrigerated food', cte.HOTEL: 'hotel', - cte.HEALTH_CARE: 'health care', - cte.RETAIL: 'retail', - cte.HALL: 'hall', + cte.HOTEL_MEDIUM_CLASS: 'hotel (Medium-class)', + cte.DORMITORY: 'dormitory', + cte.INDUSTRY: 'industry', cte.RESTAURANT: 'restaurant', - cte.EDUCATION: 'education'} + cte.HEALTH_CARE: 'health care', + cte.RETIREMENT_HOME_OR_ORPHANAGE: 'Home for the aged or orphanage', + cte.OFFICE_AND_ADMINISTRATION: 'office and administration', + cte.EVENT_LOCATION: 'event location', + cte.HALL: 'hall', + cte.SPORTS_LOCATION: 'sport location', + cte.LABOR: 'Labor', + cte.GREEN_HOUSE: 'green house', + cte.NON_HEATED: 'non-heated'} @staticmethod - def hft_from_usage(usage): + def hft_from_libs_usage(usage): """ Get HfT usage from the given internal usage key :param usage: str @@ -32,18 +45,32 @@ class UsageHelper: try: return UsageHelper._usage_to_hft[usage] except KeyError: - sys.stderr.write('Error: keyword not found.\n') + sys.stderr.write('Error: keyword not found to translate from libs_usage to hft usage.\n') _usage_to_comnet = { cte.RESIDENTIAL: 'BA Multifamily', - cte.INDUSTRY: 'BA Manufacturing Facility', - cte.OFFICE_ADMINISTRATION: 'BA Office', + cte.SINGLE_FAMILY_HOUSE: 'BA Multifamily', + cte.MULTI_FAMILY_HOUSE: 'BA Multifamily', + cte.EDUCATION: 'BA School/University', + cte.SCHOOL_WITHOUT_SHOWER: 'BA School/University', + cte.SCHOOL_WITH_SHOWER: 'BA School/University', + cte.RETAIL_SHOP_WITHOUT_REFRIGERATED_FOOD: 'BA Retail', + cte.RETAIL_SHOP_WITH_REFRIGERATED_FOOD: 'BA Retail', cte.HOTEL: 'BA Hotel', + cte.HOTEL_MEDIUM_CLASS: 'BA Hotel', + cte.DORMITORY: 'BA Dormitory', + cte.INDUSTRY: 'BA Manufacturing Facility', + cte.RESTAURANT: 'BA Dining: Family', cte.HEALTH_CARE: 'BA Hospital', - cte.RETAIL: 'BA Retail', - cte.HALL: 'BA Town Hall', - cte.RESTAURANT: 'BA Dining: Bar Lounge/Leisure', - cte.EDUCATION: 'BA School/University'} + cte.RETIREMENT_HOME_OR_ORPHANAGE: 'BA Multifamily', + cte.OFFICE_AND_ADMINISTRATION: 'BA Office', + cte.EVENT_LOCATION: 'BA Convention Center', + cte.HALL: 'BA Convention Center', + cte.SPORTS_LOCATION: 'BA Sports Arena', + cte.LABOR: 'BA Gymnasium', + cte.GREEN_HOUSE: cte.GREEN_HOUSE, + cte.NON_HEATED: cte.NON_HEATED + } _comnet_schedules_key_to_comnet_schedules = { 'C-1 Assembly': 'C-1 Assembly', @@ -62,7 +89,7 @@ class UsageHelper: 'C-14 Gymnasium': 'C-14 Gymnasium'} @staticmethod - def comnet_from_usage(usage): + def comnet_from_libs_usage(usage): """ Get Comnet usage from the given internal usage key :param usage: str @@ -71,7 +98,7 @@ class UsageHelper: try: return UsageHelper._usage_to_comnet[usage] except KeyError: - sys.stderr.write('Error: keyword not found.\n') + sys.stderr.write('Error: keyword not found to translate from libs_usage to comnet usage.\n') @staticmethod def schedules_key(usage): diff --git a/imports/usage/hft_usage_interface.py b/imports/usage/hft_usage_interface.py index 340d8a80..959afbef 100644 --- a/imports/usage/hft_usage_interface.py +++ b/imports/usage/hft_usage_interface.py @@ -5,6 +5,7 @@ SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2022 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ import xmltodict +import copy from city_model_structure.building_demand.usage_zone import UsageZone from city_model_structure.building_demand.internal_gains import InternalGains from city_model_structure.building_demand.occupancy import Occupancy @@ -33,8 +34,6 @@ class HftUsageInterface: usage = usage_zone_variant['id'] usage_archetype_variant = self._parse_zone_usage_variant(usage, usage_archetype, usage_zone_variant) self._usage_archetypes.append(usage_archetype_variant) - for usage in self._usage_archetypes: - print(usage.usage) @staticmethod def _parse_zone_usage_type(usage, zone_usage_type): @@ -135,7 +134,7 @@ class HftUsageInterface: @staticmethod def _parse_zone_usage_variant(usage, usage_zone, usage_zone_variant): # the variants mimic the inheritance concept from OOP - usage_zone_archetype = usage_zone + usage_zone_archetype = copy.deepcopy(usage_zone) usage_zone_archetype.usage = usage if 'occupancy' in usage_zone_variant: diff --git a/imports/usage/hft_usage_parameters.py b/imports/usage/hft_usage_parameters.py index d95a2c3c..48a7b267 100644 --- a/imports/usage/hft_usage_parameters.py +++ b/imports/usage/hft_usage_parameters.py @@ -4,14 +4,12 @@ SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ import sys +import copy -from imports.geometry.helpers.geometry_helper import GeometryHelper as gh +from imports.geometry.helpers.geometry_helper import GeometryHelper from imports.usage.hft_usage_interface import HftUsageInterface +from imports.usage.helpers.usage_helper import UsageHelper from city_model_structure.building_demand.usage_zone import UsageZone -from city_model_structure.building_demand.internal_gains import InternalGains -from city_model_structure.building_demand.occupancy import Occupancy -from city_model_structure.building_demand.appliances import Appliances -from city_model_structure.building_demand.thermal_control import ThermalControl class HftUsageParameters(HftUsageInterface): @@ -29,23 +27,25 @@ class HftUsageParameters(HftUsageInterface): """ city = self._city for building in city.buildings: - usage = gh.usage_from_function(building.function) + usage = GeometryHelper().libs_usage_from_libs_function(building.function) try: archetype = self._search_archetype(usage) except KeyError: sys.stderr.write(f'Building {building.name} has unknown archetype for building function:' f' {building.function}, that assigns building usage as ' - f'{gh.usage_from_function(building.function)}\n') + f'{GeometryHelper().libs_usage_from_libs_function(building.function)}\n') return for internal_zone in building.internal_zones: usage_zone = UsageZone() - usage_zone.usage = building.function + libs_usage = GeometryHelper().libs_usage_from_libs_function(building.function) + usage_zone.usage = UsageHelper().hft_from_libs_usage(libs_usage) self._assign_values(usage_zone, archetype) usage_zone.percentage = 1 internal_zone.usage_zones = [usage_zone] - def _search_archetype(self, building_usage): + def _search_archetype(self, libs_usage): + building_usage = UsageHelper().hft_from_libs_usage(libs_usage) for building_archetype in self._usage_archetypes: if building_archetype.usage == building_usage: return building_archetype @@ -53,7 +53,7 @@ class HftUsageParameters(HftUsageInterface): @staticmethod def _assign_values(usage_zone, archetype): - # Due to the fact that python is not a typed language, the wrong object type is assigned to + """ # Due to the fact that python is not a typed language, the wrong object type is assigned to # usage_zone.internal_gains when writing usage_zone.internal_gains = archetype.internal_gains. # Therefore, this walk around has been done. # Due to the fact that python is not a typed language, the wrong object type is assigned to @@ -83,3 +83,12 @@ class HftUsageParameters(HftUsageInterface): _internal_gain.schedules = archetype_internal_gain.schedules _internal_gains.append(_internal_gain) usage_zone.not_detailed_source_mean_annual_internal_gains = _internal_gains + """ + usage_zone.mechanical_air_change = archetype.mechanical_air_change + usage_zone.occupancy = copy.deepcopy(archetype.occupancy) + usage_zone.appliances = copy.deepcopy(archetype.appliances) + usage_zone.thermal_control = copy.deepcopy(archetype.thermal_control) + usage_zone.not_detailed_source_mean_annual_internal_gains = \ + copy.deepcopy(archetype.not_detailed_source_mean_annual_internal_gains) + usage_zone.days_year = archetype.days_year + usage_zone.hours_day = archetype.hours_day diff --git a/recognized_functions_and_usages.md b/recognized_functions_and_usages.md new file mode 100644 index 00000000..fad74352 --- /dev/null +++ b/recognized_functions_and_usages.md @@ -0,0 +1,64 @@ +# Functions and usages internally recognized within the libs + +The libs uses a list of building functions a building usages that are the only ones recognized. All new categories should be added to the dicctionaries that translate from the input formats to the libs functions. From the libs functions to the libs usages and from the libs usages and libs functions to the output formats. + +Input formats accepted: +* Function: + * pluto + * hft + +Output formats accepted: +* Function: + * nrel + * nrcan +* Usage: + * ca + * hft + * comnet + +Libs_functions: +* single family house +* multi family house +* row hose +* mid rise apartment +* high rise apartment +* residential +* small office +* medium office +* large office +* primary school +* secondary school +* stand alone retail +* hospital +* out-patient health care +* strip mall +* supermarket +* ware house +* quick service restaurant +* full service restaurant +* small hotel +* large hotel + +Libs_usage: +* residential +* single family house +* multi family house +* education +* school without shower +* school with shower +* retail shop without refrigerated food +* retail shop with refrigerated food +* hotel +* hotel medium class +* dormitory +* industry +* restaurant +* health care +* retirement home or orphanage +* office and administration +* event location +* hall +* sports location +* labor +* green-house +* non-heated diff --git a/unittests/test_construction_factory.py b/unittests/test_construction_factory.py index df7e1edd..9348e8d3 100644 --- a/unittests/test_construction_factory.py +++ b/unittests/test_construction_factory.py @@ -64,7 +64,6 @@ class TestConstructionFactory(TestCase): self.assertEqual(len(building.heating), 0, 'building heating is not none') self.assertEqual(len(building.cooling), 0, 'building cooling is not none') self.assertIsNotNone(building.eave_height, 'building eave height is none') - self.assertIsNotNone(building.storeys, 'building storeys are not defined') self.assertIsNotNone(building.roof_type, 'building roof type is none') self.assertIsNotNone(building.floor_area, 'building floor_area is none') self.assertIsNone(building.households, 'building households is not none') @@ -93,7 +92,7 @@ class TestConstructionFactory(TestCase): for thermal_boundary in thermal_zone.thermal_boundaries: self.assertIsNotNone(thermal_boundary.id, 'thermal_boundary id is none') self.assertIsNotNone(thermal_boundary.parent_surface, 'thermal_boundary surface is none') - self.assertIsNotNone(thermal_boundary.thermal_zones, 'thermal_boundary delimits is none') + self.assertIsNotNone(thermal_boundary.thermal_zones, 'thermal_boundary delimits no thermal zone') self.assertIsNotNone(thermal_boundary.opaque_area, 'thermal_boundary area is none') self.assertIsNotNone(thermal_boundary.azimuth, 'thermal_boundary azimuth is none') self.assertIsNotNone(thermal_boundary.inclination, 'thermal_boundary inclination is none') @@ -166,7 +165,7 @@ class TestConstructionFactory(TestCase): file = 'one_building_in_kelowna.gml' city = self._get_citygml(file) for building in city.buildings: - building.function = GeometryHelper.function_from_hft(building.function) + building.function = GeometryHelper.libs_function_from_hft(building.function) ConstructionFactory('nrcan', city).enrich() self._check_buildings(city) @@ -196,7 +195,7 @@ class TestConstructionFactory(TestCase): file = 'pluto_building.gml' city = self._get_citygml(file) for building in city.buildings: - building.function = GeometryHelper.function_from_pluto(building.function) + building.function = GeometryHelper.libs_function_from_pluto(building.function) ConstructionFactory('nrel', city).enrich() self._check_buildings(city) @@ -226,9 +225,9 @@ class TestConstructionFactory(TestCase): @staticmethod def _internal_function(function_format, original_function): if function_format == 'hft': - new_function = GeometryHelper.function_from_hft(original_function) + new_function = GeometryHelper.libs_function_from_hft(original_function) elif function_format == 'pluto': - new_function = GeometryHelper.function_from_pluto(original_function) + new_function = GeometryHelper.libs_function_from_pluto(original_function) else: raise Exception('Function key not recognized. Implemented only "hft" and "pluto"') return new_function diff --git a/unittests/test_customized_imports_factory.py b/unittests/test_customized_imports_factory.py index 80373480..245a8821 100644 --- a/unittests/test_customized_imports_factory.py +++ b/unittests/test_customized_imports_factory.py @@ -6,8 +6,8 @@ Copyright © 2021 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.mons from pathlib import Path from unittest import TestCase +import helpers.constants as cte from imports.geometry_factory import GeometryFactory -from imports.construction_factory import ConstructionFactory from imports.usage_factory import UsageFactory from imports.customized_imports_factory import CustomizedImportsFactory from imports.customized_imports.sanam_customized_usage_parameters import SanamCustomizedUsageParameters as scp @@ -28,7 +28,6 @@ class TestCustomizedImportsFactory(TestCase): file_path = (self._example_path / file).resolve() _city = GeometryFactory('citygml', file_path).city self.assertIsNotNone(_city, 'city is none') - ConstructionFactory('nrel', _city).enrich() UsageFactory('hft', _city).enrich() return _city @@ -44,6 +43,9 @@ class TestCustomizedImportsFactory(TestCase): CustomizedImportsFactory(scp, city).enrich() for building in city.buildings: - self.assertIsNot(len(building.usage_zones), 0, 'no building usage_zones defined') - for usage_zone in building.usage_zones: - self.assertIsNotNone(usage_zone.mechanical_air_change, 'usage is none') + self.assertIsNot(len(building.internal_zones), 0, 'no building internal_zones defined') + for internal_zone in building.internal_zones: + for usage_zone in internal_zone.usage_zones: + if usage_zone.usage != cte.RESIDENTIAL: + self.assertIsNotNone(usage_zone.mechanical_air_change, 'mechanical air change rate is none') + self.assertIsNotNone(usage_zone.occupancy.occupancy_density, 'occupancy density us none') diff --git a/unittests/test_doe_idf.py b/unittests/test_doe_idf.py index 76ce8440..c75e6372 100644 --- a/unittests/test_doe_idf.py +++ b/unittests/test_doe_idf.py @@ -34,15 +34,22 @@ class TestBuildings(TestCase): ExportsFactory('idf', city, output_path).export() self.assertEqual(10, len(city.buildings)) for building in city.buildings: - self.assertTrue(len(building.usage_zones) > 0) - for usage_zone in building.usage_zones: - self.assertIsNot(len(usage_zone.schedules), 0, 'no usage_zones schedules defined') - for schedule in usage_zone.schedules: - self.assertIsNotNone(schedule.type) - self.assertIsNotNone(schedule.values) - self.assertIsNotNone(schedule.data_type) - self.assertIsNotNone(schedule.time_step) - self.assertIsNotNone(schedule.time_range) - self.assertIsNotNone(schedule.day_types) - - + for internal_zone in building.internal_zones: + self.assertTrue(len(internal_zone.usage_zones) > 0) + for usage_zone in internal_zone.usage_zones: + self.assertIsNot(len(usage_zone.occupancy.occupancy_schedules), 0, 'no occupancy schedules defined') + for schedule in usage_zone.occupancy.occupancy_schedules: + self.assertIsNotNone(schedule.type) + self.assertIsNotNone(schedule.values) + self.assertIsNotNone(schedule.data_type) + self.assertIsNotNone(schedule.time_step) + self.assertIsNotNone(schedule.time_range) + self.assertIsNotNone(schedule.day_types) + self.assertIsNot(len(usage_zone.lighting.schedules), 0, 'no lighting schedules defined') + for schedule in usage_zone.lighting.schedules: + self.assertIsNotNone(schedule.type) + self.assertIsNotNone(schedule.values) + self.assertIsNotNone(schedule.data_type) + self.assertIsNotNone(schedule.time_step) + self.assertIsNotNone(schedule.time_range) + self.assertIsNotNone(schedule.day_types) diff --git a/unittests/test_enrichement.py b/unittests/test_enrichement.py index 7c5861d5..95861429 100644 --- a/unittests/test_enrichement.py +++ b/unittests/test_enrichement.py @@ -54,10 +54,10 @@ class TestGeometryFactory(TestCase): def _prepare_case_usage_first(city, input_key, construction_key, usage_key): if input_key == 'pluto': for building in city.buildings: - building.function = GeometryHelper.function_from_pluto(building.function) + building.function = GeometryHelper.libs_function_from_pluto(building.function) elif input_key == 'hft': for building in city.buildings: - building.function = GeometryHelper.function_from_hft(building.function) + building.function = GeometryHelper.libs_function_from_hft(building.function) UsageFactory(usage_key, city).enrich() ConstructionFactory(construction_key, city).enrich() @@ -65,10 +65,10 @@ class TestGeometryFactory(TestCase): def _prepare_case_construction_first(city, input_key, construction_key, usage_key): if input_key == 'pluto': for building in city.buildings: - building.function = GeometryHelper.function_from_pluto(building.function) + building.function = GeometryHelper.libs_function_from_pluto(building.function) elif input_key == 'hft': for building in city.buildings: - building.function = GeometryHelper.function_from_hft(building.function) + building.function = GeometryHelper.libs_function_from_hft(building.function) ConstructionFactory(construction_key, city).enrich() UsageFactory(usage_key, city).enrich() @@ -112,34 +112,52 @@ class TestGeometryFactory(TestCase): self._check_thermal_zones(thermal_zone) for construction_key in _construction_keys: - for usage_key in _usage_keys: - if usage_key != 'ca': - city = self._get_citygml(file_2) - self.assertTrue(len(city.buildings) == 1) - self._prepare_case_construction_first(city, 'pluto', construction_key, usage_key) - self._check_buildings(city) - for building in city.buildings: - for internal_zone in building.internal_zones: - self.assertIsNot(len(internal_zone.usage_zones), 0, 'no building usage_zones defined') - for usage_zone in internal_zone.usage_zones: - self._check_usage_zone(usage_zone) - for thermal_zone in internal_zone.thermal_zones: - self._check_thermal_zones(thermal_zone) + if construction_key != 'nrcan': + for usage_key in _usage_keys: + if usage_key != 'ca': + city = self._get_citygml(file_2) + self.assertTrue(len(city.buildings) == 1) + self._prepare_case_construction_first(city, 'pluto', construction_key, usage_key) + self._check_buildings(city) + for building in city.buildings: + for internal_zone in building.internal_zones: + self.assertIsNot(len(internal_zone.usage_zones), 0, 'no building usage_zones defined') + for usage_zone in internal_zone.usage_zones: + self._check_usage_zone(usage_zone) + for thermal_zone in internal_zone.thermal_zones: + self._check_thermal_zones(thermal_zone) for construction_key in _construction_keys: - for usage_key in _usage_keys: - if usage_key != 'ca': - city = self._get_citygml(file_2) - self.assertTrue(len(city.buildings) == 1) - self._prepare_case_usage_first(city, 'pluto', construction_key, usage_key) - self._check_buildings(city) - for building in city.buildings: - for internal_zone in building.internal_zones: - self.assertIsNot(len(internal_zone.usage_zones), 0, 'no building usage_zones defined') - for usage_zone in internal_zone.usage_zones: - self._check_usage_zone(usage_zone) - for thermal_zone in internal_zone.thermal_zones: - self._check_thermal_zones(thermal_zone) + if construction_key != 'nrcan': + for usage_key in _usage_keys: + if usage_key != 'ca': + city = self._get_citygml(file_2) + self.assertTrue(len(city.buildings) == 1) + self._prepare_case_usage_first(city, 'pluto', construction_key, usage_key) + self._check_buildings(city) + for building in city.buildings: + for internal_zone in building.internal_zones: + self.assertIsNot(len(internal_zone.usage_zones), 0, 'no building usage_zones defined') + for usage_zone in internal_zone.usage_zones: + self._check_usage_zone(usage_zone) + for thermal_zone in internal_zone.thermal_zones: + self._check_thermal_zones(thermal_zone) city = self._get_citygml(file_3) self.assertTrue(len(city.buildings) == 10) + + for construction_key in _construction_keys: + if construction_key != 'nrcan': + for usage_key in _usage_keys: + if usage_key != 'ca': + city = self._get_citygml(file_2) + self.assertTrue(len(city.buildings) == 1) + self._prepare_case_construction_first(city, 'pluto', construction_key, usage_key) + self._check_buildings(city) + for building in city.buildings: + for internal_zone in building.internal_zones: + self.assertIsNot(len(internal_zone.usage_zones), 0, 'no building usage_zones defined') + for usage_zone in internal_zone.usage_zones: + self._check_usage_zone(usage_zone) + for thermal_zone in internal_zone.thermal_zones: + self._check_thermal_zones(thermal_zone) diff --git a/unittests/test_exports.py b/unittests/test_exports.py index b4c38be1..20900b6f 100644 --- a/unittests/test_exports.py +++ b/unittests/test_exports.py @@ -9,8 +9,8 @@ from pathlib import Path from unittest import TestCase import pandas as pd from imports.geometry_factory import GeometryFactory +from imports.geometry.helpers.geometry_helper import GeometryHelper from imports.construction_factory import ConstructionFactory -from imports.schedules_factory import SchedulesFactory from imports.usage_factory import UsageFactory from exports.exports_factory import ExportsFactory import helpers.constants as cte @@ -45,9 +45,10 @@ class TestExports(TestCase): else: file_path = (self._example_path / 'one_building_in_kelowna.gml').resolve() self._complete_city = self._get_citygml(file_path) + for building in self._complete_city.buildings: + building.function = GeometryHelper().libs_function_from_hft(building.function) ConstructionFactory('nrel', self._complete_city).enrich() UsageFactory('ca', self._complete_city).enrich() - SchedulesFactory('comnet', self._complete_city).enrich() cli = 'C:\\Users\\Pilar\\PycharmProjects\\monthlyenergybalance\\tests_data\\weather\\inseldb_Summerland.cli' self._complete_city.climate_file = Path(cli) self._complete_city.climate_reference_city = 'Summerland' diff --git a/unittests/test_geometry_factory.py b/unittests/test_geometry_factory.py index db5ae4bb..d3fc1473 100644 --- a/unittests/test_geometry_factory.py +++ b/unittests/test_geometry_factory.py @@ -55,8 +55,10 @@ class TestGeometryFactory(TestCase): self.assertIsNotNone(building.grounds, 'building grounds is none') self.assertIsNotNone(building.walls, 'building walls is none') self.assertIsNotNone(building.roofs, 'building roofs is none') - self.assertIsNone(building.usage_zones, 'usage zones are defined') - self.assertIsNone(building.thermal_zones, 'thermal zones are defined') + self.assertIsNotNone(building.internal_zones, 'building internal zones is none') + for internal_zone in building.internal_zones: + self.assertIsNone(internal_zone.usage_zones, 'usage zones are defined') + self.assertIsNone(internal_zone.thermal_zones, 'thermal zones are defined') self.assertIsNone(building.basement_heated, 'building basement_heated is not none') self.assertIsNone(building.attic_heated, 'building attic_heated is not none') self.assertIsNone(building.terrains, 'building terrains is not none') @@ -67,7 +69,6 @@ class TestGeometryFactory(TestCase): self.assertEqual(len(building.heating), 0, 'building heating is not none') self.assertEqual(len(building.cooling), 0, 'building cooling is not none') self.assertIsNotNone(building.eave_height, 'building eave height is none') - self.assertIsNone(building.storeys, 'building storeys are defined') self.assertIsNotNone(building.roof_type, 'building roof type is none') self.assertIsNotNone(building.floor_area, 'building floor_area is none') self.assertIsNone(building.households, 'building households is not none') diff --git a/unittests/test_schedules_factory.py b/unittests/test_schedules_factory.py index 0506abae..f59f3cf6 100644 --- a/unittests/test_schedules_factory.py +++ b/unittests/test_schedules_factory.py @@ -31,30 +31,10 @@ class TestSchedulesFactory(TestCase): ConstructionFactory('nrel', _city).enrich() self.assertIsNotNone(_city, 'city is none') for building in _city.buildings: - building.function = GeometryHelper.hft_to_function[building.function] + building.function = GeometryHelper.libs_function_from_hft(building.function) UsageFactory('hft', _city).enrich() return _city - def test_comnet_archetypes(self): - """ - Enrich the city with commet schedule archetypes and verify it - """ - file = (self._example_path / 'one_building_in_kelowna.gml').resolve() - city = self._get_citygml(file) - occupancy_handler = 'comnet' - SchedulesFactory(occupancy_handler, city).enrich() - for building in city.buildings: - self.assertIsNot(len(building.usage_zones), 0, 'no building usage_zones defined') - for usage_zone in building.usage_zones: - self.assertIsNot(len(usage_zone.schedules), 0, 'no usage_zones schedules defined') - for schedule in usage_zone.schedules: - self.assertIsNotNone(schedule.type) - self.assertIsNotNone(schedule.values) - self.assertIsNotNone(schedule.data_type) - self.assertIsNotNone(schedule.time_step) - self.assertIsNotNone(schedule.time_range) - self.assertIsNotNone(schedule.day_types) - def test_doe_idf_archetypes(self): """ Enrich the city with doe_idf schedule archetypes and verify it @@ -64,7 +44,22 @@ class TestSchedulesFactory(TestCase): occupancy_handler = 'doe_idf' SchedulesFactory(occupancy_handler, city).enrich() for building in city.buildings: - for usage_zone in building.usage_zones: - for schedule in usage_zone.schedules: - print(schedule) - print(usage_zone.schedules[schedule]) + for internal_zone in building.internal_zones: + self.assertTrue(len(internal_zone.usage_zones) > 0) + for usage_zone in internal_zone.usage_zones: + self.assertIsNot(len(usage_zone.occupancy.occupancy_schedules), 0, 'no occupancy schedules defined') + for schedule in usage_zone.occupancy.occupancy_schedules: + self.assertIsNotNone(schedule.type) + self.assertIsNotNone(schedule.values) + self.assertIsNotNone(schedule.data_type) + self.assertIsNotNone(schedule.time_step) + self.assertIsNotNone(schedule.time_range) + self.assertIsNotNone(schedule.day_types) + self.assertIsNot(len(usage_zone.lighting.schedules), 0, 'no lighting schedules defined') + for schedule in usage_zone.lighting.schedules: + self.assertIsNotNone(schedule.type) + self.assertIsNotNone(schedule.values) + self.assertIsNotNone(schedule.data_type) + self.assertIsNotNone(schedule.time_step) + self.assertIsNotNone(schedule.time_range) + self.assertIsNotNone(schedule.day_types) diff --git a/unittests/test_usage_factory.py b/unittests/test_usage_factory.py index bae8dc2c..c3d2a0fe 100644 --- a/unittests/test_usage_factory.py +++ b/unittests/test_usage_factory.py @@ -52,7 +52,7 @@ class TestUsageFactory(TestCase): self.assertIsNotNone(building.roofs, 'building roofs is none') for internal_zone in building.internal_zones: self.assertTrue(len(internal_zone.usage_zones) > 0, 'usage zones are not defined') - self.assertIsNone(building.thermal_zones, 'thermal zones are defined') + self.assertIsNone(internal_zone.thermal_zones, 'thermal zones are defined') self.assertIsNone(building.basement_heated, 'building basement_heated is not none') self.assertIsNone(building.attic_heated, 'building attic_heated is not none') self.assertIsNone(building.terrains, 'building terrains is not none') @@ -63,7 +63,6 @@ class TestUsageFactory(TestCase): self.assertEqual(len(building.heating), 0, 'building heating is not none') self.assertEqual(len(building.cooling), 0, 'building cooling is not none') self.assertIsNotNone(building.eave_height, 'building eave height is none') - self.assertIsNone(building.storeys, 'building storeys are defined') self.assertIsNotNone(building.roof_type, 'building roof type is none') self.assertIsNotNone(building.floor_area, 'building floor_area is none') self.assertIsNone(building.households, 'building households is not none') @@ -75,7 +74,6 @@ class TestUsageFactory(TestCase): self.assertIsNotNone(usage_zone.get_internal_gains, 'internal gains is none') self.assertIsNotNone(usage_zone.hours_day, 'hours per day is none') self.assertIsNotNone(usage_zone.days_year, 'days per year is none') - self.assertIsNotNone(usage_zone.mechanical_air_change, 'mechanical air change is none') self.assertIsNotNone(usage_zone.thermal_control, 'thermal control is none') self.assertIsNotNone(usage_zone.thermal_control.mean_heating_set_point, 'control heating set point is none') self.assertIsNotNone(usage_zone.thermal_control.heating_set_back, 'control heating set back is none') @@ -88,7 +86,7 @@ class TestUsageFactory(TestCase): file = 'pluto_building.gml' city = self._get_citygml(file) for building in city.buildings: - building.function = GeometryHelper.function_from_pluto(building.function) + building.function = GeometryHelper.libs_function_from_pluto(building.function) UsageFactory('comnet', city).enrich() self._check_buildings(city) @@ -97,6 +95,7 @@ class TestUsageFactory(TestCase): self.assertIsNot(len(internal_zone.usage_zones), 0, 'no building usage_zones defined') for usage_zone in internal_zone.usage_zones: self._check_usage_zone(usage_zone) + self.assertIsNotNone(usage_zone.mechanical_air_change, 'mechanical air change is none') self.assertIsNotNone(usage_zone.thermal_control.heating_set_point_schedules, 'control heating set point schedule is none') self.assertIsNotNone(usage_zone.thermal_control.cooling_set_point_schedules, @@ -141,6 +140,7 @@ class TestUsageFactory(TestCase): self.assertIsNot(len(internal_zone.usage_zones), 0, 'no building usage_zones defined') for usage_zone in internal_zone.usage_zones: self._check_usage_zone(usage_zone) + self.assertIsNotNone(usage_zone.mechanical_air_change, 'mechanical air change is none') self.assertIsNotNone(usage_zone.not_detailed_source_mean_annual_internal_gains, 'not detailed internal gains is none') @@ -151,7 +151,7 @@ class TestUsageFactory(TestCase): file = 'pluto_building.gml' city = self._get_citygml(file) for building in city.buildings: - building.function = GeometryHelper.function_from_pluto(building.function) + building.function = GeometryHelper.libs_function_from_pluto(building.function) UsageFactory('hft', city).enrich() self._check_buildings(city) @@ -160,6 +160,7 @@ class TestUsageFactory(TestCase): self.assertIsNot(len(internal_zone.usage_zones), 0, 'no building usage_zones defined') for usage_zone in internal_zone.usage_zones: self._check_usage_zone(usage_zone) + self.assertIsNone(usage_zone.mechanical_air_change, 'mechanical air change is not none') self.assertIsNotNone(usage_zone.thermal_control.heating_set_point_schedules, 'control heating set point schedule is none') self.assertIsNotNone(usage_zone.thermal_control.cooling_set_point_schedules, @@ -167,24 +168,13 @@ class TestUsageFactory(TestCase): self.assertIsNotNone(usage_zone.occupancy, 'occupancy is none') occupancy = usage_zone.occupancy self.assertIsNotNone(occupancy.occupancy_density, 'occupancy density is none') - self.assertIsNotNone(occupancy.latent_internal_gain, 'occupancy latent internal gain is none') - self.assertIsNotNone(occupancy.sensible_convective_internal_gain, - 'occupancy sensible convective internal gain is none') - self.assertIsNotNone(occupancy.sensible_radiative_internal_gain, - 'occupancy sensible radiant internal gain is none') - self.assertIsNotNone(occupancy.occupancy_schedules, 'occupancy schedule is none') + self.assertIsNone(occupancy.latent_internal_gain, 'occupancy latent internal gain is none') + self.assertIsNone(occupancy.sensible_convective_internal_gain, + 'occupancy sensible convective internal gain is not none') + self.assertIsNone(occupancy.sensible_radiative_internal_gain, + 'occupancy sensible radiant internal gain is not none') + self.assertIsNone(occupancy.occupancy_schedules, 'occupancy schedule is not none') self.assertIsNone(occupancy.occupants, 'occupancy density is not none') - self.assertIsNotNone(usage_zone.lighting, 'lighting is none') - lighting = usage_zone.lighting - self.assertIsNotNone(lighting.lighting_density, 'lighting density is none') - self.assertIsNotNone(lighting.latent_fraction, 'lighting latent fraction is none') - self.assertIsNotNone(lighting.convective_fraction, 'lighting convective fraction is none') - self.assertIsNotNone(lighting.radiative_fraction, 'lighting radiant fraction is none') - self.assertIsNotNone(lighting.schedules, 'lighting schedule is none') - self.assertIsNotNone(usage_zone.appliances, 'appliances is none') - appliances = usage_zone.appliances - self.assertIsNotNone(appliances.appliances_density, 'appliances density is none') - self.assertIsNotNone(appliances.latent_fraction, 'appliances latent fraction is none') - self.assertIsNotNone(appliances.convective_fraction, 'appliances convective fraction is none') - self.assertIsNotNone(appliances.radiative_fraction, 'appliances radiant fraction is none') - self.assertIsNotNone(appliances.schedules, 'appliances schedule is none') + self.assertIsNone(usage_zone.lighting, 'lighting is not none') + self.assertIsNone(usage_zone.appliances, 'appliances is not none') + From 03141b74c8fe5a554f2223bc97c96a7d3b0d8158 Mon Sep 17 00:00:00 2001 From: guille Date: Thu, 24 Mar 2022 18:39:46 -0400 Subject: [PATCH 07/19] Remove useless prints --- city_model_structure/lca_calculations.py | 5 +---- imports/schedules/doe_idf.py | 2 -- imports/sensors/concordia_energy_consumption.py | 1 - 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/city_model_structure/lca_calculations.py b/city_model_structure/lca_calculations.py index 032edcd6..fd8b30be 100644 --- a/city_model_structure/lca_calculations.py +++ b/city_model_structure/lca_calculations.py @@ -5,14 +5,11 @@ Copyright © 2020 Project Author Atiya """ from city_model_structure.machine import Machine + class LcaCalculations: """ LCA Calculations class """ - - def __init__(self): - print("lca calculations class") - def emission_disposal_machines(self, ): return Machine.work_efficiency * Machine.energy_consumption_rate * Machine.carbon_emission_factor diff --git a/imports/schedules/doe_idf.py b/imports/schedules/doe_idf.py index d8f88f4f..1c9627de 100644 --- a/imports/schedules/doe_idf.py +++ b/imports/schedules/doe_idf.py @@ -47,7 +47,6 @@ class DoeIdf: self._city = city path = str(base_path / doe_idf_file) - print(path) with open(path) as xml: self._schedule_library = xmltodict.parse(xml.read()) @@ -107,7 +106,6 @@ class DoeIdf: for entry in schedules_day[f'{days_schedules[day_index]}']: values.append(entry) - print(values) schedule.values = values if 'Weekdays' in days_schedules[day_index]: diff --git a/imports/sensors/concordia_energy_consumption.py b/imports/sensors/concordia_energy_consumption.py index e9b233ad..686a4dd3 100644 --- a/imports/sensors/concordia_energy_consumption.py +++ b/imports/sensors/concordia_energy_consumption.py @@ -22,7 +22,6 @@ class ConcordiaEnergyConsumption(ConcordiaFileReport): building_measures = [self._measures["Date time"], self._measures[self._sensor_point[self._sensors[i]]]] building_headers = ["Date time", "Energy consumption"] building_energy_consumption = pd.concat(building_measures, keys=building_headers, axis=1) - print(building_energy_consumption) """ sensor = ConcordiaEnergySensor(self._sensors[i]) From cf19f6fce5ffea529851bed00c4a77e81aab6732 Mon Sep 17 00:00:00 2001 From: Pilar Date: Tue, 8 Mar 2022 19:19:52 -0500 Subject: [PATCH 08/19] fixed bug saving values --- .../ecore_greenery/greenerycatalog.ecore | 268 ------ .../ecore_greenery/greenerycatalog.py | 318 ------- .../greenerycatalog_no_quantities.ecore | 268 ------ city_model_structure/attributes/polygon.py | 49 +- city_model_structure/attributes/polyhedron.py | 2 +- city_model_structure/building.py | 112 ++- .../building_demand/appliances.py | 103 +++ .../building_demand/internal_gains.py | 18 + .../building_demand/lighting.py | 103 +++ .../building_demand/material.py | 164 +++- .../building_demand/occupancy.py | 121 +++ .../{occupants.py => occupant.py} | 53 +- .../building_demand/storey.py | 23 +- .../building_demand/surface.py | 3 + .../building_demand/thermal_boundary.py | 140 ++-- .../building_demand/thermal_control.py | 86 ++ .../building_demand/thermal_opening.py | 15 +- .../building_demand/thermal_zone.py | 110 +-- .../building_demand/usage_zone.py | 290 ++----- city_model_structure/city_object.py | 10 +- .../energy_systems/hvac_system.py | 32 + .../iot/concordia_energy_sensor.py | 45 + .../iot/concordia_gas_flow_sensor.py | 45 + .../iot/concordia_temperature_sensor.py | 45 + city_model_structure/iot/sensor.py | 81 +- city_model_structure/iot/sensor_measure.py | 40 - city_model_structure/iot/sensor_type.py | 20 - city_model_structure/iot/station.py | 41 - city_model_structure/lca_material.py | 242 ------ config/configuration.ini | 2 + .../construction/ca_constructions_reduced.xml | 32 - data/construction/us_constructions.xml | 38 +- data/greenery/ecore_greenery_catalog.xml | 59 -- data/life_cycle_assessment/lca_data.xml | 778 +++++++++--------- exports/formats/energy_ade.py | 2 +- helpers/configuration_helper.py | 16 + helpers/constants.py | 15 +- helpers/monthly_to_hourly_demand.py | 6 +- imports/construction/ca_physics_parameters.py | 19 +- ...lding_achetype.py => building_achetype.py} | 10 +- ..._layer_archetype.py => layer_archetype.py} | 38 +- .../nrel_thermal_boundary_archetype.py | 99 --- .../thermal_boundary_archetype.py | 136 +++ ...hetype.py => thermal_opening_archetype.py} | 70 +- .../helpers/storeys_generation.py | 15 +- .../construction/nrel_physics_interface.py | 25 +- imports/construction/us_physics_parameters.py | 24 +- .../sanam_customized_usage_parameters.py | 2 +- imports/geometry/citygml.py | 2 +- imports/geometry/obj.py | 2 +- imports/geometry/rhino.py | 27 +- imports/life_cycle_assessment/lca_machine.py | 14 +- imports/life_cycle_assessment/lca_material.py | 35 +- .../sensors/concordia_energy_consumption.py | 1 + imports/sensors/concordia_gas_flow.py | 4 +- imports/sensors/concordia_temperature.py | 3 +- imports/usage/ca_usage_parameters.py | 4 +- imports/usage/comnet_usage_parameters.py | 87 +- .../data_classes/hft_usage_zone_archetype.py | 144 ---- .../data_classes/usage_zone_archetype.py | 98 +++ imports/usage/hft_usage_interface.py | 4 +- imports/usage/hft_usage_parameters.py | 4 +- requirements.txt | 2 +- unittests/test_construction_factory.py | 237 ++++-- unittests/test_geometry_factory.py | 245 ++---- .../test_life_cycle_assessment_factory.py | 6 +- unittests/test_schedules_factory.py | 1 + unittests/test_sensors_factory.py | 4 +- unittests/test_usage_factory.py | 122 ++- 69 files changed, 2406 insertions(+), 2873 deletions(-) delete mode 100644 catalogs/greenery/ecore_greenery/greenerycatalog.ecore delete mode 100644 catalogs/greenery/ecore_greenery/greenerycatalog.py delete mode 100644 catalogs/greenery/ecore_greenery/greenerycatalog_no_quantities.ecore create mode 100644 city_model_structure/building_demand/appliances.py create mode 100644 city_model_structure/building_demand/lighting.py create mode 100644 city_model_structure/building_demand/occupancy.py rename city_model_structure/building_demand/{occupants.py => occupant.py} (79%) create mode 100644 city_model_structure/building_demand/thermal_control.py create mode 100644 city_model_structure/energy_systems/hvac_system.py create mode 100644 city_model_structure/iot/concordia_energy_sensor.py create mode 100644 city_model_structure/iot/concordia_gas_flow_sensor.py create mode 100644 city_model_structure/iot/concordia_temperature_sensor.py delete mode 100644 city_model_structure/iot/sensor_measure.py delete mode 100644 city_model_structure/iot/sensor_type.py delete mode 100644 city_model_structure/iot/station.py delete mode 100644 city_model_structure/lca_material.py delete mode 100644 data/greenery/ecore_greenery_catalog.xml rename imports/construction/data_classes/{nrel_building_achetype.py => building_achetype.py} (89%) rename imports/construction/data_classes/{nrel_layer_archetype.py => layer_archetype.py} (67%) delete mode 100644 imports/construction/data_classes/nrel_thermal_boundary_archetype.py create mode 100644 imports/construction/data_classes/thermal_boundary_archetype.py rename imports/construction/data_classes/{nrel_thermal_opening_archetype.py => thermal_opening_archetype.py} (56%) delete mode 100644 imports/usage/data_classes/hft_usage_zone_archetype.py create mode 100644 imports/usage/data_classes/usage_zone_archetype.py diff --git a/catalogs/greenery/ecore_greenery/greenerycatalog.ecore b/catalogs/greenery/ecore_greenery/greenerycatalog.ecore deleted file mode 100644 index 5c3bb85d..00000000 --- a/catalogs/greenery/ecore_greenery/greenerycatalog.ecore +++ /dev/null @@ -1,268 +0,0 @@ - - - - -

- - - - -
- - - - -
- - -
-
- - - - -
- - -
-
- - - - -
- - -
-
- - - - -
- - -
-
- - - - -
- - -
-
- - - - -
- - -
-
- - - - -
- - -
-
- - - - -
- - -
-
- - - - -
- - -
-
- - - - - - - -
- - -
-
- - - - -
- - -
-
- - - - -
- - -
-
- - - - -
- - -
-
- - - - -
- - -
-
- - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - -
- - - - -
-
- - - - - - - - - -
- - - - - - - - - - - - - -
-
- - - - - - - - - - - - - diff --git a/catalogs/greenery/ecore_greenery/greenerycatalog.py b/catalogs/greenery/ecore_greenery/greenerycatalog.py deleted file mode 100644 index 30ac116c..00000000 --- a/catalogs/greenery/ecore_greenery/greenerycatalog.py +++ /dev/null @@ -1,318 +0,0 @@ -"""Definition of meta model 'greenerycatalog'.""" -from functools import partial -import pyecore.ecore as Ecore -from pyecore.ecore import * - - -name = 'greenerycatalog' -nsURI = 'http://ca.concordia/greenerycatalog' -nsPrefix = 'greenery' - -eClass = EPackage(name=name, nsURI=nsURI, nsPrefix=nsPrefix) - -eClassifiers = {} -getEClassifier = partial(Ecore.getEClassifier, searchspace=eClassifiers) -Management = EEnum('Management', literals=['Intensive', 'Extensive', 'SemiIntensive', 'NA']) - -Roughness = EEnum('Roughness', literals=['VeryRough', 'Rough', - 'MediumRough', 'MediumSmooth', 'Smooth', 'VerySmooth']) - - -class Soil(EObject, metaclass=MetaEClass): - - name = EAttribute(eType=EString, unique=True, derived=False, changeable=True) - roughness = EAttribute(eType=Roughness, unique=True, derived=False, - changeable=True, default_value=Roughness.MediumRough) - conductivityOfDrySoil = EAttribute( - eType=EString, unique=True, derived=False, changeable=True, default_value='1.0 W/(m*K)') - densityOfDrySoil = EAttribute(eType=EString, unique=True, derived=False, - changeable=True, default_value='1100 kg/m³') - specificHeatOfDrySoil = EAttribute( - eType=EString, unique=True, derived=False, changeable=True, default_value='1200 J/(kg*K)') - thermalAbsorptance = EAttribute(eType=EString, unique=True, - derived=False, changeable=True, default_value='0.9') - solarAbsorptance = EAttribute(eType=EString, unique=True, - derived=False, changeable=True, default_value='0.7') - visibleAbsorptance = EAttribute(eType=EString, unique=True, - derived=False, changeable=True, default_value='0.75') - saturationVolumetricMoistureContent = EAttribute( - eType=EString, unique=True, derived=False, changeable=True, default_value='0.0') - residualVolumetricMoistureContent = EAttribute( - eType=EString, unique=True, derived=False, changeable=True, default_value='0.05') - initialVolumetricMoistureContent = EAttribute( - eType=EString, unique=True, derived=False, changeable=True, default_value='0.1') - - def __init__(self, *, name=None, roughness=None, conductivityOfDrySoil=None, densityOfDrySoil=None, specificHeatOfDrySoil=None, thermalAbsorptance=None, solarAbsorptance=None, visibleAbsorptance=None, saturationVolumetricMoistureContent=None, residualVolumetricMoistureContent=None, initialVolumetricMoistureContent=None): - # if kwargs: - # raise AttributeError('unexpected arguments: {}'.format(kwargs)) - - super().__init__() - - if name is not None: - self.name = name - - if roughness is not None: - self.roughness = roughness - - if conductivityOfDrySoil is not None: - self.conductivityOfDrySoil = conductivityOfDrySoil - - if densityOfDrySoil is not None: - self.densityOfDrySoil = densityOfDrySoil - - if specificHeatOfDrySoil is not None: - self.specificHeatOfDrySoil = specificHeatOfDrySoil - - if thermalAbsorptance is not None: - self.thermalAbsorptance = thermalAbsorptance - - if solarAbsorptance is not None: - self.solarAbsorptance = solarAbsorptance - - if visibleAbsorptance is not None: - self.visibleAbsorptance = visibleAbsorptance - - if saturationVolumetricMoistureContent is not None: - self.saturationVolumetricMoistureContent = saturationVolumetricMoistureContent - - if residualVolumetricMoistureContent is not None: - self.residualVolumetricMoistureContent = residualVolumetricMoistureContent - - if initialVolumetricMoistureContent is not None: - self.initialVolumetricMoistureContent = initialVolumetricMoistureContent - - -class Plant(EObject, metaclass=MetaEClass): - - name = EAttribute(eType=EString, unique=True, derived=False, changeable=True) - height = EAttribute(eType=EString, unique=True, derived=False, - changeable=True, default_value='0.1 m') - leafAreaIndex = EAttribute(eType=EString, unique=True, derived=False, - changeable=True, default_value='2.5') - leafReflectivity = EAttribute(eType=EString, unique=True, - derived=False, changeable=True, default_value='0.1') - leafEmissivity = EAttribute(eType=EString, unique=True, derived=False, - changeable=True, default_value='0.9') - minimalStomatalResistance = EAttribute( - eType=EString, unique=True, derived=False, changeable=True, default_value='100.0 s/m') - co2Sequestration = EAttribute(eType=EString, unique=True, derived=False, - changeable=True, default_value='kgCO₂eq') - growsOn = EReference(ordered=True, unique=True, containment=False, derived=False, upper=-1) - - def __init__(self, *, name=None, height=None, leafAreaIndex=None, leafReflectivity=None, leafEmissivity=None, minimalStomatalResistance=None, growsOn=None, co2Sequestration=None): - # if kwargs: - # raise AttributeError('unexpected arguments: {}'.format(kwargs)) - - super().__init__() - - if name is not None: - self.name = name - - if height is not None: - self.height = height - - if leafAreaIndex is not None: - self.leafAreaIndex = leafAreaIndex - - if leafReflectivity is not None: - self.leafReflectivity = leafReflectivity - - if leafEmissivity is not None: - self.leafEmissivity = leafEmissivity - - if minimalStomatalResistance is not None: - self.minimalStomatalResistance = minimalStomatalResistance - - if co2Sequestration is not None: - self.co2Sequestration = co2Sequestration - - if growsOn: - self.growsOn.extend(growsOn) - - -class SupportEnvelope(EObject, metaclass=MetaEClass): - - roughness = EAttribute(eType=Roughness, unique=True, derived=False, - changeable=True, default_value=Roughness.MediumRough) - solarAbsorptance = EAttribute(eType=EDouble, unique=True, - derived=False, changeable=True, default_value=0.0) - conductivity = EAttribute(eType=EDouble, unique=True, derived=False, - changeable=True, default_value=0.0) - visibleAbsorptance = EAttribute(eType=EDouble, unique=True, - derived=False, changeable=True, default_value=0.0) - specificHeat = EAttribute(eType=EDouble, unique=True, derived=False, - changeable=True, default_value=0.0) - density = EAttribute(eType=EDouble, unique=True, derived=False, - changeable=True, default_value=0.0) - thermalAbsorptance = EAttribute(eType=EDouble, unique=True, - derived=False, changeable=True, default_value=0.0) - - def __init__(self, *, roughness=None, solarAbsorptance=None, conductivity=None, visibleAbsorptance=None, specificHeat=None, density=None, thermalAbsorptance=None): - # if kwargs: - # raise AttributeError('unexpected arguments: {}'.format(kwargs)) - - super().__init__() - - if roughness is not None: - self.roughness = roughness - - if solarAbsorptance is not None: - self.solarAbsorptance = solarAbsorptance - - if conductivity is not None: - self.conductivity = conductivity - - if visibleAbsorptance is not None: - self.visibleAbsorptance = visibleAbsorptance - - if specificHeat is not None: - self.specificHeat = specificHeat - - if density is not None: - self.density = density - - if thermalAbsorptance is not None: - self.thermalAbsorptance = thermalAbsorptance - - -class GreeneryCatalog(EObject, metaclass=MetaEClass): - - name = EAttribute(eType=EString, unique=True, derived=False, changeable=True) - description = EAttribute(eType=EString, unique=True, derived=False, changeable=True) - source = EAttribute(eType=EString, unique=True, derived=False, changeable=True) - plantCategories = EReference(ordered=True, unique=True, - containment=True, derived=False, upper=-1) - vegetationCategories = EReference(ordered=True, unique=True, - containment=True, derived=False, upper=-1) - soils = EReference(ordered=True, unique=True, containment=True, derived=False, upper=-1) - - def __init__(self, *, name=None, description=None, source=None, plantCategories=None, vegetationCategories=None, soils=None): - # if kwargs: - # raise AttributeError('unexpected arguments: {}'.format(kwargs)) - - super().__init__() - - if name is not None: - self.name = name - - if description is not None: - self.description = description - - if source is not None: - self.source = source - - if plantCategories: - self.plantCategories.extend(plantCategories) - - if vegetationCategories: - self.vegetationCategories.extend(vegetationCategories) - - if soils: - self.soils.extend(soils) - - -class PlantCategory(EObject, metaclass=MetaEClass): - """Excluding (that is non-overlapping) categories like Trees, Hedeges, Grasses that help users finding a specific biol. plant species.""" - name = EAttribute(eType=EString, unique=True, derived=False, changeable=True) - plants = EReference(ordered=True, unique=True, containment=True, derived=False, upper=-1) - - def __init__(self, *, name=None, plants=None): - # if kwargs: - # raise AttributeError('unexpected arguments: {}'.format(kwargs)) - - super().__init__() - - if name is not None: - self.name = name - - if plants: - self.plants.extend(plants) - - -class IrrigationSchedule(EObject, metaclass=MetaEClass): - - name = EAttribute(eType=EString, unique=True, derived=False, changeable=True) - - def __init__(self, *, name=None): - # if kwargs: - # raise AttributeError('unexpected arguments: {}'.format(kwargs)) - - super().__init__() - - if name is not None: - self.name = name - - -class Vegetation(EObject, metaclass=MetaEClass): - """Plant life or total plant cover (as of an area)""" - name = EAttribute(eType=EString, unique=True, derived=False, changeable=True) - thicknessOfSoil = EAttribute(eType=EString, unique=True, derived=False, - changeable=True, default_value='20 cm') - management = EAttribute(eType=Management, unique=True, derived=False, - changeable=True, default_value=Management.NA) - airGap = EAttribute(eType=EString, unique=True, derived=False, - changeable=True, default_value='0.0 cm') - soil = EReference(ordered=True, unique=True, containment=False, derived=False) - plants = EReference(ordered=True, unique=True, containment=True, derived=False, upper=-1) - - def __init__(self, *, name=None, thicknessOfSoil=None, soil=None, plants=None, management=None, airGap=None): - # if kwargs: - # raise AttributeError('unexpected arguments: {}'.format(kwargs)) - - super().__init__() - - if name is not None: - self.name = name - - if thicknessOfSoil is not None: - self.thicknessOfSoil = thicknessOfSoil - - if management is not None: - self.management = management - - if airGap is not None: - self.airGap = airGap - - if soil is not None: - self.soil = soil - - if plants: - self.plants.extend(plants) - - -class VegetationCategory(EObject, metaclass=MetaEClass): - """Excluding (that is non-overlapping) categories to help users finding a specific vegetation template.""" - name = EAttribute(eType=EString, unique=True, derived=False, changeable=True) - vegetationTemplates = EReference(ordered=True, unique=True, - containment=True, derived=False, upper=-1) - - def __init__(self, *, vegetationTemplates=None, name=None): - # if kwargs: - # raise AttributeError('unexpected arguments: {}'.format(kwargs)) - - super().__init__() - - if name is not None: - self.name = name - - if vegetationTemplates: - self.vegetationTemplates.extend(vegetationTemplates) - - -class PlantPercentage(EObject, metaclass=MetaEClass): - - percentage = EAttribute(eType=EString, unique=True, derived=False, - changeable=True, default_value='100') - plant = EReference(ordered=True, unique=True, containment=False, derived=False) - - def __init__(self, *, percentage=None, plant=None): - # if kwargs: - # raise AttributeError('unexpected arguments: {}'.format(kwargs)) - - super().__init__() - - if percentage is not None: - self.percentage = percentage - - if plant is not None: - self.plant = plant diff --git a/catalogs/greenery/ecore_greenery/greenerycatalog_no_quantities.ecore b/catalogs/greenery/ecore_greenery/greenerycatalog_no_quantities.ecore deleted file mode 100644 index db58a9c0..00000000 --- a/catalogs/greenery/ecore_greenery/greenerycatalog_no_quantities.ecore +++ /dev/null @@ -1,268 +0,0 @@ - - - - -
- - - - -
- - - - -
- - -
-
- - - - -
- - -
-
- - - - -
- - -
-
- - - - -
- - -
-
- - - - -
- - -
-
- - - - -
- - -
-
- - - - -
- - -
-
- - - - -
- - -
-
- - - - -
- - -
-
- - - - - - - -
- - -
-
- - - - -
- - -
-
- - - - -
- - -
-
- - - - -
- - -
-
- - - - -
- - -
-
- - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - -
- - - - -
-
- - - - - - - - - -
- - - - - - - - - - - - - -
-
- - - - - - - - - - - - - diff --git a/city_model_structure/attributes/polygon.py b/city_model_structure/attributes/polygon.py index cdfd56da..126e9ae4 100644 --- a/city_model_structure/attributes/polygon.py +++ b/city_model_structure/attributes/polygon.py @@ -16,6 +16,7 @@ from city_model_structure.attributes.plane import Plane from city_model_structure.attributes.point import Point import helpers.constants as cte + class Polygon: """ Polygon class @@ -78,6 +79,48 @@ class Polygon: z = vector_0[2] * vector_1[2] return x+y+z + def contains_point(self, point): + """ + Determines if the given point is contained by the current polygon + :return: boolean + """ + # fixme: This method doesn't seems to work. + n = len(self.vertices) + angle_sum = 0 + for i in range(0, n): + vector_0 = self.vertices[i] + vector_1 = self.vertices[(i+1) % n] + # set to origin + vector_0[0] = vector_0[0] - point.coordinates[0] + vector_0[1] = vector_0[1] - point.coordinates[1] + vector_0[2] = vector_0[2] - point.coordinates[2] + vector_1[0] = vector_1[0] - point.coordinates[0] + vector_1[1] = vector_1[1] - point.coordinates[1] + vector_1[2] = vector_1[2] - point.coordinates[2] + module = Polygon._module(vector_0) * Polygon._module(vector_1) + + scalar_product = Polygon._scalar_product(vector_0, vector_1) + angle = np.pi/2 + if module != 0: + angle = abs(np.arcsin(scalar_product / module)) + angle_sum += angle + print(angle_sum) + return abs(angle_sum - math.pi*2) < cte.EPSILON + + def contains_polygon(self, polygon): + """ + Determines if the given polygon is contained by the current polygon + :return: boolean + """ + print('contains') + for point in polygon.points: + print(point.coordinates, self.contains_point(point)) + + if not self.contains_point(point): + return False + print('Belong!') + return True + @property def points_list(self) -> np.ndarray: """ @@ -249,7 +292,7 @@ class Polygon: points_list = self.points_list normal = self.normal if np.linalg.norm(normal) == 0: - sys.stderr.write(f'Not able to triangulate polygon [normal length is 0]') + sys.stderr.write('Not able to triangulate polygon\n') return [self] # are points concave or convex? total_points_list, concave_points, convex_points = self._starting_lists(points_list, normal) @@ -297,10 +340,10 @@ class Polygon: continue break if len(total_points_list) <= 3 and len(convex_points) > 0: - sys.stderr.write('Not able to triangulate polygon [convex surface]\n') + sys.stderr.write('Not able to triangulate polygon\n') return [self] if j >= 100: - sys.stderr.write('Not able to triangulate polygon [infinite loop]\n') + sys.stderr.write('Not able to triangulate polygon\n') return [self] last_ear = self._triangle(points_list, total_points_list, concave_points[1]) ears.append(last_ear) diff --git a/city_model_structure/attributes/polyhedron.py b/city_model_structure/attributes/polyhedron.py index 44a1f8bc..3f292f79 100644 --- a/city_model_structure/attributes/polyhedron.py +++ b/city_model_structure/attributes/polyhedron.py @@ -114,7 +114,7 @@ class Polyhedron: if self._trimesh is None: for face in self.faces: if len(face) != 3: - sys.stderr.write(f'Not able to generate trimesh the face has {len(face)} vertices\n') + sys.stderr.write('Not able to generate trimesh\n') return None self._trimesh = Trimesh(vertices=self.vertices, faces=self.faces) return self._trimesh diff --git a/city_model_structure/building.py b/city_model_structure/building.py index 951eab05..f7d2dc16 100644 --- a/city_model_structure/building.py +++ b/city_model_structure/building.py @@ -5,17 +5,17 @@ Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@conc contributors: Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ -from typing import List, Union +from typing import List, Union, TypeVar import numpy as np from city_model_structure.building_demand.surface import Surface -from city_model_structure.building_demand.thermal_zone import ThermalZone -from city_model_structure.building_demand.thermal_boundary import ThermalBoundary from city_model_structure.building_demand.usage_zone import UsageZone from city_model_structure.building_demand.storey import Storey from city_model_structure.city_object import CityObject from city_model_structure.building_demand.household import Household from city_model_structure.attributes.polyhedron import Polyhedron +ThermalZone = TypeVar('ThermalZone') + class Building(CityObject): """ @@ -35,9 +35,8 @@ class Building(CityObject): self._roof_type = None self._storeys = None self._geometrical_zones = None - self._thermal_zones = [] - self._thermal_boundaries = None - self._usage_zones = [] + self._thermal_zones = None + self._usage_zones = None self._type = 'building' self._heating = dict() self._cooling = dict() @@ -78,28 +77,6 @@ class Building(CityObject): """ return self._grounds - @property - def is_heated(self): - """ - Get building heated flag - :return: Boolean - """ - for usage_zone in self.usage_zones: - if usage_zone.is_heated: - return usage_zone.is_heated - return False - - @property - def is_cooled(self): - """ - Get building cooled flag - :return: Boolean - """ - for usage_zone in self.usage_zones: - if usage_zone.is_cooled: - return usage_zone.is_cooled - return False - @property def roofs(self) -> List[Surface]: """ @@ -117,18 +94,23 @@ class Building(CityObject): return self._walls @property - def usage_zones(self) -> List[UsageZone]: + def usage_zones(self) -> Union[None, List[UsageZone]]: """ Get city object usage zones :return: [UsageZone] """ - if len(self._usage_zones) == 0: - for thermal_zone in self.thermal_zones: - self._usage_zones.extend(thermal_zone.usage_zones) return self._usage_zones + @usage_zones.setter + def usage_zones(self, value): + """ + Set city object usage zones + :param value: [UsageZone] + """ + self._usage_zones = value + @property - def terrains(self) -> List[Surface]: + def terrains(self) -> Union[None, List[Surface]]: """ Get city object terrain surfaces :return: [Surface] @@ -170,26 +152,21 @@ class Building(CityObject): self._basement_heated = int(value) @property - def name(self): - """ - Get building name - :return: str - """ - return self._name - - @property - def thermal_zones(self) -> List[ThermalZone]: + def thermal_zones(self) -> Union[None, List[ThermalZone]]: """ Get building thermal zones :return: [ThermalZone] """ - if len(self._thermal_zones) == 0: - if self.storeys is None: - return [] - for storey in self.storeys: - self._thermal_zones.append(storey.thermal_zone) return self._thermal_zones + @thermal_zones.setter + def thermal_zones(self, value): + """ + Set city object thermal zones + :param value: [ThermalZone] + """ + self._thermal_zones = value + @property def heated_volume(self): """ @@ -213,7 +190,7 @@ class Building(CityObject): :param value: int """ if value is not None: - self._year_of_construction = value + self._year_of_construction = int(value) @property def function(self) -> Union[None, str]: @@ -341,6 +318,14 @@ class Building(CityObject): break return self._roof_type + @roof_type.setter + def roof_type(self, value): + """ + Set roof type for the building flat or pitch + :return: str + """ + self._roof_type = value + @property def floor_area(self): """ @@ -354,24 +339,6 @@ class Building(CityObject): self._floor_area += surface.perimeter_polygon.area return self._floor_area - @property - def thermal_boundaries(self) -> List[ThermalBoundary]: - """ - Get all thermal boundaries associated to the building's thermal zones - :return: [ThermalBoundary] - """ - if self._thermal_boundaries is None: - self._thermal_boundaries = [] - for thermal_zone in self.thermal_zones: - _thermal_boundary_duplicated = False - for thermal_boundary in thermal_zone.thermal_boundaries: - if len(thermal_boundary.thermal_zones) > 1: - if thermal_zone != thermal_boundary.thermal_zones[1]: - self._thermal_boundaries.append(thermal_boundary) - else: - self._thermal_boundaries.append(thermal_boundary) - return self._thermal_boundaries - @property def households(self) -> List[Household]: """ @@ -379,3 +346,16 @@ class Building(CityObject): :return: List[Household] """ return self._households + + @property + def is_conditioned(self): + """ + Get building heated flag + :return: Boolean + """ + if self.thermal_zones is None: + return False + for thermal_zone in self.thermal_zones: + if thermal_zone.hvac_system is not None: + return True + return False diff --git a/city_model_structure/building_demand/appliances.py b/city_model_structure/building_demand/appliances.py new file mode 100644 index 00000000..ed09f795 --- /dev/null +++ b/city_model_structure/building_demand/appliances.py @@ -0,0 +1,103 @@ +""" +Appliances module +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2022 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +""" +from typing import Union +from city_model_structure.attributes.schedule import Schedule + + +class Appliances: + """ + Appliances class + """ + def __init__(self): + self._lighting_density = None + self._convective_fraction = None + self._radiant_fraction = None + self._latent_fraction = None + self._schedule = None + + @property + def lighting_density(self) -> Union[None, float]: + """ + Get lighting density in Watts per m2 + :return: None or float + """ + return self._lighting_density + + @lighting_density.setter + def lighting_density(self, value): + """ + Set lighting density in Watts per m2 + :param value: float + """ + if value is not None: + self._lighting_density = float(value) + + @property + def convective_fraction(self) -> Union[None, float]: + """ + Get convective fraction + :return: None or float + """ + return self._convective_fraction + + @convective_fraction.setter + def convective_fraction(self, value): + """ + Set convective fraction + :param value: float + """ + if value is not None: + self._convective_fraction = float(value) + + @property + def radiant_fraction(self) -> Union[None, float]: + """ + Get radiant fraction + :return: None or float + """ + return self._radiant_fraction + + @radiant_fraction.setter + def radiant_fraction(self, value): + """ + Set radiant fraction + :param value: float + """ + if value is not None: + self._radiant_fraction = float(value) + + @property + def latent_fraction(self) -> Union[None, float]: + """ + Get latent fraction + :return: None or float + """ + return self._latent_fraction + + @latent_fraction.setter + def latent_fraction(self, value): + """ + Set latent fraction + :param value: float + """ + if value is not None: + self._latent_fraction = float(value) + + @property + def schedule(self) -> Union[None, Schedule]: + """ + Get schedule + :return: None or Schedule + """ + return self._schedule + + @schedule.setter + def schedule(self, value): + """ + Set schedule + :param value: Schedule + """ + self._schedule = value diff --git a/city_model_structure/building_demand/internal_gains.py b/city_model_structure/building_demand/internal_gains.py index 75e60cb7..4cfeabb9 100644 --- a/city_model_structure/building_demand/internal_gains.py +++ b/city_model_structure/building_demand/internal_gains.py @@ -5,6 +5,7 @@ Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@conc """ from typing import Union +from city_model_structure.attributes.schedule import Schedule class InternalGains: @@ -18,6 +19,7 @@ class InternalGains: self._convective_fraction = None self._radiative_fraction = None self._latent_fraction = None + self._schedule = None @property def type(self) -> Union[None, str]: @@ -103,3 +105,19 @@ class InternalGains: """ if value is not None: self._latent_fraction = float(value) + + @property + def schedule(self) -> Union[None, Schedule]: + """ + Get internal gain schedule + :return: Schedule + """ + return self._schedule + + @schedule.setter + def schedule(self, value): + """ + Set internal gain schedule + :param value: Schedule + """ + self._schedule = value diff --git a/city_model_structure/building_demand/lighting.py b/city_model_structure/building_demand/lighting.py new file mode 100644 index 00000000..0407a432 --- /dev/null +++ b/city_model_structure/building_demand/lighting.py @@ -0,0 +1,103 @@ +""" +Lighting module +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2022 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +""" +from typing import Union +from city_model_structure.attributes.schedule import Schedule + + +class Lighting: + """ + Lighting class + """ + def __init__(self): + self._lighting_density = None + self._convective_fraction = None + self._radiant_fraction = None + self._latent_fraction = None + self._schedule = None + + @property + def lighting_density(self) -> Union[None, float]: + """ + Get lighting density in Watts per m2 + :return: None or float + """ + return self._lighting_density + + @lighting_density.setter + def lighting_density(self, value): + """ + Set lighting density in Watts per m2 + :param value: float + """ + if value is not None: + self._lighting_density = float(value) + + @property + def convective_fraction(self) -> Union[None, float]: + """ + Get convective fraction + :return: None or float + """ + return self._convective_fraction + + @convective_fraction.setter + def convective_fraction(self, value): + """ + Set convective fraction + :param value: float + """ + if value is not None: + self._convective_fraction = float(value) + + @property + def radiant_fraction(self) -> Union[None, float]: + """ + Get radiant fraction + :return: None or float + """ + return self._radiant_fraction + + @radiant_fraction.setter + def radiant_fraction(self, value): + """ + Set radiant fraction + :param value: float + """ + if value is not None: + self._radiant_fraction = float(value) + + @property + def latent_fraction(self) -> Union[None, float]: + """ + Get latent fraction + :return: None or float + """ + return self._latent_fraction + + @latent_fraction.setter + def latent_fraction(self, value): + """ + Set latent fraction + :param value: float + """ + if value is not None: + self._latent_fraction = float(value) + + @property + def schedule(self) -> Union[None, Schedule]: + """ + Get schedule + :return: None or Schedule + """ + return self._schedule + + @schedule.setter + def schedule(self, value): + """ + Set schedule + :param value: Schedule + """ + self._schedule = value diff --git a/city_model_structure/building_demand/material.py b/city_model_structure/building_demand/material.py index cb015fa7..5762550d 100644 --- a/city_model_structure/building_demand/material.py +++ b/city_model_structure/building_demand/material.py @@ -6,6 +6,7 @@ Contributor Atiya atiya.atiya@mail.concordia.ca Contributor Mohammad Reza mohammad.seyedabadi@mail.concordia.ca """ +import ast from typing import Union @@ -14,6 +15,7 @@ class Material: Material class """ def __init__(self): + self._type = type self._id = None self._name = None self._conductivity = None @@ -25,7 +27,30 @@ class Material: self._visible_absorptance = None self._no_mass = False self._thermal_resistance = None - self._lca_id = None + self._embodied_carbon = None + self._embodied_carbon_unit = None + self._recycling_ratio = None + self._onsite_recycling_ratio = None + self._company_recycling_ratio = None + self._landfilling_ratio = None + self._cost = None + self._cost_unit = None + + @property + def type(self): + """ + Get material type + :return: str + """ + return self._type + + @type.setter + def type(self, value): + """ + Set material type + :param value: string + """ + self._type = str(value) @property def id(self): @@ -213,10 +238,137 @@ class Material: self._thermal_resistance = float(value) @property - def lca_id(self): - return self._lca_id + def embodied_carbon(self) -> Union[None, float]: + """ + Get material embodied carbon + :return: None or float + """ + return self._embodied_carbon - @lca_id.setter - def lca_id(self, value): - self._lca_id = value + @embodied_carbon.setter + def embodied_carbon(self, value): + """ + Set material embodied carbon + :param value: float + """ + if value is not None: + self._embodied_carbon = float(value) + @property + def embodied_carbon_unit(self) -> Union[None, str]: + """ + Get material embodied carbon unit + :return: None or string + """ + return self._embodied_carbon + + @embodied_carbon_unit.setter + def embodied_carbon_unit(self, value): + """ + Set material embodied carbon unit + :param value: string + """ + if value is not None: + self._embodied_carbon_unit = str(value) + + @property + def recycling_ratio(self) -> Union[None, float]: + """ + Get material recycling ratio + :return: None or float + """ + return self._recycling_ratio + + @recycling_ratio.setter + def recycling_ratio(self, value): + """ + Set material recycling ratio + :param value: float + """ + if value is not None: + self._recycling_ratio = float(value) + + @property + def onsite_recycling_ratio(self) -> Union[None, float]: + """ + Get material onsite recycling ratio + :return: None or float + """ + return self._onsite_recycling_ratio + + @onsite_recycling_ratio.setter + def onsite_recycling_ratio(self, value): + """ + Set material onsite recycling ratio + :param value: float + """ + if value is not None: + self._onsite_recycling_ratio = float(value) + + @property + def company_recycling_ratio(self) -> Union[None, float]: + """ + Get material company recycling ratio + :return: None or float + """ + return self._company_recycling_ratio + + @company_recycling_ratio.setter + def company_recycling_ratio(self, value): + """ + Set material company recycling ratio + :param value: float + """ + if value is not None: + self._company_recycling_ratio = float(value) + + @property + def landfilling_ratio(self) -> Union[None, float]: + """ + Get material landfilling ratio + :return: None or float + """ + return self._landfilling_ratio + + @landfilling_ratio.setter + def landfilling_ratio(self, value): + """ + Set material landfilling ratio + :param value: float + """ + if value is not None: + self._landfilling_ratio = float(value) + + @property + def cost(self) -> Union[None, float]: + """ + Get material cost + :return: None or float + """ + return self._cost + + @cost.setter + def cost(self, value): + """ + Set material cost + :param value: float + """ + if value is not None: + self._cost = float(value) + + @property + def cost_unit(self) -> Union[None, str]: + """ + Get material cost unit + :return: None or string + """ + return self._cost_unit + + @cost_unit.setter + def cost_unit(self, value): + """ + Set material cost unit + :param value: string + """ + if value is not None: + self._cost_unit = float(value) diff --git a/city_model_structure/building_demand/occupancy.py b/city_model_structure/building_demand/occupancy.py new file mode 100644 index 00000000..443030ca --- /dev/null +++ b/city_model_structure/building_demand/occupancy.py @@ -0,0 +1,121 @@ +""" +Occupancy module +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2022 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +""" +from typing import Union, List +from city_model_structure.attributes.schedule import Schedule +from city_model_structure.building_demand.occupant import Occupant + + +class Occupancy: + """ + Occupancy class + """ + def __init__(self): + self._occupancy_density = None + self._sensible_convective_internal_gain = None + self._sensible_radiant_internal_gain = None + self._latent_internal_gain = None + self._occupancy_schedule = None + self._occupants = None + + @property + def occupancy_density(self) -> Union[None, float]: + """ + Get density in m2 per person + :return: None or float + """ + return self._occupancy_density + + @occupancy_density.setter + def occupancy_density(self, value): + """ + Set density in m2 per persons + :param value: float + """ + if value is not None: + self._occupancy_density = float(value) + + @property + def sensible_convective_internal_gain(self) -> Union[None, float]: + """ + Get sensible convective internal gain in Watts per m2 + :return: None or float + """ + return self._sensible_convective_internal_gain + + @sensible_convective_internal_gain.setter + def sensible_convective_internal_gain(self, value): + """ + Set sensible convective internal gain in Watts per m2 + :param value: float + """ + if value is not None: + self._sensible_convective_internal_gain = float(value) + + @property + def sensible_radiant_internal_gain(self) -> Union[None, float]: + """ + Get sensible radiant internal gain in Watts per m2 + :return: None or float + """ + return self._sensible_radiant_internal_gain + + @sensible_radiant_internal_gain.setter + def sensible_radiant_internal_gain(self, value): + """ + Set sensible radiant internal gain in Watts per m2 + :param value: float + """ + if value is not None: + self._sensible_radiant_internal_gain = float(value) + + @property + def latent_internal_gain(self) -> Union[None, float]: + """ + Get latent internal gain in Watts per m2 + :return: None or float + """ + return self._latent_internal_gain + + @latent_internal_gain.setter + def latent_internal_gain(self, value): + """ + Set latent internal gain in Watts per m2 + :param value: float + """ + if value is not None: + self._latent_internal_gain = float(value) + + @property + def occupancy_schedule(self) -> Union[None, Schedule]: + """ + Get occupancy schedule + :return: None or Schedule + """ + return self._occupancy_schedule + + @occupancy_schedule.setter + def occupancy_schedule(self, value): + """ + Set occupancy schedule + :param value: Schedule + """ + self._occupancy_schedule = value + + @property + def occupants(self) -> Union[None, List[Occupant]]: + """ + Get list of occupants + :return: None or List of Occupant + """ + return self._occupants + + @occupants.setter + def occupants(self, value): + """ + Set list of occupants + :param value: [Occupant] + """ + self._occupants = value diff --git a/city_model_structure/building_demand/occupants.py b/city_model_structure/building_demand/occupant.py similarity index 79% rename from city_model_structure/building_demand/occupants.py rename to city_model_structure/building_demand/occupant.py index 3d7a39b6..8a7dd533 100644 --- a/city_model_structure/building_demand/occupants.py +++ b/city_model_structure/building_demand/occupant.py @@ -1,19 +1,16 @@ """ -Occupants module +Occupant module SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2020 Project Author Sanam Dabirian sanam.dabirian@mail.concordia.ca Contributors Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ -from typing import TypeVar import calendar as cal -UsageZone = TypeVar('UsageZone') - -class Occupants: +class Occupant: """ - Occupants class + Occupant class """ def __init__(self): @@ -24,15 +21,11 @@ class Occupants: self._heat_dissipation = None self._occupancy_rate = None self._occupant_type = None - self._usage_zone = None - self._occupant_schedule = None - self._number_of_occupants = None self._arrival_time = None self._departure_time = None self._break_time = None self._day_of_week = None self._pd_of_meetings_duration = None - self._complete_year_schedule = None @property def heat_dissipation(self): @@ -82,46 +75,6 @@ class Occupants: """ self._occupant_type = float(value) - @property - def usage_zone(self) -> UsageZone: - """ - Get the zone an occupant is in - :return: UsageZone - """ - return self._usage_zone - - @property - def occupant_schedule(self): - """ - Get the schedules when an occupant is in a zone (24 values, 1 per hour of the day) - :return: [float] - """ - return self._occupant_schedule - - @occupant_schedule.setter - def occupant_schedule(self, value): - """ - Set the schedules when an occupant is in a zone (24 values, 1 per hour of the day) - :param value: [float] - """ - self._occupant_schedule = [float(i) for i in value] - - @property - def number_of_occupants(self): - """ - Get the number of occupants - :return: int - """ - return self._number_of_occupants - - @number_of_occupants.setter - def number_of_occupants(self, value): - """ - Set the number of occupants - :param value: int - """ - self._number_of_occupants = int(value) - @property def arrival_time(self): """ diff --git a/city_model_structure/building_demand/storey.py b/city_model_structure/building_demand/storey.py index f7a211d8..143eec29 100644 --- a/city_model_structure/building_demand/storey.py +++ b/city_model_structure/building_demand/storey.py @@ -12,12 +12,10 @@ from city_model_structure.building_demand.thermal_zone import ThermalZone class Storey: - # todo: rethink this class for buildings with windows """ Storey class """ - def __init__(self, name, storey_surfaces, neighbours, volume): - # todo: the information of the parent surface is lost -> need to recover it + def __init__(self, name, storey_surfaces, neighbours, volume, floor_area): self._name = name self._storey_surfaces = storey_surfaces self._thermal_boundaries = None @@ -25,6 +23,7 @@ class Storey: self._thermal_zone = None self._neighbours = neighbours self._volume = volume + self._floor_area = floor_area @property def name(self): @@ -59,7 +58,13 @@ class Storey: if self._thermal_boundaries is None: self._thermal_boundaries = [] for surface in self.surfaces: - self._thermal_boundaries.append(ThermalBoundary(surface)) + if surface.holes_polygons is None: + windows_areas = None + else: + windows_areas = [] + for hole in surface.holes_polygons: + windows_areas.append(hole.area) + self._thermal_boundaries.append(ThermalBoundary(surface, surface.solid_polygon.area, windows_areas)) return self._thermal_boundaries @property @@ -81,7 +86,7 @@ class Storey: :return: ThermalZone """ if self._thermal_zone is None: - self._thermal_zone = ThermalZone(self.thermal_boundaries, self.volume) + self._thermal_zone = ThermalZone(self.thermal_boundaries, self.volume, self.floor_area) return self._thermal_zone @property @@ -91,3 +96,11 @@ class Storey: :return: float """ return self._volume + + @property + def floor_area(self): + """ + Get storey's floor area in square meters + :return: float + """ + return self._floor_area diff --git a/city_model_structure/building_demand/surface.py b/city_model_structure/building_demand/surface.py index 1e2178c8..6c73d268 100644 --- a/city_model_structure/building_demand/surface.py +++ b/city_model_structure/building_demand/surface.py @@ -71,6 +71,7 @@ class Surface: if value is not None: self._id = str(value) + # todo: implement share surfaces @property def share_surfaces(self): """ @@ -191,6 +192,8 @@ class Surface: Get surface type Ground, Wall or Roof :return: str """ + + # todo: there are more types: internal wall, internal floor... this method must be redefined if self._type is None: grad = np.rad2deg(self.inclination) if grad >= 170: diff --git a/city_model_structure/building_demand/thermal_boundary.py b/city_model_structure/building_demand/thermal_boundary.py index 7b444a14..946e7642 100644 --- a/city_model_structure/building_demand/thermal_boundary.py +++ b/city_model_structure/building_demand/thermal_boundary.py @@ -5,25 +5,24 @@ Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@conc Contributors Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ import uuid -from typing import List, TypeVar, Union +from typing import List, Union +from helpers.configuration_helper import ConfigurationHelper as ch from city_model_structure.building_demand.layer import Layer from city_model_structure.building_demand.thermal_opening import ThermalOpening from city_model_structure.building_demand.thermal_zone import ThermalZone from city_model_structure.building_demand.surface import Surface -Polygon = TypeVar('Polygon') - class ThermalBoundary: """ ThermalBoundary class """ - def __init__(self, surface): - self._surface = surface + def __init__(self, parent_surface, opaque_area, windows_areas): + self._parent_surface = parent_surface + self._opaque_area = opaque_area + self._windows_areas = windows_areas self._id = None self._thermal_zones = None - # ToDo: up to at least LOD2 will be just one thermal opening per Thermal boundary only if window_ratio > 0, - # review for LOD3 and LOD4 self._thermal_openings = None self._layers = None self._outside_solar_absorptance = None @@ -32,16 +31,16 @@ class ThermalBoundary: self._u_value = None self._shortwave_reflectance = None self._construction_name = None - self._hi = 3.5 - self._he = 20 - self._window_ratio = None + self._hi = ch().convective_heat_transfer_coefficient_interior + self._he = ch().convective_heat_transfer_coefficient_exterior self._refurbishment_measure = None - self._surface_geometry = None self._thickness = None self._virtual_internal_surface = None self._inside_emissivity = None self._alpha_coefficient = None self._radiative_coefficient = None + self._window_ratio = None + self._calculated = False @property def id(self): @@ -54,13 +53,12 @@ class ThermalBoundary: return self._id @property - def surface(self) -> Surface: + def parent_surface(self) -> Surface: """ Get the surface that belongs to the thermal boundary :return: Surface """ - # todo: in LoD4 this property will be a list of surfaces, not only one - return self._surface + return self._parent_surface @property def thermal_zones(self) -> List[ThermalZone]: @@ -73,47 +71,35 @@ class ThermalBoundary: @thermal_zones.setter def thermal_zones(self, value): """ - Set the thermal zones delimited by the thermal boundary + Get the thermal zones delimited by the thermal boundary :param value: [ThermalZone] """ self._thermal_zones = value + # todo: do I need these two?? @property def azimuth(self): """ Get the thermal boundary azimuth in radians :return: float """ - return self._surface.azimuth + return self.parent_surface.azimuth @property def inclination(self): """ - Set the thermal boundary inclination in radians + Get the thermal boundary inclination in radians :return: float """ - return self._surface.inclination + return self.parent_surface.inclination @property - def area(self): + def opaque_area(self): """ - Set the thermal boundary area in square meters + Get the thermal boundary area in square meters :return: float """ - # to check the lod without depending on that parameter - if float(self.surface.solid_polygon.area) - float(self.surface.perimeter_polygon.area) < 1e-3: - area = float(self.surface.perimeter_polygon.area) * (1 - float(self.window_ratio)) - else: - area = self.surface.solid_polygon.area - return area - - @property - def _total_area_including_windows(self): - """ - Get the thermal boundary plus windows area in square meters - :return: float - """ - return self.surface.perimeter_polygon.area + return float(self._opaque_area) @property def thickness(self): @@ -125,7 +111,8 @@ class ThermalBoundary: self._thickness = 0.0 if self.layers is not None: for layer in self.layers: - self._thickness += layer.thickness + if not layer.material.no_mass: + self._thickness += layer.thickness return self._thickness @property @@ -181,30 +168,34 @@ class ThermalBoundary: self._outside_visible_absorptance = float(value) @property - def thermal_openings(self) -> List[ThermalOpening]: + def thermal_openings(self) -> Union[None, List[ThermalOpening]]: """ Get thermal boundary thermal openings :return: [ThermalOpening] """ if self._thermal_openings is None: - if float(self.window_ratio) > 0: - thermal_opening = ThermalOpening() - thermal_opening.area = float(self._total_area_including_windows) * float(self.window_ratio) - thermal_opening.hi = self.hi - thermal_opening.he = self.he - self._thermal_openings = [thermal_opening] + if self.window_ratio is not None: + if self.window_ratio == 0: + self._thermal_openings = [] + else: + thermal_opening = ThermalOpening() + if self.window_ratio == 1: + _area = self.opaque_area + else: + _area = self.opaque_area * self.window_ratio / (1-self.window_ratio) + thermal_opening.area = _area + self._thermal_openings = [thermal_opening] else: - self._thermal_openings = [] + if len(self.windows_areas) > 0: + self._thermal_openings = [] + for window_area in self.windows_areas: + thermal_opening = ThermalOpening() + thermal_opening.area = window_area + self._thermal_openings.append(thermal_opening) + else: + self._thermal_openings = [] return self._thermal_openings - @thermal_openings.setter - def thermal_openings(self, value): - """ - Set thermal boundary thermal openings - :param value: [ThermalOpening] - """ - self._thermal_openings = value - @property def construction_name(self) -> Union[None, str]: """ @@ -244,24 +235,47 @@ class ThermalBoundary: Get thermal boundary surface type :return: str """ - return self._surface.type + return self.parent_surface.type @property def window_ratio(self) -> Union[None, float]: """ Get thermal boundary window ratio - :return: None or float + It returns the window ratio calculated as the total windows' areas in a wall divided by + the total (opaque + transparent) area of that wall if windows are defined in the geometry imported. + If not, it returns the window ratio imported from an external source (e.g. construction library, manually assigned). + If none of those sources are available, it returns None. + :return: float """ + if self.windows_areas is not None: + if not self._calculated: + _calculated = True + if len(self.windows_areas) == 0: + self._window_ratio = 0 + else: + total_window_area = 0 + for window_area in self.windows_areas: + total_window_area += window_area + self._window_ratio = total_window_area / (self.opaque_area + total_window_area) return self._window_ratio @window_ratio.setter def window_ratio(self, value): """ Set thermal boundary window ratio - :param value: float + :param value: str """ - if value is not None: - self._window_ratio = float(value) + if self._calculated: + raise ValueError('Window ratio cannot be assigned when the windows are defined in the geometry.') + self._window_ratio = float(value) + + @property + def windows_areas(self) -> [float]: + """ + Get windows areas + :return: [float] + """ + return self._windows_areas @property def u_value(self) -> Union[None, float]: @@ -327,7 +341,7 @@ class ThermalBoundary: :param value: float """ if value is not None: - self._hi = float(value) + self._hi = value @property def he(self) -> Union[None, float]: @@ -344,14 +358,7 @@ class ThermalBoundary: :param value: float """ if value is not None: - self._he = float(value) - - @property - def surface_geometry(self): - """ - Raises not implemented error - """ - raise NotImplementedError + self._he = value @property def virtual_internal_surface(self) -> Surface: @@ -360,10 +367,9 @@ class ThermalBoundary: :return: Surface """ if self._virtual_internal_surface is None: - self._virtual_internal_surface = self.surface.inverse + self._virtual_internal_surface = self.parent_surface.inverse return self._virtual_internal_surface - # todo: need extract information from construction library or assume them at the beginning of workflows @property def inside_emissivity(self) -> Union[None, float]: """ diff --git a/city_model_structure/building_demand/thermal_control.py b/city_model_structure/building_demand/thermal_control.py new file mode 100644 index 00000000..20888023 --- /dev/null +++ b/city_model_structure/building_demand/thermal_control.py @@ -0,0 +1,86 @@ +""" +ThermalControl module +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2022 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +""" +from typing import Union +from city_model_structure.attributes.schedule import Schedule + + +class ThermalControl: + """ + ThermalControl class + """ + def __init__(self): + self._heating_set_point = None + self._cooling_set_point = None + self._hvac_availability = None + self._heating_set_back = None + + @property + def heating_set_point(self) -> Union[None, Schedule]: + """ + Get heating set point defined for a thermal zone in Celsius + :return: None or Schedule + """ + return self._heating_set_point + + @heating_set_point.setter + def heating_set_point(self, value): + """ + Set heating set point defined for a thermal zone in Celsius + :param value: Schedule + """ + self._heating_set_point = value + + @property + def heating_set_back(self) -> Union[None, float]: + """ + Get heating set back defined for a thermal zone in Celsius + Heating set back is the only parameter which is not a schedule as it is either one value or it is implicit in the + set point schedule + :return: None or float + """ + return self._heating_set_back + + @heating_set_back.setter + def heating_set_back(self, value): + """ + Set heating set back defined for a thermal zone in Celsius + :param value: float + """ + if value is not None: + self._heating_set_back = float(value) + + @property + def cooling_set_point(self) -> Union[None, Schedule]: + """ + Get cooling set point defined for a thermal zone in Celsius + :return: None or Schedule + """ + return self._cooling_set_point + + @cooling_set_point.setter + def cooling_set_point(self, value): + """ + Set cooling set point defined for a thermal zone in Celsius + :param value: Schedule + """ + self._cooling_set_point = value + + @property + def hvac_availability(self) -> Union[None, Schedule]: + """ + Get the availability of the conditioning system defined for a thermal zone + :return: None or Schedule + """ + return self._hvac_availability + + @hvac_availability.setter + def hvac_availability(self, value): + """ + Set the availability of the conditioning system defined for a thermal zone + :param value: Schedule + """ + self._hvac_availability = value + diff --git a/city_model_structure/building_demand/thermal_opening.py b/city_model_structure/building_demand/thermal_opening.py index 0683f095..101dcffc 100644 --- a/city_model_structure/building_demand/thermal_opening.py +++ b/city_model_structure/building_demand/thermal_opening.py @@ -6,6 +6,7 @@ Contributors Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ import uuid from typing import TypeVar, Union +from helpers.configuration_helper import ConfigurationHelper as ch Polygon = TypeVar('Polygon') @@ -25,9 +26,8 @@ class ThermalOpening: self._front_side_solar_transmittance_at_normal_incidence = None self._back_side_solar_transmittance_at_normal_incidence = None self._overall_u_value = None - self._hi = None - self._he = None - self._surface_geometry = None + self._hi = ch().convective_heat_transfer_coefficient_interior + self._he = ch().convective_heat_transfer_coefficient_exterior self._inside_emissivity = None self._alpha_coefficient = None self._radiative_coefficient = None @@ -240,15 +240,6 @@ class ThermalOpening: if value is not None: self._he = float(value) - @property - def surface_geometry(self) -> Polygon: - """ - Get the polygon that defines the thermal opening - :return: Polygon - """ - return self._surface_geometry - - # todo: need extract information from construction library or assume them at the beginning of workflows @property def inside_emissivity(self) -> Union[None, float]: """ diff --git a/city_model_structure/building_demand/thermal_zone.py b/city_model_structure/building_demand/thermal_zone.py index a3ad26e1..dd20bc28 100644 --- a/city_model_structure/building_demand/thermal_zone.py +++ b/city_model_structure/building_demand/thermal_zone.py @@ -5,32 +5,33 @@ Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@conc Contributors Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ import uuid -from typing import List, TypeVar, Union +from typing import List, Union, Tuple, TypeVar from city_model_structure.building_demand.usage_zone import UsageZone -import ast +from city_model_structure.building_demand.thermal_control import ThermalControl +from city_model_structure.energy_systems.hvac_system import HvacSystem +from city_model_structure.attributes.schedule import Schedule ThermalBoundary = TypeVar('ThermalBoundary') -Polyhedron = TypeVar('Polyhedron') class ThermalZone: """ ThermalZone class """ - def __init__(self, thermal_boundaries, volume): - self._floor_area = None + def __init__(self, thermal_boundaries, volume, floor_area): + self._id = None + self._floor_area = floor_area self._thermal_boundaries = thermal_boundaries - self._is_mechanically_ventilated = None self._additional_thermal_bridge_u_value = None self._effective_thermal_capacity = None self._indirectly_heated_area_ratio = None self._infiltration_rate_system_on = None self._infiltration_rate_system_off = None - self._usage_zones = [] + self._usage_zones = None self._volume = volume - self._volume_geometry = None - self._id = None self._ordinate_number = None + self._thermal_control = None + self._hvac_system = None @property def id(self): @@ -43,34 +44,11 @@ class ThermalZone: return self._id @property - def is_mechanically_ventilated(self) -> Union[None, bool]: - """ - Get thermal zone mechanical ventilation flag - :return: None or Boolean - """ - return self._is_mechanically_ventilated - - @is_mechanically_ventilated.setter - def is_mechanically_ventilated(self, value): - """ - Set thermal zone mechanical ventilation flag - :param value: Boolean - """ - if value is not None: - self._is_mechanically_ventilated = ast.literal_eval(value) - - @property - def floor_area(self): + def floor_area(self) -> float: """ Get thermal zone floor area in m2 :return: float """ - if self._floor_area is None: - self._floor_area = 0 - for thermal_boundary in self.thermal_boundaries: - s = thermal_boundary.surface - if s.type == 'Ground': - self._floor_area += s.perimeter_polygon.area return self._floor_area @property @@ -133,10 +111,10 @@ class ThermalZone: self._indirectly_heated_area_ratio = float(value) @property - def infiltration_rate_system_on(self) -> Union[None, float]: + def infiltration_rate_system_on(self) -> Union[None, Schedule]: """ Get thermal zone infiltration rate system on in air changes per hour (ACH) - :return: None or float + :return: None or Schedule """ return self._infiltration_rate_system_on @@ -144,16 +122,15 @@ class ThermalZone: def infiltration_rate_system_on(self, value): """ Set thermal zone infiltration rate system on in air changes per hour (ACH) - :param value: float + :param value: Schedule """ - if value is not None: - self._infiltration_rate_system_on = float(value) + self._infiltration_rate_system_on = value @property - def infiltration_rate_system_off(self) -> Union[None, float]: + def infiltration_rate_system_off(self) -> Union[None, Schedule]: """ Get thermal zone infiltration rate system off in air changes per hour (ACH) - :return: None or float + :return: None or Schedule """ return self._infiltration_rate_system_off @@ -161,15 +138,14 @@ class ThermalZone: def infiltration_rate_system_off(self, value): """ Set thermal zone infiltration rate system on in air changes per hour (ACH) - :param value: float + :param value: Schedule """ - if value is not None: - self._infiltration_rate_system_off = float(value) + self._infiltration_rate_system_off = value @property - def usage_zones(self) -> List[UsageZone]: + def usage_zones(self) -> Tuple[float, UsageZone]: """ - Get thermal zone usage zones + Get list of usage zones and the percentage of thermal zone's volume affected by that usage :return: [UsageZone] """ return self._usage_zones @@ -177,8 +153,8 @@ class ThermalZone: @usage_zones.setter def usage_zones(self, values): """ - Set thermal zone usage zones - :param values: [UsageZone] + Set list of usage zones and the percentage of thermal zone's volume affected by that usage + :param values: Tuple[float, UsageZone] """ self._usage_zones = values @@ -190,14 +166,6 @@ class ThermalZone: """ return self._volume - @property - def volume_geometry(self) -> Polyhedron: - """ - Get the polyhedron defined by the thermal zone - :return: Polyhedron - """ - return self._volume_geometry - @property def ordinate_number(self) -> Union[None, int]: """ @@ -214,3 +182,35 @@ class ThermalZone: """ if value is not None: self._ordinate_number = int(value) + + @property + def thermal_control(self) -> Union[None, ThermalControl]: + """ + Get thermal control of this thermal zone + :return: None or ThermalControl + """ + return self._thermal_control + + @thermal_control.setter + def thermal_control(self, value): + """ + Set thermal control for this thermal zone + :param value: ThermalControl + """ + self._thermal_control = value + + @property + def hvac_system(self) -> Union[None, HvacSystem]: + """ + Get HVAC system installed for this thermal zone + :return: None or HvacSystem + """ + return self._hvac_system + + @hvac_system.setter + def hvac_system(self, value): + """ + Set HVAC system installed for this thermal zone + :param value: HvacSystem + """ + self._hvac_system = value diff --git a/city_model_structure/building_demand/usage_zone.py b/city_model_structure/building_demand/usage_zone.py index a2c16435..590ce6d5 100644 --- a/city_model_structure/building_demand/usage_zone.py +++ b/city_model_structure/building_demand/usage_zone.py @@ -5,13 +5,12 @@ Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.mons Contributors Guille Gutierrez guillermo.gutierrezmorote@concordia.ca """ import uuid -from typing import List, TypeVar, Union -import ast - -InternalGains = TypeVar('InternalGains') -Occupants = TypeVar('Occupants') -Polyhedron = TypeVar('Polyhedron') -Schedule = TypeVar('Schedule') +from typing import List, Union +import helpers.constants as cte +from city_model_structure.building_demand.internal_gains import InternalGains +from city_model_structure.building_demand.occupancy import Occupancy +from city_model_structure.building_demand.lighting import Lighting +from city_model_structure.building_demand.appliances import Appliances class UsageZone: @@ -21,23 +20,15 @@ class UsageZone: def __init__(self): self._id = None self._usage = None - self._internal_gains = None - self._heating_setpoint = None - self._heating_setback = None - self._cooling_setpoint = None - self._occupancy_density = None + self._not_detailed_source_mean_annual_internal_gains = None self._hours_day = None self._days_year = None - self._dhw_average_volume_pers_day = None - self._dhw_preparation_temperature = None - self._electrical_app_average_consumption_sqm_year = None +# self._electrical_app_average_consumption_sqm_year = None self._mechanical_air_change = None - self._occupants = None - self._schedules = None - self._volume = None - self._volume_geometry = None - self._is_heated = False - self._is_cooled = False + self._occupancy = None + self._lighting = None + self._appliances = None + self._internal_gains = None @property def id(self): @@ -50,71 +41,20 @@ class UsageZone: return self._id @property - def internal_gains(self) -> List[InternalGains]: + def not_detailed_source_mean_annual_internal_gains(self) -> List[InternalGains]: """ - Get usage zone internal gains + Get usage zone internal gains with unknown energy source :return: [InternalGains] """ - return self._internal_gains + return self._not_detailed_source_mean_annual_internal_gains - @internal_gains.setter - def internal_gains(self, value): + @not_detailed_source_mean_annual_internal_gains.setter + def not_detailed_source_mean_annual_internal_gains(self, value): """ - Set usage zone internal gains + Set usage zone internal gains with unknown energy source :param value: [InternalGains] """ - self._internal_gains = value - - @property - def heating_setpoint(self) -> Union[None, float]: - """ - Get usage zone heating set point in Celsius - :return: None or float - """ - return self._heating_setpoint - - @heating_setpoint.setter - def heating_setpoint(self, value): - """ - Set usage zone heating set point in Celsius - :param value: float - """ - if value is not None: - self._heating_setpoint = float(value) - - @property - def heating_setback(self) -> Union[None, float]: - """ - Get usage zone heating setback in Celsius - :return: None or float - """ - return self._heating_setback - - @heating_setback.setter - def heating_setback(self, value): - """ - Set usage zone heating setback in Celsius - :param value: float - """ - if value is not None: - self._heating_setback = float(value) - - @property - def cooling_setpoint(self) -> Union[None, float]: - """ - Get usage zone cooling setpoint in Celsius - :return: None or float - """ - return self._cooling_setpoint - - @cooling_setpoint.setter - def cooling_setpoint(self, value): - """ - Set usage zone cooling setpoint in Celsius - :param value: float - """ - if value is not None: - self._cooling_setpoint = float(value) + self._not_detailed_source_mean_annual_internal_gains = value @property def hours_day(self) -> Union[None, float]: @@ -184,89 +124,6 @@ class UsageZone: if value is not None: self._usage = str(value) - @property - def occupants(self) -> List[Occupants]: - """ - Get occupants data - :return: [Occupants] - """ - return self._occupants - - @occupants.setter - def occupants(self, values): - """ - Set occupants data - :param values: [Occupants] - """ - self._occupants = values - - @property - def schedules(self) -> List[Schedule]: - """ - Get usage zone schedules - :return: List[Schedule] - """ - return self._schedules - - @schedules.setter - def schedules(self, values): - """ - Set usage zone schedules - :param values: List[Schedule] - """ - self._schedules = values - - @property - def occupancy_density(self) -> Union[None, float]: - """ - Get density in persons per m2 - :return: None or float - """ - return self._occupancy_density - - @occupancy_density.setter - def occupancy_density(self, value): - """ - Set density in persons per m2 - :param value: float - """ - if value is not None: - self._occupancy_density = float(value) - - @property - def dhw_average_volume_pers_day(self) -> Union[None, float]: - """ - Get average DHW consumption in cubic meters per person per day - :return: None or float - """ - return self._dhw_average_volume_pers_day - - @dhw_average_volume_pers_day.setter - def dhw_average_volume_pers_day(self, value): - """ - Set average DHW consumption in cubic meters per person per day - :param value: float - """ - if value is not None: - self._dhw_average_volume_pers_day = float(value) - - @property - def dhw_preparation_temperature(self) -> Union[None, float]: - """ - Get preparation temperature of the DHW in Celsius - :return: None or float - """ - return self._dhw_preparation_temperature - - @dhw_preparation_temperature.setter - def dhw_preparation_temperature(self, value): - """ - Set preparation temperature of the DHW in Celsius - :param value: float - """ - if value is not None: - self._dhw_preparation_temperature = float(value) - @property def electrical_app_average_consumption_sqm_year(self) -> Union[None, float]: """ @@ -285,60 +142,93 @@ class UsageZone: self._electrical_app_average_consumption_sqm_year = float(value) @property - def volume_geometry(self) -> Polyhedron: + def occupancy(self) -> Union[None, Occupancy]: """ - Get the polyhedron defined by the usage zone - :return: Polyhedron + Get occupancy in the usage zone + :return: None or Occupancy """ - return self._volume_geometry + return self._occupancy + + @occupancy.setter + def occupancy(self, value): + """ + Set occupancy in the usage zone + :param value: Occupancy + """ + self._occupancy = value @property - def volume(self) -> Union[None, float]: + def lighting(self) -> Union[None, Lighting]: """ - Get the volume in cubic meters - :return: None or float + Get lighting information + :return: None or Lighting """ - return self._volume + return self._lighting - @volume.setter - def volume(self, value): + @lighting.setter + def lighting(self, value): """ - Set volume in cubic meters - :param value: float + Set lighting information + :param value: Lighting """ - if value is not None: - self._volume = float(value) + self._lighting = value @property - def is_heated(self) -> Union[None, bool]: + def appliances(self) -> Union[None, Appliances]: """ - Get thermal zone heated flag - :return: None or Boolean + Get appliances information + :return: None or Appliances """ - return self._is_heated + return self._appliances - @is_heated.setter - def is_heated(self, value): + @appliances.setter + def appliances(self, value): """ - Set thermal zone heated flag - :param value: Boolean + Set appliances information + :param value: Appliances """ - if value is not None: - self._is_heated = ast.literal_eval(value) + self._appliances = value - @property - def is_cooled(self) -> Union[None, bool]: + def get_internal_gains(self) -> [InternalGains]: """ - Get thermal zone cooled flag - :return: None or Boolean + Calculates and returns the list of all internal gains defined + :return: InternalGains """ - return self._is_cooled - - @is_cooled.setter - def is_cooled(self, value): - """ - Set thermal zone cooled flag - :param value: Boolean - """ - if value is not None: - self._is_cooled = ast.literal_eval(value) + if self.occupancy is not None: + if self.occupancy.latent_internal_gain is not None: + _internal_gain = InternalGains() + _internal_gain.type = cte.OCCUPANCY + _total_heat_gain = (self.occupancy.sensible_convective_internal_gain + + self.occupancy.sensible_radiant_internal_gain + + self.occupancy.latent_internal_gain) + _internal_gain.average_internal_gain = _total_heat_gain + _internal_gain.latent_fraction = self.occupancy.latent_internal_gain / _total_heat_gain + _internal_gain.radiative_fraction = self.occupancy.sensible_radiant_internal_gain / _total_heat_gain + _internal_gain.convective_fraction = self.occupancy.sensible_convective_internal_gain / _total_heat_gain + _internal_gain.schedule = self.occupancy.occupancy_schedule + self._internal_gains = [_internal_gain] + if self.lighting is not None: + _internal_gain = InternalGains() + _internal_gain.type = cte.LIGHTING + _internal_gain.average_internal_gain = self.lighting.lighting_density + _internal_gain.latent_fraction = self.lighting.latent_fraction + _internal_gain.radiative_fraction = self.lighting.radiant_fraction + _internal_gain.convective_fraction = self.lighting.convective_fraction + _internal_gain.schedule = self.lighting.schedule + if self._internal_gains is not None: + self._internal_gains.append(_internal_gain) + else: + self._internal_gains = [_internal_gain] + if self.appliances is not None: + _internal_gain = InternalGains() + _internal_gain.type = cte.APPLIANCES + _internal_gain.average_internal_gain = self.appliances.lighting_density + _internal_gain.latent_fraction = self.appliances.latent_fraction + _internal_gain.radiative_fraction = self.appliances.radiant_fraction + _internal_gain.convective_fraction = self.appliances.convective_fraction + _internal_gain.schedule = self.appliances.schedule + if self._internal_gains is not None: + self._internal_gains.append(_internal_gain) + else: + self._internal_gains = [_internal_gain] + return self._internal_gains diff --git a/city_model_structure/city_object.py b/city_model_structure/city_object.py index 2147909b..d71e3cea 100644 --- a/city_model_structure/city_object.py +++ b/city_model_structure/city_object.py @@ -36,13 +36,21 @@ class CityObject: self._beam = dict() self._sensors = [] + @property + def name(self): + """ + Get building name + :return: str + """ + return self._name + @property def lod(self) -> int: """ Get city object level of detail 1, 2, 3 or 4 :return: int """ - lod = math.log(self._lod, 2) + 1 + lod = int(math.log(self._lod, 2) + 1) return lod @property diff --git a/city_model_structure/energy_systems/hvac_system.py b/city_model_structure/energy_systems/hvac_system.py new file mode 100644 index 00000000..4bc6edf3 --- /dev/null +++ b/city_model_structure/energy_systems/hvac_system.py @@ -0,0 +1,32 @@ +""" +HvacSystem module +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2022 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +""" +from typing import Union + + +class HvacSystem: + """ + HvacSystem class + """ + def __init__(self): + self._type = None + + @property + def type(self) -> Union[None, str]: + """ + Get hvac system type a thermal zone + :return: None or str + """ + return self._type + + @type.setter + def type(self, value): + """ + Set heating set point defined for a thermal zone + :param value: str + """ + if value is not None: + self._type = str(value) + diff --git a/city_model_structure/iot/concordia_energy_sensor.py b/city_model_structure/iot/concordia_energy_sensor.py new file mode 100644 index 00000000..afc7b930 --- /dev/null +++ b/city_model_structure/iot/concordia_energy_sensor.py @@ -0,0 +1,45 @@ +""" +Energy Sensor module +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@concordia.ca +""" + +import pandas as pd +from city_model_structure.iot.sensor import Sensor + + +class ConcordiaEnergySensor(Sensor): + """ + Concordia energy sensor. + """ + + def __init__(self, name): + super().__init__() + self._name = name + self._interval = 5 + self._interval_units = 'minutes' + self._type = 'ConcordiaEnergySensor' + self._units = 'kW' + self._measures = pd.DataFrame(columns=["Date time", "Energy consumption"]) + + @property + def measures(self) -> pd.DataFrame: + """ + Get sensor measures [yyyy-mm-dd, hh:mm:ss kW] + :return: DataFrame["Date time", "Energy consumption"] + """ + return self._measures + + @measures.deleter + def measures(self): + """ + Delete sensor measures + """ + self._measures.drop = None + + def add_period(self, measures): + """ + Add or update a period measures to the dataframe + """ + measures = self._measures.append(measures, ignore_index=True) + self._measures = measures.drop_duplicates('Date time', keep='last') diff --git a/city_model_structure/iot/concordia_gas_flow_sensor.py b/city_model_structure/iot/concordia_gas_flow_sensor.py new file mode 100644 index 00000000..86da31bd --- /dev/null +++ b/city_model_structure/iot/concordia_gas_flow_sensor.py @@ -0,0 +1,45 @@ +""" +Gas Flow Sensor module +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +""" + +import pandas as pd +from city_model_structure.iot.sensor import Sensor + + +class ConcordiaGasFlowSensor(Sensor): + """ + Concordia gas flow sensor. + """ + + def __init__(self, name): + super().__init__() + self._name = name + self._interval = 5 + self._interval_units = 'minutes' + self._type = 'ConcordiaGasFlowSensor' + self._units = 'm3' + self._measures = pd.DataFrame(columns=["Date time", "Gas Flow Cumulative Monthly"]) + + @property + def measures(self) -> pd.DataFrame: + """ + Get sensor measures [yyyy-mm-dd, hh:mm:ss m3] + :return: DataFrame["Date time", "Gas Flow Cumulative Monthly"] + """ + return self._measures + + @measures.deleter + def measures(self): + """ + Delete sensor measures + """ + self._measures.drop = None + + def add_period(self, measures): + """ + Add or update a period measures to the dataframe + """ + measures = self._measures.append(measures, ignore_index=True) + self._measures = measures.drop_duplicates('Date time', keep='last') diff --git a/city_model_structure/iot/concordia_temperature_sensor.py b/city_model_structure/iot/concordia_temperature_sensor.py new file mode 100644 index 00000000..9ae6079c --- /dev/null +++ b/city_model_structure/iot/concordia_temperature_sensor.py @@ -0,0 +1,45 @@ +""" +Temperature Sensor module +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +""" + +import pandas as pd +from city_model_structure.iot.sensor import Sensor + + +class ConcordiaTemperatureSensor(Sensor): + """ + Concordia temperature sensor. + """ + + def __init__(self, name): + super().__init__() + self._name = name + self._interval = 5 + self._interval_units = 'minutes' + self._type = 'ConcordiaTemperatureSensor' + self._units = 'Celsius' + self._measures = pd.DataFrame(columns=["Date time", "Temperature"]) + + @property + def measures(self) -> pd.DataFrame: + """ + Get sensor measures [yyyy-mm-dd, hh:mm:ss Celsius] + :return: DataFrame["Date time", "Temperature"] + """ + return self._measures + + @measures.deleter + def measures(self): + """ + Delete sensor measures + """ + self._measures.drop = None + + def add_period(self, measures): + """ + Add or update a period measures to the dataframe + """ + measures = self._measures.append(measures, ignore_index=True) + self._measures = measures.drop_duplicates('Date time', keep='last') diff --git a/city_model_structure/iot/sensor.py b/city_model_structure/iot/sensor.py index c3801749..4eceafd1 100644 --- a/city_model_structure/iot/sensor.py +++ b/city_model_structure/iot/sensor.py @@ -1,73 +1,76 @@ """ Sensor module SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2022 Project Author Guille Gutierrez guillermo.gutierrezmorote@concordia.ca +Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@concordia.ca """ -import uuid -from city_model_structure.iot.sensor_measure import SensorMeasure -from city_model_structure.iot.sensor_type import SensorType +from helpers.location import Location class Sensor: """ Sensor abstract class """ - def __init__(self, sensor_id=None, model=None, sensor_type=None, indoor=False, board=None): - self._id = sensor_id - self._model = model - self._type = sensor_type - self._indoor = indoor - self._board = board - self._measures = [] + def __init__(self): + self._name = None + self._type = None + self._units = None + self._location = None @property - def id(self): + def name(self): """ - Get the sensor id a random uuid will be assigned if no ID was provided to the constructor - :return: Id + Get sensor name + :return: str """ - if self._id is None: - self._id = uuid.uuid4() - return self._id + if self._name is None: + raise ValueError('Undefined sensor name') + return self._name + + @name.setter + def name(self, value): + """ + Set sensor name + :param value: str + """ + if value is not None: + self._name = str(value) @property - def type(self) -> SensorType: + def type(self): """ Get sensor type - :return: SensorTypeEnum or Error + :return: str """ - if self._type is None: - raise ValueError('Unknown sensor type') return self._type @property - def model(self): + def units(self): """ - Get sensor model is any - :return: str or None + Get sensor units + :return: str """ - return self._model + return self._units @property - def board(self): + def location(self) -> Location: """ - Get sensor board if any - :return: str or None + Get sensor location + :return: Location """ - return self._model + return self._location + + @location.setter + def location(self, value): + """ + Set sensor location + :param value: Location + """ + self._location = value @property - def indoor(self): - """ - Get is the sensor it's located indoor or outdoor - :return: boolean - """ - return self._indoor - - @property - def measures(self) -> [SensorMeasure]: + def measures(self): """ Raises not implemented error """ - return self._measures + raise NotImplementedError diff --git a/city_model_structure/iot/sensor_measure.py b/city_model_structure/iot/sensor_measure.py deleted file mode 100644 index afdd40db..00000000 --- a/city_model_structure/iot/sensor_measure.py +++ /dev/null @@ -1,40 +0,0 @@ -""" -Sensor measure module -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2022 Project Author Guille Gutierrez guillermo.gutierrezmorote@concordia.ca -""" - -class SensorMeasure: - def __init__(self, latitude, longitude, utc_timestamp, value): - self._latitude = latitude - self._longitude = longitude - self._utc_timestamp = utc_timestamp - self._value = value - - @property - def latitude(self): - """ - Get measure latitude - """ - return self._latitude - - @property - def longitude(self): - """ - Get measure longitude - """ - return self._longitude - - @property - def utc_timestamp(self): - """ - Get measure timestamp in utc - """ - return self._utc_timestamp - - @property - def value(self): - """ - Get sensor measure value - """ - return self._value \ No newline at end of file diff --git a/city_model_structure/iot/sensor_type.py b/city_model_structure/iot/sensor_type.py deleted file mode 100644 index 414b6f60..00000000 --- a/city_model_structure/iot/sensor_type.py +++ /dev/null @@ -1,20 +0,0 @@ -""" -Sensor type module -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2022 Project Author Guille Gutierrez guillermo.gutierrezmorote@concordia.ca -""" - -from enum import Enum - -class SensorType(Enum): - HUMIDITY = 0 - TEMPERATURE = 1 - CO2 = 2 - NOISE = 3 - PRESSURE = 4 - DIRECT_RADIATION = 5 - DIFFUSE_RADIATION = 6 - GLOBAL_RADIATION = 7 - AIR_QUALITY = 8 - GAS_FLOW = 9 - ENERGY = 10 diff --git a/city_model_structure/iot/station.py b/city_model_structure/iot/station.py deleted file mode 100644 index 8a15e62d..00000000 --- a/city_model_structure/iot/station.py +++ /dev/null @@ -1,41 +0,0 @@ -""" -Station -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2022 Project Author Guille Gutierrez guillermo.gutierrezmorote@concordia.ca -""" -import uuid - -from city_model_structure.iot.sensor import Sensor - - -class Station: - def __init__(self, station_id=None, _mobile=False): - self._id = station_id - self._mobile = _mobile - self._sensors = [] - - @property - def id(self): - """ - Get the station id a random uuid will be assigned if no ID was provided to the constructor - :return: Id - """ - if self._id is None: - self._id = uuid.uuid4() - return self._id - - @property - def _mobile(self): - """ - Get if the station is mobile or not - :return: bool - """ - return self._mobile - - @property - def sensors(self) -> [Sensor]: - """ - Get the sensors belonging to the station - :return: [Sensor] - """ - return self._sensors diff --git a/city_model_structure/lca_material.py b/city_model_structure/lca_material.py deleted file mode 100644 index 6d73384b..00000000 --- a/city_model_structure/lca_material.py +++ /dev/null @@ -1,242 +0,0 @@ -""" -Material module -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2020 Project Author Atiya atiya.atiya@mail.concordia.ca -Contributor Mohammad Reza mohammad.seyedabadi@mail.concordia.ca -""" - -from typing import Union - -class LcaMaterial: - def __init__(self): - self._id = None - self._type = None - self._name = None - self._density = None - self._density_unit = None - self._embodied_carbon = None - self._embodied_carbon_unit = None - self._recycling_ratio = None - self._company_recycling_ratio = None - self._onsite_recycling_ratio = None - self._landfilling_ratio = None - self._cost = None - self._cost_unit = None - - @property - def id(self): - """ - Get material id - :return: int - """ - return self._id - - @id.setter - def id(self, value): - """ - Set material id - :param value: int - """ - self._id = int(value) - - @property - def type(self): - """ - Get material type - :return: str - """ - return self._type - - @type.setter - def type(self, value): - """ - Set material type - :param value: string - """ - self._type = str(value) - - @property - def name(self): - """ - Get material name - :return: str - """ - return self._name - - @name.setter - def name(self, value): - """ - Set material name - :param value: string - """ - self._name = str(value) - - @property - def density(self) -> Union[None, float]: - """ - Get material density in kg/m3 - :return: None or float - """ - return self._density - - @density.setter - def density(self, value): - """ - Set material density - :param value: float - """ - if value is not None: - self._density = float(value) - - @property - def density_unit(self) -> Union[None, str]: - """ - Get material density unit - :return: None or string - """ - return self._density_unit - - @density_unit.setter - def density_unit(self, value): - """ - Set material density unit - :param value: string - """ - if value is not None: - self._density_unit = str(value) - - @property - def embodied_carbon(self) -> Union[None, float]: - """ - Get material embodied carbon - :return: None or float - """ - return self._embodied_carbon - - @embodied_carbon.setter - def embodied_carbon(self, value): - """ - Set material embodied carbon - :param value: float - """ - if value is not None: - self._embodied_carbon = float(value) - - @property - def embodied_carbon_unit(self) -> Union[None, str]: - """ - Get material embodied carbon unit - :return: None or string - """ - return self._embodied_carbon - - @embodied_carbon_unit.setter - def embodied_carbon_unit(self, value): - """ - Set material embodied carbon unit - :param value: string - """ - if value is not None: - self._embodied_carbon_unit = str(value) - - @property - def recycling_ratio(self) -> Union[None, float]: - """ - Get material recycling ratio - :return: None or float - """ - return self._recycling_ratio - - @recycling_ratio.setter - def recycling_ratio(self, value): - """ - Set material recycling ratio - :param value: float - """ - if value is not None: - self._recycling_ratio = float(value) - - @property - def onsite_recycling_ratio(self) -> Union[None, float]: - """ - Get material onsite recycling ratio - :return: None or float - """ - return self._onsite_recycling_ratio - - @onsite_recycling_ratio.setter - def onsite_recycling_ratio(self, value): - """ - Set material onsite recycling ratio - :param value: float - """ - if value is not None: - self._onsite_recycling_ratio = float(value) - - @property - def company_recycling_ratio(self) -> Union[None, float]: - """ - Get material company recycling ratio - :return: None or float - """ - return self._company_recycling_ratio - - @company_recycling_ratio.setter - def company_recycling_ratio(self, value): - """ - Set material company recycling ratio - :param value: float - """ - if value is not None: - self._company_recycling_ratio = float(value) - - @property - def landfilling_ratio(self) -> Union[None, float]: - """ - Get material landfilling ratio - :return: None or float - """ - return self._landfilling_ratio - - @landfilling_ratio.setter - def landfilling_ratio(self, value): - """ - Set material landfilling ratio - :param value: float - """ - if value is not None: - self._landfilling_ratio = float(value) - - @property - def cost(self) -> Union[None, float]: - """ - Get material cost - :return: None or float - """ - return self._cost - - @cost.setter - def cost(self, value): - """ - Set material cost - :param value: float - """ - if value is not None: - self._cost = float(value) - - @property - def cost_unit(self) -> Union[None, str]: - """ - Get material cost unit - :return: None or string - """ - return self._cost_unit - - @cost_unit.setter - def cost_unit(self, value): - """ - Set material cost unit - :param value: string - """ - if value is not None: - self._cost_unit = float(value) \ No newline at end of file diff --git a/config/configuration.ini b/config/configuration.ini index b2ddfb04..399649f1 100644 --- a/config/configuration.ini +++ b/config/configuration.ini @@ -11,3 +11,5 @@ comnet_occupancy_sensible_radiant = 0.1 comnet_plugs_latent = 0 comnet_plugs_convective = 0.75 comnet_plugs_radiant = 0.25 +convective_heat_transfer_coefficient_interior = 3.5 +convective_heat_transfer_coefficient_exterior = 20 diff --git a/data/construction/ca_constructions_reduced.xml b/data/construction/ca_constructions_reduced.xml index 38a217d9..42bf460e 100644 --- a/data/construction/ca_constructions_reduced.xml +++ b/data/construction/ca_constructions_reduced.xml @@ -105,84 +105,52 @@ #wall below grade 0.512 - 0 - 0 0.512 - 0 - 0 0.67 - 0 - 0 0.848 - 0 - 0 1.048 - 0 - 0 1.154 - 0 - 0 1.243 - 0 - 0 1.425 - 0 - 0 #slab on grade 0.512 - 0 - 0 0.67 - 0 - 0 0.67 - 0 - 0 0.848 - 0 - 0 0.848 - 0 - 0 1.05 - 0 - 0 1.154 - 0 - 0 1.154 - 0 - 0 diff --git a/data/construction/us_constructions.xml b/data/construction/us_constructions.xml index 2d4a27f8..b7a2ab51 100644 --- a/data/construction/us_constructions.xml +++ b/data/construction/us_constructions.xml @@ -39,7 +39,7 @@ - + 1.311 2240 836.8 @@ -47,14 +47,14 @@ 0.7 0.7 - + true 0.21648 0.9 0.7 0.8 - + 0.045 265 836.8 @@ -62,7 +62,7 @@ 0.7 0.7 - + 0.6918 1858 837 @@ -70,7 +70,7 @@ 0.92 0.92 - + 1.7296 2243 837 @@ -78,7 +78,7 @@ 0.65 0.65 - + 0.0432 91 837 @@ -86,7 +86,7 @@ 0.5 0.5 - + 0.16 784.9 830 @@ -94,7 +94,7 @@ 0.92 0.92 - + 0.11 544.62 1210 @@ -102,7 +102,7 @@ 0.78 0.78 - + 0.115 513 1255 @@ -110,7 +110,7 @@ 0.78 0.78 - + 0.1211 593 2510 @@ -118,7 +118,7 @@ 0.78 0.78 - + 0.049 265 836.8 @@ -126,14 +126,14 @@ 0.7 0.7 - + true 0.36256 0.9 0.7 0.7 - + 45.006 7680 418.4 @@ -141,7 +141,7 @@ 0.7 0.3 - + 0.16 1121.29 1460 @@ -149,28 +149,28 @@ 0.7 0.7 - + true 0.21648 0.9 0.7 0.8 - + true 0.21648 0.9 0.7 0.8 - + true 0.21648 0.9 0.7 0.8 - + 0.045 265 836.8 @@ -178,7 +178,7 @@ 0.7 0.7 - + 44.96 7688.86 410 diff --git a/data/greenery/ecore_greenery_catalog.xml b/data/greenery/ecore_greenery_catalog.xml deleted file mode 100644 index 41b7801c..00000000 --- a/data/greenery/ecore_greenery_catalog.xml +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/data/life_cycle_assessment/lca_data.xml b/data/life_cycle_assessment/lca_data.xml index 6e231b60..29bb356d 100644 --- a/data/life_cycle_assessment/lca_data.xml +++ b/data/life_cycle_assessment/lca_data.xml @@ -1,394 +1,5 @@ - - - 1.8 - 560 - 0.8 - 0.3 - 0.7 - 0.2 - .... - - - 1.2 - 310 - 0.8 - 0.3 - 0.7 - 0.2 - .... - - - 2 - 3080 - 0.8 - 0.3 - 0.7 - 0.2 - .... - - - 1.4 - 300 - 0.8 - 0.3 - 0.7 - 0.2 - .... - - - 1.6 - 900 - 0.8 - 0 - 1 - 0.2 - .... - - - 1.6 - 2340 - 0.8 - 0 - 1 - 0.2 - .... - - - 1.6 - 1570 - 0.8 - 0 - 1 - 0.2 - .... - - - 1.4 - 1840 - 0.8 - 0 - 1 - 0.2 - .... - - - 1.3 - 410 - 0.8 - 0 - 1 - 0.2 - .... - - - 2.3 - 160 - 0.8 - 0 - 1 - 0.2 - .... - - - 2.3 - 170 - 0.8 - 0 - 1 - 0.2 - .... - - - 2.3 - 230 - 0.8 - 0 - 1 - 0.2 - .... - - - 2.4 - 240 - 0.8 - 0 - 1 - 0.2 - .... - - - 2.4 - 280 - 0.8 - 0 - 1 - 0.2 - .... - - - 2.3 - 170 - 0.8 - 0 - 1 - 0.2 - .... - - - 1.2 - 440 - 0.8 - 0 - 1 - 0.2 - .... - - - 2.58 - 2660 - 0.95 - 0 - 1 - 0.05 - .... - - - 2.58 - 5260 - 0.95 - 0 - 1 - 0.05 - .... - - - 0.06 - 1760 - 0.9 - 0 - 1 - 0.1 - .... - - - 0.122 - 3080 - 0.9 - 0 - 1 - 0.1 - .... - - - 0.028 - 3180 - 0.9 - 0 - 1 - 0.1 - .... - - - 0.024 - 5140 - 0.9 - 0 - 1 - 0.1 - .... - - - 0.1 - 6040 - 0.9 - 0 - 1 - 0.1 - .... - - - 0.3 - 5380 - 0.9 - 0 - 1 - 0.1 - .... - - - 0.032 - 2150 - 0.9 - 0 - 1 - 0.1 - .... - - - 0.9 - 3420 - 0.6 - 0 - 1 - 0.4 - .... - - - 0.7 - 1430 - 0.6 - 0 - 1 - 0.4 - .... - - - 0.65 - 2780 - 0.6 - 0 - 1 - 0.4 - .... - - - 0.72 - 2190 - 0.6 - 0 - 1 - 0.4 - .... - - - 1.43 - 1070 - 0 - 0 - 0 - 1 - .... - - - 1.43 - 240 - 0 - 0 - 0 - 1 - .... - - - 1.43 - 430 - 0 - 0 - 0 - 1 - .... - - - 1.43 - 340 - 0 - 0 - 0 - 1 - .... - - - 1.2 - 440 - 0.8 - 0 - 1 - 0.2 - .... - - - 2.1 - 1410 - 0.8 - 0 - 1 - 0.2 - .... - - - 1.43 - 250 - 0 - 0 - 0 - 1 - .... - - - 1.44 - 1480 - 0.8 - 0 - 1 - 0.2 - .... - - - 1.44 - 2220 - 0.8 - 0 - 1 - 0.2 - .... - - - 1.27 - 3960 - 0.8 - 0 - 1 - 0.2 - .... - - - 1.15 - 760 - 0.8 - 0 - 1 - 0.2 - .... - - - 8 - 3160 - 0.98 - 0 - 1 - 0.02 - .... - - - 2.7 - 5370 - 0.98 - 0 - 1 - 0.02 - .... - - - 7.85 - 3910 - 0.98 - 0 - 1 - 0.02 - .... - - 0.32 @@ -556,4 +167,393 @@ 1.00000 + + + 1.8 + 560 + 0.8 + 0.3 + 0.7 + 0.2 + .... + + + 1.2 + 310 + 0.8 + 0.3 + 0.7 + 0.2 + .... + + + 2 + 3080 + 0.8 + 0.3 + 0.7 + 0.2 + .... + + + 1.4 + 300 + 0.8 + 0.3 + 0.7 + 0.2 + .... + + + 1.6 + 900 + 0.8 + 0 + 1 + 0.2 + .... + + + 1.6 + 2340 + 0.8 + 0 + 1 + 0.2 + .... + + + 1.6 + 1570 + 0.8 + 0 + 1 + 0.2 + .... + + + 1.4 + 1840 + 0.8 + 0 + 1 + 0.2 + .... + + + 1.3 + 410 + 0.8 + 0 + 1 + 0.2 + .... + + + 2.3 + 160 + 0.8 + 0 + 1 + 0.2 + .... + + + 2.3 + 170 + 0.8 + 0 + 1 + 0.2 + .... + + + 2.3 + 230 + 0.8 + 0 + 1 + 0.2 + .... + + + 2.4 + 240 + 0.8 + 0 + 1 + 0.2 + .... + + + 2.4 + 280 + 0.8 + 0 + 1 + 0.2 + .... + + + 2.3 + 170 + 0.8 + 0 + 1 + 0.2 + .... + + + 1.2 + 440 + 0.8 + 0 + 1 + 0.2 + .... + + + 2.58 + 2660 + 0.95 + 0 + 1 + 0.05 + .... + + + 2.58 + 5260 + 0.95 + 0 + 1 + 0.05 + .... + + + 0.06 + 1760 + 0.9 + 0 + 1 + 0.1 + .... + + + 0.122 + 3080 + 0.9 + 0 + 1 + 0.1 + .... + + + 0.028 + 3180 + 0.9 + 0 + 1 + 0.1 + .... + + + 0.024 + 5140 + 0.9 + 0 + 1 + 0.1 + .... + + + 0.1 + 6040 + 0.9 + 0 + 1 + 0.1 + .... + + + 0.3 + 5380 + 0.9 + 0 + 1 + 0.1 + .... + + + 0.032 + 2150 + 0.9 + 0 + 1 + 0.1 + .... + + + 0.9 + 3420 + 0.6 + 0 + 1 + 0.4 + .... + + + 0.7 + 1430 + 0.6 + 0 + 1 + 0.4 + .... + + + 0.65 + 2780 + 0.6 + 0 + 1 + 0.4 + .... + + + 0.72 + 2190 + 0.6 + 0 + 1 + 0.4 + .... + + + 1.43 + 1070 + 0 + 0 + 0 + 1 + .... + + + 1.43 + 240 + 0 + 0 + 0 + 1 + .... + + + 1.43 + 430 + 0 + 0 + 0 + 1 + .... + + + 1.43 + 340 + 0 + 0 + 0 + 1 + .... + + + 1.2 + 440 + 0.8 + 0 + 1 + 0.2 + .... + + + 2.1 + 1410 + 0.8 + 0 + 1 + 0.2 + .... + + + 1.43 + 250 + 0 + 0 + 0 + 1 + .... + + + 1.44 + 1480 + 0.8 + 0 + 1 + 0.2 + .... + + + 1.44 + 2220 + 0.8 + 0 + 1 + 0.2 + .... + + + 1.27 + 3960 + 0.8 + 0 + 1 + 0.2 + .... + + + 1.15 + 760 + 0.8 + 0 + 1 + 0.2 + .... + + + 8 + 3160 + 0.98 + 0 + 1 + 0.02 + .... + + + 2.7 + 5370 + 0.98 + 0 + 1 + 0.02 + .... + + + 7.85 + 3910 + 0.98 + 0 + 1 + 0.02 + .... + + \ No newline at end of file diff --git a/exports/formats/energy_ade.py b/exports/formats/energy_ade.py index 60c10af2..cf335397 100644 --- a/exports/formats/energy_ade.py +++ b/exports/formats/energy_ade.py @@ -329,7 +329,7 @@ class EnergyAde: }, 'energy:area': { '@uom': 'm2', - '#text': f'{thermal_boundary.area}' + '#text': f'{thermal_boundary.opaque_area}' }, 'energy:surfaceGeometry': { 'gml:MultiSurface': { diff --git a/helpers/configuration_helper.py b/helpers/configuration_helper.py index 85677ceb..51d9a3d8 100644 --- a/helpers/configuration_helper.py +++ b/helpers/configuration_helper.py @@ -105,3 +105,19 @@ class ConfigurationHelper: :return: 0.1 """ return self._config.getfloat('buildings', 'comnet_occupancy_sensible_radiant').real + + @property + def convective_heat_transfer_coefficient_interior(self) -> float: + """ + Get configured convective heat transfer coefficient for surfaces inside the building + :return: 3.5 + """ + return self._config.getfloat('buildings', 'convective_heat_transfer_coefficient_interior').real + + @property + def convective_heat_transfer_coefficient_exterior(self) -> float: + """ + Get configured convective heat transfer coefficient for surfaces outside the building + :return: 20 + """ + return self._config.getfloat('buildings', 'convective_heat_transfer_coefficient_exterior').real diff --git a/helpers/constants.py b/helpers/constants.py index 4ce228d4..b55cf955 100644 --- a/helpers/constants.py +++ b/helpers/constants.py @@ -85,11 +85,22 @@ EDUCATION = 'education' LIGHTING = 'Lights' OCCUPANCY = 'Occupancy' -RECEPTACLE = 'Receptacle' +APPLIANCES = 'Appliances' HVAC_AVAILABILITY = 'HVAC Avail' INFILTRATION = 'Infiltration' COOLING_SET_POINT = 'ClgSetPt' HEATING_SET_POINT = 'HtgSetPt' -# todo: are any of these two the receptacle concept?? EQUIPMENT = 'Equipment' ACTIVITY = 'Activity' + +# Geometry +EPSILON = 0.0000001 + +# HVAC types +ONLY_HEATING = 'Heating' +ONLY_COOLING = 'Colling' +ONLY_VENTILATION = 'Ventilation' +HEATING_AND_VENTILATION = 'Heating and ventilation' +COOLING_AND_VENTILATION = 'Cooling and ventilation' +HEATING_AND_COLLING = 'Heating and cooling' +FULL_HVAC = 'Heating and cooling and ventilation' diff --git a/helpers/monthly_to_hourly_demand.py b/helpers/monthly_to_hourly_demand.py index 4251507f..029a42dd 100644 --- a/helpers/monthly_to_hourly_demand.py +++ b/helpers/monthly_to_hourly_demand.py @@ -5,7 +5,7 @@ Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.mons """ import calendar as cal import pandas as pd -from city_model_structure.building_demand.occupants import Occupants +from city_model_structure.building_demand.occupant import Occupant import helpers.constants as cte @@ -34,7 +34,7 @@ class MonthlyToHourlyDemand: # todo: if these are data frames, then they should be called as (Occupancy should be in low case): # usage_zone.schedules.Occupancy # self._conditioning_seasons.heating - occupancy = Occupants().get_complete_year_schedule(usage_zone.schedules['Occupancy']) + occupancy = Occupant().get_complete_year_schedule(usage_zone.schedules['Occupancy']) heating_schedule = self._conditioning_seasons['heating'] hourly_heating = [] @@ -92,7 +92,7 @@ class MonthlyToHourlyDemand: for usage_zone in self._building.usage_zones: temp_set = float(usage_zone.cooling_setpoint) temp_back = 100 - occupancy = Occupants().get_complete_year_schedule(usage_zone.schedules['Occupancy']) + occupancy = Occupant().get_complete_year_schedule(usage_zone.schedules['Occupancy']) cooling_schedule = self._conditioning_seasons['cooling'] hourly_cooling = [] diff --git a/imports/construction/ca_physics_parameters.py b/imports/construction/ca_physics_parameters.py index 25f3e1de..d04c81ea 100644 --- a/imports/construction/ca_physics_parameters.py +++ b/imports/construction/ca_physics_parameters.py @@ -33,7 +33,14 @@ class CaPhysicsParameters(NrelPhysicsInterface): f'and building year of construction: {building.year_of_construction}\n') continue - self._create_storeys(building, archetype) + # if building has no thermal zones defined from geometry, one thermal zone per storey is assigned + if building.thermal_zones is None: + self._create_storeys(building, archetype) + thermal_zones = [] + for storey in building.storeys: + thermal_zones.append(storey.thermal_zone) + building.thermal_zones = thermal_zones + self._assign_values(building, archetype) def _search_archetype(self, function, year_of_construction): @@ -60,11 +67,15 @@ class CaPhysicsParameters(NrelPhysicsInterface): thermal_boundary.u_value = thermal_boundary_archetype.overall_u_value thermal_boundary.outside_solar_absorptance = thermal_boundary_archetype.outside_solar_absorptance thermal_boundary.construction_name = thermal_boundary_archetype.construction_name - thermal_boundary.window_ratio = thermal_boundary_archetype.window_ratio + try: + thermal_boundary.window_ratio = thermal_boundary_archetype.window_ratio + except ValueError: + # This is the normal operation way when the windows are defined in the geometry + continue if thermal_boundary.thermal_openings is not None: for thermal_opening in thermal_boundary.thermal_openings: - if thermal_boundary_archetype.thermal_opening is not None: - thermal_opening_archetype = thermal_boundary_archetype.thermal_opening + if thermal_boundary_archetype.thermal_opening_archetype is not None: + thermal_opening_archetype = thermal_boundary_archetype.thermal_opening_archetype thermal_opening.frame_ratio = thermal_opening_archetype.frame_ratio thermal_opening.g_value = thermal_opening_archetype.g_value thermal_opening.overall_u_value = thermal_opening_archetype.overall_u_value diff --git a/imports/construction/data_classes/nrel_building_achetype.py b/imports/construction/data_classes/building_achetype.py similarity index 89% rename from imports/construction/data_classes/nrel_building_achetype.py rename to imports/construction/data_classes/building_achetype.py index 3956fb98..fb35bad6 100644 --- a/imports/construction/data_classes/nrel_building_achetype.py +++ b/imports/construction/data_classes/building_achetype.py @@ -1,15 +1,15 @@ """ -NrelBuildingArchetype stores construction information by building archetypes +BuildingArchetype stores construction information by building archetypes SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ from typing import List -from imports.construction.data_classes.nrel_thermal_boundary_archetype import NrelThermalBoundaryArchetype +from imports.construction.data_classes.thermal_boundary_archetype import ThermalBoundaryArchetype -class NrelBuildingArchetype: +class BuildingArchetype: """ - NrelBuildingArchetype class + BuildingArchetype class """ def __init__(self, archetype_keys, average_storey_height, storeys_above_ground, effective_thermal_capacity, additional_thermal_bridge_u_value, indirectly_heated_area_ratio, infiltration_rate_system_off, @@ -89,7 +89,7 @@ class NrelBuildingArchetype: return self._infiltration_rate_system_on @property - def thermal_boundary_archetypes(self) -> List[NrelThermalBoundaryArchetype]: + def thermal_boundary_archetypes(self) -> List[ThermalBoundaryArchetype]: """ Get thermal boundary archetypes associated to the building archetype :return: list of boundary archetypes diff --git a/imports/construction/data_classes/nrel_layer_archetype.py b/imports/construction/data_classes/layer_archetype.py similarity index 67% rename from imports/construction/data_classes/nrel_layer_archetype.py rename to imports/construction/data_classes/layer_archetype.py index 46e0ea64..adad7439 100644 --- a/imports/construction/data_classes/nrel_layer_archetype.py +++ b/imports/construction/data_classes/layer_archetype.py @@ -1,17 +1,16 @@ """ -NrelLayerArchetype stores layer and materials information, complementing the NrelBuildingArchetype class +LayerArchetype stores layer and materials information, complementing the BuildingArchetype class SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ -class NrelLayerArchetype: +class LayerArchetype: """ - NrelLayerArchetype class + LayerArchetype class """ def __init__(self, name, solar_absorptance, thermal_absorptance, visible_absorptance, thickness=None, - conductivity=None, specific_heat=None, density=None, no_mass=False, thermal_resistance=None, - lca_id=None): + conductivity=None, specific_heat=None, density=None, no_mass=False, thermal_resistance=None): self._thickness = thickness self._conductivity = conductivity self._specific_heat = specific_heat @@ -22,12 +21,11 @@ class NrelLayerArchetype: self._no_mass = no_mass self._name = name self._thermal_resistance = thermal_resistance - self._lca_id = lca_id @property def thickness(self): """ - Get nrel layer archetype thickness in meters + Get thickness in meters :return: float """ return self._thickness @@ -35,7 +33,7 @@ class NrelLayerArchetype: @property def conductivity(self): """ - Get nrel layer archetype conductivity in W/mK + Get conductivity in W/mK :return: float """ return self._conductivity @@ -43,7 +41,7 @@ class NrelLayerArchetype: @property def specific_heat(self): """ - Get nrel layer archetype conductivity in J/kgK + Get specific heat in J/kgK :return: float """ return self._specific_heat @@ -51,7 +49,7 @@ class NrelLayerArchetype: @property def density(self): """ - Get nrel layer archetype density in kg/m3 + Get density in kg/m3 :return: float """ return self._density @@ -59,7 +57,7 @@ class NrelLayerArchetype: @property def solar_absorptance(self): """ - Get nrel layer archetype solar absorptance + Get solar absorptance :return: float """ return self._solar_absorptance @@ -67,7 +65,7 @@ class NrelLayerArchetype: @property def thermal_absorptance(self): """ - Get nrel layer archetype thermal absorptance + Get thermal absorptance :return: float """ return self._thermal_absorptance @@ -75,7 +73,7 @@ class NrelLayerArchetype: @property def visible_absorptance(self): """ - Get nrel layer archetype visible absorptance + Get visible absorptance :return: float """ return self._visible_absorptance @@ -83,7 +81,7 @@ class NrelLayerArchetype: @property def no_mass(self) -> bool: """ - Get nrel layer archetype no mass flag + Get no mass flag :return: Boolean """ return self._no_mass @@ -91,7 +89,7 @@ class NrelLayerArchetype: @property def name(self): """ - Get nrel layer archetype name + Get name :return: str """ return self._name @@ -99,15 +97,7 @@ class NrelLayerArchetype: @property def thermal_resistance(self): """ - Get nrel layer archetype thermal resistance in m2K/W + Get thermal resistance in m2K/W :return: float """ return self._thermal_resistance - - @property - def lca_id(self): - """ - Get nrel lca_id equivalent for the material - :return: int - """ - return self._lca_id diff --git a/imports/construction/data_classes/nrel_thermal_boundary_archetype.py b/imports/construction/data_classes/nrel_thermal_boundary_archetype.py deleted file mode 100644 index 78f78cb6..00000000 --- a/imports/construction/data_classes/nrel_thermal_boundary_archetype.py +++ /dev/null @@ -1,99 +0,0 @@ -""" -NrelThermalBoundaryArchetype stores thermal boundaries information, complementing the NrelBuildingArchetype class -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca -""" -from typing import List - -from imports.construction.data_classes.nrel_layer_archetype import NrelLayerArchetype -from imports.construction.data_classes.nrel_thermal_opening_archetype import NrelThermalOpeningArchetype - - -class NrelThermalBoundaryArchetype: - """ - NrelThermalBoundaryArchetype class - """ - def __init__(self, boundary_type, window_ratio, construction_name, layers, thermal_opening, - outside_solar_absorptance=None, outside_thermal_absorptance=None, outside_visible_absorptance=None, - overall_u_value=None): - self._boundary_type = boundary_type - self._outside_solar_absorptance = outside_solar_absorptance - self._outside_thermal_absorptance = outside_thermal_absorptance - self._outside_visible_absorptance = outside_visible_absorptance - self._window_ratio = window_ratio - self._construction_name = construction_name - self._overall_u_value = overall_u_value - self._layers = layers - self._thermal_opening = thermal_opening - - @property - def boundary_type(self): - """ - Get nrel thermal boundary archetype type - :return: str - """ - return self._boundary_type - - @property - def outside_solar_absorptance(self): - """ - Get nrel thermal boundary archetype outside solar absorptance - :return: float - """ - return self._outside_solar_absorptance - - @property - def outside_thermal_absorptance(self): - """ - Get nrel thermal boundary archetype outside thermal absorptance - :return: float - """ - return self._outside_thermal_absorptance - - @property - def outside_visible_absorptance(self): - """ - Get nrel thermal boundary archetype outside visible absorptance - :return: float - """ - return self._outside_visible_absorptance - - @property - def window_ratio(self): - """ - Get nrel thermal boundary archetype window ratio - :return: float - """ - return self._window_ratio - - @property - def construction_name(self): - """ - Get nrel thermal boundary archetype construction name - :return: str - """ - return self._construction_name - - @property - def layers(self) -> List[NrelLayerArchetype]: - """ - Get nrel thermal boundary archetype layers - :return: [NrelLayerArchetype] - """ - return self._layers - - @property - def thermal_opening(self) -> NrelThermalOpeningArchetype: - """ - Get nrel thermal boundary archetype - :return: NrelThermalOpeningArchetype - """ - return self._thermal_opening - - @property - def overall_u_value(self): - """ - Get nrel thermal boundary archetype overall U-value in W/m2K - :return: float - """ - return self._overall_u_value diff --git a/imports/construction/data_classes/thermal_boundary_archetype.py b/imports/construction/data_classes/thermal_boundary_archetype.py new file mode 100644 index 00000000..11aecf44 --- /dev/null +++ b/imports/construction/data_classes/thermal_boundary_archetype.py @@ -0,0 +1,136 @@ +""" +ThermalBoundaryArchetype stores thermal boundaries information, complementing the BuildingArchetype class +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2022 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +""" +from typing import List + +from imports.construction.data_classes.layer_archetype import LayerArchetype +from imports.construction.data_classes.thermal_opening_archetype import ThermalOpeningArchetype + + +class ThermalBoundaryArchetype: + """ + ThermalBoundaryArchetype class + """ + def __init__(self, boundary_type, window_ratio, construction_name, layers, thermal_opening, + outside_solar_absorptance=None, outside_thermal_absorptance=None, outside_visible_absorptance=None, + overall_u_value=None, shortwave_reflectance=None, inside_emissivity=None, alpha_coefficient=None, + radiative_coefficient=None): + self._boundary_type = boundary_type + self._outside_solar_absorptance = outside_solar_absorptance + self._outside_thermal_absorptance = outside_thermal_absorptance + self._outside_visible_absorptance = outside_visible_absorptance + self._window_ratio = window_ratio + self._construction_name = construction_name + self._overall_u_value = overall_u_value + self._layers = layers + self._thermal_opening_archetype = thermal_opening + self._shortwave_reflectance = shortwave_reflectance + self._inside_emissivity = inside_emissivity + self._alpha_coefficient = alpha_coefficient + self._radiative_coefficient = radiative_coefficient + + @property + def boundary_type(self): + """ + Get type + :return: str + """ + return self._boundary_type + + @property + def outside_solar_absorptance(self): + """ + Get outside solar absorptance + :return: float + """ + return self._outside_solar_absorptance + + @property + def outside_thermal_absorptance(self): + """ + Get outside thermal absorptance + :return: float + """ + return self._outside_thermal_absorptance + + @property + def outside_visible_absorptance(self): + """ + Get outside visible absorptance + :return: float + """ + return self._outside_visible_absorptance + + @property + def window_ratio(self): + """ + Get window ratio + :return: float + """ + return self._window_ratio + + @property + def construction_name(self): + """ + Get construction name + :return: str + """ + return self._construction_name + + @property + def layers(self) -> List[LayerArchetype]: + """ + Get layers + :return: [NrelLayerArchetype] + """ + return self._layers + + @property + def thermal_opening_archetype(self) -> ThermalOpeningArchetype: + """ + Get thermal opening archetype + :return: ThermalOpeningArchetype + """ + return self._thermal_opening_archetype + + @property + def overall_u_value(self): + """ + Get overall U-value in W/m2K + :return: float + """ + return self._overall_u_value + + @property + def shortwave_reflectance(self): + """ + Get shortwave reflectance + :return: float + """ + return self._shortwave_reflectance + + @property + def inside_emissivity(self): + """ + Get emissivity inside + :return: float + """ + return self._inside_emissivity + + @property + def alpha_coefficient(self): + """ + Get alpha coefficient + :return: float + """ + return self._alpha_coefficient + + @property + def radiative_coefficient(self): + """ + Get radiative coefficient + :return: float + """ + return self._radiative_coefficient diff --git a/imports/construction/data_classes/nrel_thermal_opening_archetype.py b/imports/construction/data_classes/thermal_opening_archetype.py similarity index 56% rename from imports/construction/data_classes/nrel_thermal_opening_archetype.py rename to imports/construction/data_classes/thermal_opening_archetype.py index 9f696116..e1966267 100644 --- a/imports/construction/data_classes/nrel_thermal_opening_archetype.py +++ b/imports/construction/data_classes/thermal_opening_archetype.py @@ -1,30 +1,34 @@ """ -NrelThermalOpeningArchetype stores thermal openings information, complementing the NrelBuildingArchetype class +ThermalOpeningArchetype stores thermal openings information, complementing the BuildingArchetype class SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ -class NrelThermalOpeningArchetype: +class ThermalOpeningArchetype: """ - NrelThermalOpeningArchetype class + ThermalOpeningArchetype class """ def __init__(self, conductivity=None, frame_ratio=None, g_value=None, thickness=None, back_side_solar_transmittance_at_normal_incidence=None, - front_side_solar_transmittance_at_normal_incidence=None, shgc=None, overall_u_value=None): + front_side_solar_transmittance_at_normal_incidence=None, overall_u_value=None, + openable_ratio=None, inside_emissivity=None, alpha_coefficient=None, radiative_coefficient=None): self._conductivity = conductivity self._frame_ratio = frame_ratio self._g_value = g_value self._thickness = thickness self._back_side_solar_transmittance_at_normal_incidence = back_side_solar_transmittance_at_normal_incidence self._front_side_solar_transmittance_at_normal_incidence = front_side_solar_transmittance_at_normal_incidence - self._shgc = shgc self._overall_u_value = overall_u_value + self._openable_ratio = openable_ratio + self._inside_emissivity = inside_emissivity + self._alpha_coefficient = alpha_coefficient + self._radiative_coefficient = radiative_coefficient @property def conductivity(self): """ - Get nrel thermal opening archetype conductivity in W/mK + Get conductivity in W/mK :return: float """ return self._conductivity @@ -32,7 +36,7 @@ class NrelThermalOpeningArchetype: @property def frame_ratio(self): """ - Get nrel thermal opening archetype frame ratio + Get frame ratio :return: float """ return self._frame_ratio @@ -40,7 +44,7 @@ class NrelThermalOpeningArchetype: @property def g_value(self): """ - Get nrel thermal opening archetype g-value + Get g-value, also called shgc :return: float """ return self._g_value @@ -48,7 +52,7 @@ class NrelThermalOpeningArchetype: @property def thickness(self): """ - Get nrel thermal opening archetype thickness in meters + Get thickness in meters :return: float """ return self._thickness @@ -56,7 +60,7 @@ class NrelThermalOpeningArchetype: @property def back_side_solar_transmittance_at_normal_incidence(self): """ - Get nrel thermal opening archetype back side solar transmittance at normal incidence + Get back side solar transmittance at normal incidence :return: float """ return self._back_side_solar_transmittance_at_normal_incidence @@ -64,23 +68,47 @@ class NrelThermalOpeningArchetype: @property def front_side_solar_transmittance_at_normal_incidence(self): """ - Get nrel thermal opening archetype front side solar transmittance at normal incidence + Get front side solar transmittance at normal incidence :return: float """ return self._front_side_solar_transmittance_at_normal_incidence - @property - def shgc(self): - """ - Get nrel thermal opening archetype shcg - :return: float - """ - return self._shgc - @property def overall_u_value(self): """ - Get nrel thermal opening archetype overall U-value in W/m2K - :param value: float + Get overall U-value in W/m2K + :return: float """ return self._overall_u_value + + @property + def openable_ratio(self): + """ + Get openable ratio + :return: float + """ + return self._openable_ratio + + @property + def inside_emissivity(self): + """ + Get emissivity inside + :return: float + """ + return self._inside_emissivity + + @property + def alpha_coefficient(self): + """ + Get alpha coefficient + :return: float + """ + return self._alpha_coefficient + + @property + def radiative_coefficient(self): + """ + Get radiative coefficient + :return: float + """ + return self._radiative_coefficient diff --git a/imports/construction/helpers/storeys_generation.py b/imports/construction/helpers/storeys_generation.py index ae89cebb..b67a820d 100644 --- a/imports/construction/helpers/storeys_generation.py +++ b/imports/construction/helpers/storeys_generation.py @@ -1,7 +1,7 @@ """ Storeys generation helper SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +Copyright © 2022 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ import sys import math @@ -22,6 +22,9 @@ class StoreysGeneration: self._building = building self._divide_in_storeys = divide_in_storeys self._storeys = None + self._floor_area = 0 + for ground in building.grounds: + self._floor_area += ground.perimeter_polygon.area @property def storeys(self) -> [Storey]: @@ -34,7 +37,7 @@ class StoreysGeneration: self._building.storeys_above_ground) number_of_storeys = 1 if not self._divide_in_storeys or number_of_storeys == 1: - return [Storey('storey_0', self._building.surfaces, [None, None], self._building.volume)] + return [Storey('storey_0', self._building.surfaces, [None, None], self._building.volume, self._floor_area)] if number_of_storeys == 0: raise Exception('Number of storeys cannot be 0') @@ -79,13 +82,13 @@ class StoreysGeneration: surfaces_child.append(ceiling) volume = ceiling.area_above_ground * height total_volume += volume - storeys.append(Storey(name, surfaces_child, neighbours, volume)) + storeys.append(Storey(name, surfaces_child, neighbours, volume, self._floor_area)) name = 'storey_' + str(number_of_storeys - 1) neighbours = ['storey_' + str(number_of_storeys - 2), None] volume = self._building.volume - total_volume if volume < 0: raise Exception('Error in storeys creation, volume of last storey cannot be lower that 0') - storeys.append(Storey(name, surfaces_child_last_storey, neighbours, volume)) + storeys.append(Storey(name, surfaces_child_last_storey, neighbours, volume, self._floor_area)) return storeys @staticmethod @@ -144,12 +147,12 @@ class StoreysGeneration: """ for storey in self._building.storeys: for thermal_boundary in storey.thermal_boundaries: - if thermal_boundary.surface.type != cte.INTERIOR_WALL or thermal_boundary.surface.type != cte.INTERIOR_SLAB: + if thermal_boundary.type != cte.INTERIOR_WALL or thermal_boundary.type != cte.INTERIOR_SLAB: # external thermal boundary -> only one thermal zone thermal_zones = [storey.thermal_zone] else: # internal thermal boundary -> two thermal zones - grad = np.rad2deg(thermal_boundary.surface.inclination) + grad = np.rad2deg(thermal_boundary.inclination) if grad >= 170: thermal_zones = [storey.thermal_zone, storey.neighbours[0]] else: diff --git a/imports/construction/nrel_physics_interface.py b/imports/construction/nrel_physics_interface.py index f01297b8..9065ca9e 100644 --- a/imports/construction/nrel_physics_interface.py +++ b/imports/construction/nrel_physics_interface.py @@ -6,10 +6,10 @@ Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.mons """ import xmltodict -from imports.construction.data_classes.nrel_building_achetype import NrelBuildingArchetype as nba -from imports.construction.data_classes.nrel_thermal_boundary_archetype import NrelThermalBoundaryArchetype as ntba -from imports.construction.data_classes.nrel_thermal_opening_archetype import NrelThermalOpeningArchetype as ntoa -from imports.construction.data_classes.nrel_layer_archetype import NrelLayerArchetype as nla +from imports.construction.data_classes.building_achetype import BuildingArchetype as nba +from imports.construction.data_classes.thermal_boundary_archetype import ThermalBoundaryArchetype as ntba +from imports.construction.data_classes.thermal_opening_archetype import ThermalOpeningArchetype as ntoa +from imports.construction.data_classes.layer_archetype import LayerArchetype as nla class NrelPhysicsInterface: @@ -71,7 +71,6 @@ class NrelPhysicsInterface: for current_layer in c_lib['layers']['layer']: material_lib = self._search_construction_type('material', current_layer['material']) name = material_lib['@name'] - lca_id = material_lib['@lca_id'] solar_absorptance = material_lib['solar_absorptance']['#text'] thermal_absorptance = material_lib['thermal_absorptance']['#text'] visible_absorptance = material_lib['visible_absorptance']['#text'] @@ -82,7 +81,7 @@ class NrelPhysicsInterface: if units != 'm2 K/W': raise Exception(f'thermal resistance units = {units}, expected m2 K/W') layer = nla(name, solar_absorptance, thermal_absorptance, visible_absorptance, no_mass=no_mass, - thermal_resistance=thermal_resistance, lca_id=lca_id) + thermal_resistance=thermal_resistance) else: thickness = current_layer['thickness']['#text'] units = current_layer['thickness']['@units'] @@ -101,7 +100,7 @@ class NrelPhysicsInterface: if units != 'kg/m3': raise Exception(f'density units = {units}, expected kg/m3') layer = nla(name, solar_absorptance, thermal_absorptance, visible_absorptance, thickness=thickness, - conductivity=conductivity, specific_heat=specific_heat, density=density, lca_id=lca_id) + conductivity=conductivity, specific_heat=specific_heat, density=density) layers.append(layer) thermal_opening = None @@ -150,10 +149,14 @@ class NrelPhysicsInterface: units = c_lib['overall_u_value']['@units'] if units != 'W/m2 K': raise Exception(f'overall U-value units = {units}, expected W/m2 K') - outside_solar_absorptance = c_lib['outside_solar_absorptance']['#text'] - thermal_boundary_archetype = ntba(construction_type, window_ratio, construction_name, layers, - thermal_opening, outside_solar_absorptance=outside_solar_absorptance, - overall_u_value=overall_u_value) + if 'outside_solar_absorptance' in c_lib: + outside_solar_absorptance = c_lib['outside_solar_absorptance']['#text'] + thermal_boundary_archetype = ntba(construction_type, window_ratio, construction_name, layers, + thermal_opening, outside_solar_absorptance=outside_solar_absorptance, + overall_u_value=overall_u_value) + else: + thermal_boundary_archetype = ntba(construction_type, window_ratio, construction_name, layers, + thermal_opening, overall_u_value=overall_u_value) else: thermal_boundary_archetype = ntba(construction_type, window_ratio, construction_name, layers, thermal_opening) diff --git a/imports/construction/us_physics_parameters.py b/imports/construction/us_physics_parameters.py index 76d7b0f4..6e19e96d 100644 --- a/imports/construction/us_physics_parameters.py +++ b/imports/construction/us_physics_parameters.py @@ -41,7 +41,14 @@ class UsPhysicsParameters(NrelPhysicsInterface): f'and building year of construction: {building.year_of_construction}\n') continue - self._create_storeys(building, archetype) + # if building has no thermal zones defined from geometry, one thermal zone per storey is assigned + if building.thermal_zones is None: + self._create_storeys(building, archetype) + thermal_zones = [] + for storey in building.storeys: + thermal_zones.append(storey.thermal_zone) + building.thermal_zones = thermal_zones + self._assign_values(building, archetype) def _search_archetype(self, building_type, standard, climate_zone): @@ -63,12 +70,15 @@ class UsPhysicsParameters(NrelPhysicsInterface): for thermal_boundary in thermal_zone.thermal_boundaries: construction_type = ConstructionHelper.nrel_construction_types[thermal_boundary.type] thermal_boundary_archetype = self._search_construction_in_archetype(archetype, construction_type) - if thermal_boundary_archetype.outside_solar_absorptance is not None: - thermal_boundary.outside_solar_absorptance = thermal_boundary_archetype.outside_solar_absorptance + thermal_boundary.outside_solar_absorptance = thermal_boundary_archetype.outside_solar_absorptance thermal_boundary.outside_thermal_absorptance = thermal_boundary_archetype.outside_thermal_absorptance thermal_boundary.outside_visible_absorptance = thermal_boundary_archetype.outside_visible_absorptance thermal_boundary.construction_name = thermal_boundary_archetype.construction_name - thermal_boundary.window_ratio = thermal_boundary_archetype.window_ratio + try: + thermal_boundary.window_ratio = thermal_boundary_archetype.window_ratio + except ValueError: + # This is the normal operation way when the windows are defined in the geometry + continue thermal_boundary.layers = [] for layer_archetype in thermal_boundary_archetype.layers: layer = Layer() @@ -83,13 +93,11 @@ class UsPhysicsParameters(NrelPhysicsInterface): material.thermal_absorptance = layer_archetype.thermal_absorptance material.visible_absorptance = layer_archetype.visible_absorptance material.thermal_resistance = layer_archetype.thermal_resistance - if layer_archetype.lca_id is not None: - material.lca_id = layer_archetype.lca_id layer.material = material thermal_boundary.layers.append(layer) for thermal_opening in thermal_boundary.thermal_openings: - if thermal_boundary_archetype.thermal_opening is not None: - thermal_opening_archetype = thermal_boundary_archetype.thermal_opening + if thermal_boundary_archetype.thermal_opening_archetype is not None: + thermal_opening_archetype = thermal_boundary_archetype.thermal_opening_archetype thermal_opening.frame_ratio = thermal_opening_archetype.frame_ratio thermal_opening.g_value = thermal_opening_archetype.g_value thermal_opening.conductivity = thermal_opening_archetype.conductivity diff --git a/imports/customized_imports/sanam_customized_usage_parameters.py b/imports/customized_imports/sanam_customized_usage_parameters.py index 6604a8e5..442ec9d3 100644 --- a/imports/customized_imports/sanam_customized_usage_parameters.py +++ b/imports/customized_imports/sanam_customized_usage_parameters.py @@ -7,7 +7,7 @@ Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.mons import sys import xmltodict from imports.geometry.helpers.geometry_helper import GeometryHelper as gh -from imports.usage.data_classes.hft_usage_zone_archetype import HftUsageZoneArchetype as huza +from imports.usage.data_classes.usage_zone_archetype import UsageZoneArchetype as huza import helpers.constants as cte diff --git a/imports/geometry/citygml.py b/imports/geometry/citygml.py index 847c2bef..09803d55 100644 --- a/imports/geometry/citygml.py +++ b/imports/geometry/citygml.py @@ -83,7 +83,7 @@ class CityGml: surfaces = CityGmlLod2(city_object).surfaces else: raise NotImplementedError("Not supported level of detail") - return Building(name, lod, surfaces, year_of_construction, function, self._lower_corner, []) + return Building(name, lod, surfaces, year_of_construction, function, self._lower_corner, terrains=None) def _create_parts_consisting_building(self, city_object): name = city_object['@id'] diff --git a/imports/geometry/obj.py b/imports/geometry/obj.py index 6349921b..3ce1fae3 100644 --- a/imports/geometry/obj.py +++ b/imports/geometry/obj.py @@ -76,6 +76,6 @@ class Obj: perimeter_polygon = solid_polygon surface = Surface(solid_polygon, perimeter_polygon) surfaces.append(surface) - building = Building(name, lod, surfaces, year_of_construction, function, self._lower_corner) + building = Building(name, lod, surfaces, year_of_construction, function, self._lower_corner, terrains=None) self._city.add_city_object(building) return self._city diff --git a/imports/geometry/rhino.py b/imports/geometry/rhino.py index 38c350f0..b9007ecb 100644 --- a/imports/geometry/rhino.py +++ b/imports/geometry/rhino.py @@ -3,16 +3,18 @@ Rhino module parses rhino files and import the geometry into the city model stru SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@concordia.ca """ -import numpy as np from numpy import inf -from rhino3dm import * -from rhino3dm._rhino3dm import MeshType +#from rhino3dm import * +#from rhino3dm._rhino3dm import MeshType +from city_model_structure.attributes.point import Point +import numpy as np +from helpers.configuration_helper import ConfigurationHelper from city_model_structure.attributes.polygon import Polygon from city_model_structure.building import Building -from city_model_structure.building_demand.surface import Surface as LibsSurface from city_model_structure.city import City -from helpers.configuration_helper import ConfigurationHelper +from city_model_structure.building_demand.surface import Surface as LibsSurface +from helpers.constants import EPSILON from imports.geometry.helpers.geometry_helper import GeometryHelper @@ -25,7 +27,19 @@ class Rhino: self._max_x = self._max_y = self._max_z = min_float @staticmethod - def _solid_points(coordinates): + def _in_perimeter(wall, corner): + res = wall.contains_point(Point(corner)) + print(f'belong: {res} wall:({wall.coordinates}) corner: ({corner})') + return res + + @staticmethod + def _add_hole(solid_polygon, hole): + first = solid_polygon.points[0] + points = first + hole.points + solid_polygon.points + return Polygon(points) + + @staticmethod + def _solid_points(coordinates) -> np.ndarray: solid_points = np.fromstring(coordinates, dtype=float, sep=' ') solid_points = GeometryHelper.to_points_matrix(solid_points) return solid_points @@ -82,6 +96,7 @@ class Rhino: windows.append(Polygon(surface.perimeter_polygon.inverse)) else: buildings.append(rhino_object) + print(f'windows: {len(windows)}') # todo: this method will be pretty inefficient for hole in windows: corner = hole.coordinates[0] diff --git a/imports/life_cycle_assessment/lca_machine.py b/imports/life_cycle_assessment/lca_machine.py index dbfe1760..87c35c5c 100644 --- a/imports/life_cycle_assessment/lca_machine.py +++ b/imports/life_cycle_assessment/lca_machine.py @@ -8,7 +8,6 @@ import xmltodict from pathlib import Path from city_model_structure.machine import Machine - class LcaMachine: def __init__(self, city, base_path): self._city = city @@ -17,15 +16,12 @@ class LcaMachine: def enrich(self): self._city.machines = [] - # print(self._base_path) path = Path(self._base_path / 'lca_data.xml').resolve() with open(path) as xml: self._lca = xmltodict.parse(xml.read()) - for machine in self._lca["library"]["machines"]['machine']: - self._city.machines.append(Machine(machine['@id'], machine['@name'], machine['work_efficiency']['#text'], - machine['work_efficiency']['@unit'], - machine['energy_consumption_rate']['#text'], - machine['energy_consumption_rate']['@unit'], - machine['carbon_emission_factor']['#text'], - machine['carbon_emission_factor']['@unit'])) + for machine in self._lca["library"]["machines"]['machine']: + self._city.machines.append(Machine(machine['@id'], machine['@name'], machine['work_efficiency']['#text'], + machine['work_efficiency']['@unit'], machine['energy_consumption_rate']['#text'], + machine['energy_consumption_rate']['@unit'], machine['carbon_emission_factor']['#text'], + machine['carbon_emission_factor']['@unit'])) diff --git a/imports/life_cycle_assessment/lca_material.py b/imports/life_cycle_assessment/lca_material.py index e0081b7c..5044b315 100644 --- a/imports/life_cycle_assessment/lca_material.py +++ b/imports/life_cycle_assessment/lca_material.py @@ -7,8 +7,6 @@ Contributor Mohammad Reza mohammad.seyedabadi@mail.concordia.ca import xmltodict from pathlib import Path from city_model_structure.building_demand.material import Material -from city_model_structure.lca_material import LcaMaterial as LcaMat - class LcaMaterial: def __init__(self, city, base_path): @@ -17,25 +15,26 @@ class LcaMaterial: self._lca = None def enrich(self): - self._city.lca_materials = [] + self._city.materials = [] path = Path(self._base_path / 'lca_data.xml').resolve() with open(path) as xml: self._lca = xmltodict.parse(xml.read()) for material in self._lca["library"]["building_materials"]['material']: - _lca_material = LcaMat() - _lca_material.type = material['@type'] - _lca_material.id = material['@id'] - _lca_material.name = material['@name'] - _lca_material.density = material['density']['#text'] - _lca_material.density_unit = material['density']['@unit'] - _lca_material.embodied_carbon = material['embodied_carbon']['#text'] - _lca_material.embodied_carbon_unit = material['embodied_carbon']['@unit'] - _lca_material.recycling_ratio = material['recycling_ratio'] - _lca_material.onsite_recycling_ratio = material['onsite_recycling_ratio'] - _lca_material.company_recycling_ratio = material['company_recycling_ratio'] - _lca_material.landfilling_ratio = material['landfilling_ratio'] - _lca_material.cost = 10 # todo: change this into material['cost']['#text'] - _lca_material._cost_unit = material['cost']['@unit'] - self._city.lca_materials.append(_lca_material) + _material = Material() + _material.type = material['@type'] + _material.id = material['@id'] + _material.name = material['@name'] + _material.density=material['density']['#text'] + _material.density_unit=material['density']['@unit'] + _material.embodied_carbon=material['embodied_carbon']['#text'] + _material.embodied_carbon_unit=material['embodied_carbon']['@unit'] + _material.recycling_ratio=material['recycling_ratio'] + _material.onsite_recycling_ratio=material['onsite_recycling_ratio'] + _material.company_recycling_ratio=material['company_recycling_ratio'] + _material.landfilling_ratio=material['landfilling_ratio'] + _material.cost=material['cost']['#text'] + _material._cost_unit=material['cost']['@unit'] + + self._city.materials.append(_material) diff --git a/imports/sensors/concordia_energy_consumption.py b/imports/sensors/concordia_energy_consumption.py index 686a4dd3..4f3c7d63 100644 --- a/imports/sensors/concordia_energy_consumption.py +++ b/imports/sensors/concordia_energy_consumption.py @@ -5,6 +5,7 @@ Copyright © 2021 Project Author Guille Gutierrez guillermo.gutierrezmorote@conc """ import pandas as pd from imports.sensors.concordia_file_report import ConcordiaFileReport +from city_model_structure.iot.concordia_energy_sensor import ConcordiaEnergySensor class ConcordiaEnergyConsumption(ConcordiaFileReport): diff --git a/imports/sensors/concordia_gas_flow.py b/imports/sensors/concordia_gas_flow.py index 11dec2e2..e9176fc7 100644 --- a/imports/sensors/concordia_gas_flow.py +++ b/imports/sensors/concordia_gas_flow.py @@ -5,7 +5,7 @@ Copyright © 2021 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.mons """ import pandas as pd from imports.sensors.concordia_file_report import ConcordiaFileReport - +from city_model_structure.iot.concordia_gas_flow_sensor import ConcordiaGasFlowSensor class ConcordiaGasFlow(ConcordiaFileReport): @@ -24,7 +24,6 @@ class ConcordiaGasFlow(ConcordiaFileReport): building_measures = [self._measures["Date time"], self._measures[self._sensor_point[self._sensors[i]]]] building_headers = ["Date time", "Gas Flow Cumulative Monthly"] building_energy_consumption = pd.concat(building_measures, keys=building_headers, axis=1) - """ sensor = ConcordiaGasFlowSensor(self._sensors[i]) sensor_exist = False for j in range(len(obj.sensors)): @@ -35,4 +34,3 @@ class ConcordiaGasFlow(ConcordiaFileReport): if not sensor_exist: sensor.add_period(building_energy_consumption) obj.sensors.append(sensor) - """ \ No newline at end of file diff --git a/imports/sensors/concordia_temperature.py b/imports/sensors/concordia_temperature.py index e1f1cb32..f4b937a8 100644 --- a/imports/sensors/concordia_temperature.py +++ b/imports/sensors/concordia_temperature.py @@ -5,6 +5,7 @@ Copyright © 2021 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.mons """ import pandas as pd from imports.sensors.concordia_file_report import ConcordiaFileReport +from city_model_structure.iot.concordia_temperature_sensor import ConcordiaTemperatureSensor class ConcordiaTemperature(ConcordiaFileReport): @@ -22,7 +23,6 @@ class ConcordiaTemperature(ConcordiaFileReport): building_measures = [self._measures["Date time"], self._measures[self._sensor_point[self._sensors[i]]]] building_headers = ["Date time", "Temperature"] building_energy_consumption = pd.concat(building_measures, keys=building_headers, axis=1) - """ sensor = ConcordiaTemperatureSensor(self._sensors[i]) sensor_exist = False for j in range(len(obj.sensors)): @@ -33,4 +33,3 @@ class ConcordiaTemperature(ConcordiaFileReport): if not sensor_exist: sensor.add_period(building_energy_consumption) obj.sensors.append(sensor) - """ \ No newline at end of file diff --git a/imports/usage/ca_usage_parameters.py b/imports/usage/ca_usage_parameters.py index 01cc85a5..cd921332 100644 --- a/imports/usage/ca_usage_parameters.py +++ b/imports/usage/ca_usage_parameters.py @@ -58,14 +58,14 @@ class CaUsageParameters(HftUsageInterface): # usage_zone.internal_gains when writing usage_zone.internal_gains = archetype.internal_gains. # Therefore, this walk around has been done. internal_gains = [] - for archetype_internal_gain in archetype.internal_gains: + for archetype_internal_gain in archetype.not_detailed_source_mean_annual_internal_gains: internal_gain = InternalGains() internal_gain.average_internal_gain = archetype_internal_gain.average_internal_gain internal_gain.convective_fraction = archetype_internal_gain.convective_fraction internal_gain.radiative_fraction = archetype_internal_gain.radiative_fraction internal_gain.latent_fraction = archetype_internal_gain.latent_fraction internal_gains.append(internal_gain) - usage_zone.internal_gains = internal_gains + usage_zone.not_detailed_source_mean_annual_internal_gains = internal_gains usage_zone.heating_setpoint = archetype.heating_setpoint usage_zone.heating_setback = archetype.heating_setback usage_zone.cooling_setpoint = archetype.cooling_setpoint diff --git a/imports/usage/comnet_usage_parameters.py b/imports/usage/comnet_usage_parameters.py index 5d10312c..0d0a835d 100644 --- a/imports/usage/comnet_usage_parameters.py +++ b/imports/usage/comnet_usage_parameters.py @@ -12,9 +12,10 @@ from helpers.configuration_helper import ConfigurationHelper as ch from imports.geometry.helpers.geometry_helper import GeometryHelper from imports.usage.helpers.usage_helper import UsageHelper from city_model_structure.building_demand.usage_zone import UsageZone +from city_model_structure.building_demand.lighting import Lighting +from city_model_structure.building_demand.occupancy import Occupancy +from city_model_structure.building_demand.appliances import Appliances from city_model_structure.building_demand.internal_gains import InternalGains -from imports.usage.data_classes.hft_usage_zone_archetype import HftUsageZoneArchetype as huza -from imports.usage.data_classes.hft_internal_gains_archetype import HftInternalGainsArchetype as higa class ComnetUsageParameters: @@ -68,45 +69,44 @@ class ComnetUsageParameters: 'process': process_data} @staticmethod - def _parse_zone_usage_type(usage, data): - if data['occupancy'][usage][0] <= 0: - occupancy_density = 0 - else: - occupancy_density = 1 / data['occupancy'][usage][0] - mechanical_air_change = data['ventilation rate'][usage][0] - internal_gains = [] - # lighting - latent_fraction = ch().comnet_lighting_latent - convective_fraction = ch().comnet_lighting_convective - radiative_fraction = ch().comnet_lighting_radiant - average_internal_gain = data['lighting'][usage][4] - internal_gains.append(higa(internal_gains_type=cte.LIGHTING, average_internal_gain=average_internal_gain, - convective_fraction=convective_fraction, radiative_fraction=radiative_fraction, - latent_fraction=latent_fraction)) - # occupancy - latent_fraction = data['occupancy'][usage][2] / (data['occupancy'][usage][1] + data['occupancy'][usage][2]) - sensible_fraction = float(1 - latent_fraction) - convective_fraction = sensible_fraction * ch().comnet_occupancy_sensible_convective - radiative_fraction = sensible_fraction * ch().comnet_occupancy_sensible_radiant - average_internal_gain = (data['occupancy'][usage][1] + data['occupancy'][usage][2]) \ - * occupancy_density * cte.BTU_H_TO_WATTS - internal_gains.append(higa(internal_gains_type=cte.OCCUPANCY, average_internal_gain=average_internal_gain, - convective_fraction=convective_fraction, radiative_fraction=radiative_fraction, - latent_fraction=latent_fraction)) - # plug loads - if data['plug loads'][usage][0] != 'n.a.': - latent_fraction = ch().comnet_plugs_latent - convective_fraction = ch().comnet_plugs_convective - radiative_fraction = ch().comnet_plugs_radiant - average_internal_gain = data['plug loads'][usage][0] - internal_gains.append(higa(internal_gains_type=cte.RECEPTACLE, average_internal_gain=average_internal_gain, - convective_fraction=convective_fraction, radiative_fraction=radiative_fraction, - latent_fraction=latent_fraction)) + def _parse_zone_usage_type(usage, height, data): + _usage_zone = UsageZone() + _usage_zone.usage = usage - usage_zone_archetype = huza(usage=usage, internal_gains=internal_gains, - occupancy_density=occupancy_density, - mechanical_air_change=mechanical_air_change) - return usage_zone_archetype + # lighting + _lighting = Lighting() + _lighting.latent_fraction = ch().comnet_lighting_latent + _lighting.convective_fraction = ch().comnet_lighting_convective + _lighting.radiative_fraction = ch().comnet_lighting_radiant + _lighting.average_internal_gain = data['lighting'][usage][4] + + # plug loads + _appliances = None + if data['plug loads'][usage][0] != 'n.a.': + _appliances = Appliances() + _appliances.latent_fraction = ch().comnet_plugs_latent + _appliances.convective_fraction = ch().comnet_plugs_convective + _appliances.radiative_fraction = ch().comnet_plugs_radiant + _appliances.average_internal_gain = data['plug loads'][usage][0] + + # occupancy + _occupancy = Occupancy() + _occupancy.occupancy_density = data['occupancy'][usage][0] * cte.METERS_TO_FEET**2 + _occupancy.sensible_convective_internal_gain = data['occupancy'][usage][1] \ + * ch().comnet_occupancy_sensible_convective + _occupancy.sensible_radiant_internal_gain = data['occupancy'][usage][1] * ch().comnet_occupancy_sensible_radiant + _occupancy.latent_internal_gain = data['occupancy'][usage][2] + + if _occupancy.occupancy_density <= 0: + _usage_zone.mechanical_air_change = 0 + else: + _usage_zone.mechanical_air_change = data['ventilation rate'][usage][0] / _occupancy.occupancy_density \ + * cte.HOUR_TO_MINUTES / cte.METERS_TO_FEET**3 / height + + _usage_zone.occupancy = _occupancy + _usage_zone.lighting = _lighting + _usage_zone.appliances = _appliances + return _usage_zone def enrich_buildings(self): """ @@ -151,12 +151,11 @@ class ComnetUsageParameters: for archetype_internal_gain in archetype.internal_gains: internal_gain = InternalGains() internal_gain.type = archetype_internal_gain.type - internal_gain.average_internal_gain = archetype_internal_gain.average_internal_gain * cte.METERS_TO_FEET**2 + internal_gain.average_internal_gain = archetype_internal_gain.average_internal_gain internal_gain.convective_fraction = archetype_internal_gain.convective_fraction internal_gain.radiative_fraction = archetype_internal_gain.radiative_fraction internal_gain.latent_fraction = archetype_internal_gain.latent_fraction internal_gains.append(internal_gain) usage_zone.internal_gains = internal_gains - usage_zone.occupancy_density = archetype.occupancy_density * cte.METERS_TO_FEET**2 - usage_zone.mechanical_air_change = archetype.mechanical_air_change * usage_zone.occupancy_density \ - * cte.HOUR_TO_MINUTES / cte.METERS_TO_FEET**3 / height + usage_zone.occupancy_density = archetype.occupancy_density + usage_zone.mechanical_air_change = archetype.mechanical_air_change \ No newline at end of file diff --git a/imports/usage/data_classes/hft_usage_zone_archetype.py b/imports/usage/data_classes/hft_usage_zone_archetype.py deleted file mode 100644 index b945e9c9..00000000 --- a/imports/usage/data_classes/hft_usage_zone_archetype.py +++ /dev/null @@ -1,144 +0,0 @@ -""" -HftUsageZoneArchetype stores usage information by building archetypes -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca -""" -from typing import List -from imports.usage.data_classes.hft_internal_gains_archetype import HftInternalGainsArchetype - - -class HftUsageZoneArchetype: - """ - HftUsageZoneArchetype class - """ - def __init__(self, usage=None, internal_gains=None, heating_set_point=None, heating_set_back=None, - cooling_set_point=None, occupancy_density=None, hours_day=None, days_year=None, - dhw_average_volume_pers_day=None, dhw_preparation_temperature=None, - electrical_app_average_consumption_sqm_year=None, mechanical_air_change=None, - occupancy=None, schedules=None): - self._usage = usage - self._internal_gains = internal_gains - self._heating_setpoint = heating_set_point - self._heating_setback = heating_set_back - self._cooling_setpoint = cooling_set_point - self._occupancy_density = occupancy_density - self._hours_day = hours_day - self._days_year = days_year - self._dhw_average_volume_pers_day = dhw_average_volume_pers_day - self._dhw_preparation_temperature = dhw_preparation_temperature - self._electrical_app_average_consumption_sqm_year = electrical_app_average_consumption_sqm_year - self._mechanical_air_change = mechanical_air_change - self._occupancy = occupancy - self._schedules = schedules - - @property - def internal_gains(self) -> List[HftInternalGainsArchetype]: - """ - Get usage zone internal gains - :return: [InternalGains] - """ - return self._internal_gains - - @property - def heating_setpoint(self): - """ - Get usage zone heating set point in celsius grads - :return: float - """ - return self._heating_setpoint - - @property - def heating_setback(self): - """ - Get usage zone heating setback in celsius grads - :return: float - """ - return self._heating_setback - - @property - def cooling_setpoint(self): - """ - Get usage zone cooling setpoint in celsius grads - :return: float - """ - return self._cooling_setpoint - - @property - def hours_day(self): - """ - Get usage zone usage hours per day - :return: float - """ - return self._hours_day - - @property - def days_year(self): - """ - Get usage zone usage days per year - :return: float - """ - return self._days_year - - @property - def mechanical_air_change(self): - """ - Set usage zone mechanical air change in air change per hour (ACH) - :return: float - """ - return self._mechanical_air_change - - @property - def usage(self): - """ - Get usage zone usage - :return: str - """ - return self._usage - - @property - def occupancy(self): - """ - Get schedules data - :return: [Occupancy] - """ - return self._occupancy - - @property - def schedules(self): - """ - Get schedules - :return: [Schedule_Values] - """ - return self._schedules - - @property - def occupancy_density(self): - """ - Get schedules density in persons per m2 - :return: float - """ - return self._occupancy_density - - @property - def dhw_average_volume_pers_day(self): - """ - Get average DHW consumption in m3 per person per day - :return: float - """ - return self._dhw_average_volume_pers_day - - @property - def dhw_preparation_temperature(self): - """ - Get preparation temperature of the DHW in degree Celsius - :return: float - """ - return self._dhw_preparation_temperature - - @property - def electrical_app_average_consumption_sqm_year(self): - """ - Get average consumption of electrical appliances in Joules per m2 and year (J/m2yr) - :return: float - """ - return self._electrical_app_average_consumption_sqm_year diff --git a/imports/usage/data_classes/usage_zone_archetype.py b/imports/usage/data_classes/usage_zone_archetype.py new file mode 100644 index 00000000..67d1f964 --- /dev/null +++ b/imports/usage/data_classes/usage_zone_archetype.py @@ -0,0 +1,98 @@ +""" +UsageZoneArchetype stores usage information by usage type +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +""" + +from typing import List +from imports.usage.data_classes.hft_internal_gains_archetype import HftInternalGainsArchetype + + +class UsageZoneArchetype: + """ + UsageZoneArchetype class + """ + def __init__(self, usage=None, not_detailed_source_mean_annual_internal_gains=None, hours_day=None, days_year=None, + electrical_app_average_consumption_sqm_year=None, mechanical_air_change=None, occupancy=None, + lighting=None, appliances=None): + self._usage = usage + self._not_detailed_source_mean_annual_internal_gains = not_detailed_source_mean_annual_internal_gains + self._hours_day = hours_day + self._days_year = days_year + self._electrical_app_average_consumption_sqm_year = electrical_app_average_consumption_sqm_year + self._mechanical_air_change = mechanical_air_change + self._occupancy = occupancy + self._lighting = lighting + self._appliances = appliances + + @property + def not_detailed_source_mean_annual_internal_gains(self) -> List[HftInternalGainsArchetype]: + """ + Get usage zone internal gains from not detailed heating source in W/m2 + :return: [InternalGains] + """ + return self._not_detailed_source_mean_annual_internal_gains + + @property + def hours_day(self): + """ + Get usage zone usage hours per day + :return: float + """ + return self._hours_day + + @property + def days_year(self): + """ + Get usage zone usage days per year + :return: float + """ + return self._days_year + + @property + def mechanical_air_change(self): + """ + Set usage zone mechanical air change in air change per hour (ACH) + :return: float + """ + return self._mechanical_air_change + + @property + def usage(self): + """ + Get usage zone usage + :return: str + """ + return self._usage + + @property + def electrical_app_average_consumption_sqm_year(self): + """ + Get average consumption of electrical appliances in Joules per m2 and year (J/m2yr) + :return: float + """ + return self._electrical_app_average_consumption_sqm_year + + @property + def occupancy(self): + """ + Get occupancy data + :return: Occupancy + """ + return self._occupancy + + @property + def lighting(self): + """ + Get lighting data + :return: Lighting + """ + return self._lighting + + @property + def appliances(self): + """ + Get appliances data + :return: Appliances + """ + return self._appliances diff --git a/imports/usage/hft_usage_interface.py b/imports/usage/hft_usage_interface.py index 3da6457d..ed6dfefc 100644 --- a/imports/usage/hft_usage_interface.py +++ b/imports/usage/hft_usage_interface.py @@ -5,7 +5,7 @@ SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ import xmltodict -from imports.usage.data_classes.hft_usage_zone_archetype import HftUsageZoneArchetype as huza +from imports.usage.data_classes.usage_zone_archetype import UsageZoneArchetype as huza from imports.usage.data_classes.hft_internal_gains_archetype import HftInternalGainsArchetype as higa @@ -96,7 +96,7 @@ class HftUsageInterface: # todo: for internal_gain in usage_zone_variant['schedules']['internGains']:???????????????? # There are no more internal gains? How is it saved when more than one??? # for internal_gain in usage_zone.internal_gains: - internal_gains = usage_zone.internal_gains[0] + internal_gains = usage_zone.not_detailed_source_mean_annual_internal_gains[0] latent_fraction = internal_gains.latent_fraction convective_fraction = internal_gains.convective_fraction average_internal_gain = internal_gains.average_internal_gain diff --git a/imports/usage/hft_usage_parameters.py b/imports/usage/hft_usage_parameters.py index fc254aa2..d2deeeae 100644 --- a/imports/usage/hft_usage_parameters.py +++ b/imports/usage/hft_usage_parameters.py @@ -58,14 +58,14 @@ class HftUsageParameters(HftUsageInterface): # usage_zone.internal_gains when writing usage_zone.internal_gains = archetype.internal_gains. # Therefore, this walk around has been done. internal_gains = [] - for archetype_internal_gain in archetype.internal_gains: + for archetype_internal_gain in archetype.not_detailed_source_mean_annual_internal_gains: internal_gain = InternalGains() internal_gain.average_internal_gain = archetype_internal_gain.average_internal_gain internal_gain.convective_fraction = archetype_internal_gain.convective_fraction internal_gain.radiative_fraction = archetype_internal_gain.radiative_fraction internal_gain.latent_fraction = archetype_internal_gain.latent_fraction internal_gains.append(internal_gain) - usage_zone.internal_gains = internal_gains + usage_zone.not_detailed_source_mean_annual_internal_gains = internal_gains usage_zone.heating_setpoint = archetype.heating_setpoint usage_zone.heating_setback = archetype.heating_setback usage_zone.cooling_setpoint = archetype.cooling_setpoint diff --git a/requirements.txt b/requirements.txt index 5329b459..cce571d9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ openpyxl~=3.0.7 networkx~=2.5.1 parseidf~=1.0.0 ply~=3.11 -rhino3dm~=7.11.1 +rhino3dm~=7.7.0 scipy==1.7.1 PyYAML==6.0 yaml~=0.2.5 diff --git a/unittests/test_construction_factory.py b/unittests/test_construction_factory.py index 03266f95..e0ece40a 100644 --- a/unittests/test_construction_factory.py +++ b/unittests/test_construction_factory.py @@ -1,15 +1,16 @@ """ TestConstructionFactory test and validate the city model structure construction parameters SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +Copyright © 2022 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ from pathlib import Path from unittest import TestCase +import helpers.constants as cte from imports.geometry_factory import GeometryFactory from imports.construction_factory import ConstructionFactory from imports.geometry.helpers.geometry_helper import GeometryHelper -from imports.life_cycle_assessment_factory import LifeCycleAssessment + class TestConstructionFactory(TestCase): """ @@ -23,46 +24,138 @@ class TestConstructionFactory(TestCase): self._city = None self._example_path = (Path(__file__).parent / 'tests_data').resolve() - def _get_citygml(self, file=None): - if file is None: - file = 'pluto_building.gml' + def _get_citygml(self, file): file_path = (self._example_path / file).resolve() self._city = GeometryFactory('citygml', file_path).city - LifeCycleAssessment('material', self._city).enrich() self.assertIsNotNone(self._city, 'city is none') return self._city - def test_city_with_construction_extended_library(self): - """ - Enrich the city with the physic information and verify it - :return: None - """ - - city = self._get_citygml() + def _check_buildings(self, city): for building in city.buildings: - building.function = GeometryHelper.pluto_to_function[building.function] - self.assertIsNotNone(building.surfaces, 'Building has no surfaces') + self.assertIsNotNone(building.name, 'building name is none') + self.assertIsNotNone(building.lod, 'building lod is none') + self.assertIsNotNone(building.type, 'building type is none') + self.assertIsNotNone(building.volume, 'building volume is none') + self.assertIsNotNone(building.detailed_polyhedron, 'building detailed polyhedron is none') + self.assertIsNotNone(building.simplified_polyhedron, 'building simplified polyhedron is none') + self.assertIsNotNone(building.surfaces, 'building surfaces is none') + self.assertIsNotNone(building.centroid, 'building centroid is none') + self.assertIsNotNone(building.max_height, 'building max_height is none') + self.assertEqual(len(building.external_temperature), 0, 'building external temperature is calculated') + self.assertEqual(len(building.global_horizontal), 0, 'building global horizontal is calculated') + self.assertEqual(len(building.diffuse), 0, 'building diffuse is calculated') + self.assertEqual(len(building.beam), 0, 'building beam is calculated') + self.assertIsNotNone(building.lower_corner, 'building lower corner is none') + self.assertEqual(len(building.sensors), 0, 'building sensors are assigned') + self.assertIsNotNone(building.geometrical_zones, 'no geometrical zones created') + self.assertIsNotNone(building.grounds, 'building grounds is none') + self.assertIsNotNone(building.walls, 'building walls is none') + self.assertIsNotNone(building.roofs, 'building roofs is none') + self.assertIsNone(building.usage_zones, 'usage zones are defined') + self.assertTrue(len(building.thermal_zones) > 0, 'thermal zones are not defined') + self.assertIsNone(building.basement_heated, 'building basement_heated is not none') + self.assertIsNone(building.attic_heated, 'building attic_heated is not none') + self.assertIsNone(building.terrains, 'building terrains is not none') + self.assertIsNotNone(building.year_of_construction, 'building year_of_construction is none') + self.assertIsNotNone(building.function, 'building function is none') + self.assertIsNotNone(building.average_storey_height, 'building average_storey_height is none') + self.assertIsNotNone(building.storeys_above_ground, 'building storeys_above_ground is none') + self.assertEqual(len(building.heating), 0, 'building heating is not none') + self.assertEqual(len(building.cooling), 0, 'building cooling is not none') + self.assertIsNotNone(building.eave_height, 'building eave height is none') + self.assertIsNotNone(building.storeys, 'building storeys are not defined') + self.assertIsNotNone(building.roof_type, 'building roof type is none') + self.assertIsNotNone(building.floor_area, 'building floor_area is none') + self.assertIsNone(building.households, 'building households is not none') + self.assertFalse(building.is_conditioned, 'building is conditioned') - # case 1: NREL - ConstructionFactory('nrel', city).enrich() + def _check_thermal_zones(self, building): + for thermal_zone in building.thermal_zones: + self.assertIsNotNone(thermal_zone.id, 'thermal_zone id is none') + self.assertIsNotNone(thermal_zone.floor_area, 'thermal_zone floor area is none') + self.assertTrue(len(thermal_zone.thermal_boundaries) > 0, 'thermal_zone thermal_boundaries not defined') + self.assertIsNotNone(thermal_zone.additional_thermal_bridge_u_value, 'additional_thermal_bridge_u_value is none') + self.assertIsNotNone(thermal_zone.effective_thermal_capacity, 'thermal_zone effective_thermal_capacity is none') + self.assertIsNotNone(thermal_zone.indirectly_heated_area_ratio, + 'thermal_zone indirectly_heated_area_ratio is none') + self.assertIsNotNone(thermal_zone.infiltration_rate_system_off, + 'thermal_zone infiltration_rate_system_off is none') + self.assertIsNotNone(thermal_zone.infiltration_rate_system_on, 'thermal_zone infiltration_rate_system_on is none') + self.assertIsNone(thermal_zone.usage_zones, 'thermal_zone usage_zones is not none') + self.assertIsNotNone(thermal_zone.volume, 'thermal_zone volume is none') + self.assertIsNone(thermal_zone.ordinate_number, 'thermal_zone ordinate number is not none') + self.assertIsNone(thermal_zone.thermal_control, 'thermal_zone thermal_control is not none') + self.assertIsNone(thermal_zone.hvac_system, 'thermal_zone hvac_system is not none') + + def _check_thermal_boundaries(self, thermal_zone): + for thermal_boundary in thermal_zone.thermal_boundaries: + self.assertIsNotNone(thermal_boundary.id, 'thermal_boundary id is none') + self.assertIsNotNone(thermal_boundary.parent_surface, 'thermal_boundary surface is none') + self.assertIsNotNone(thermal_boundary.thermal_zones, 'thermal_boundary delimits is none') + self.assertIsNotNone(thermal_boundary.opaque_area, 'thermal_boundary area is none') + self.assertIsNotNone(thermal_boundary.azimuth, 'thermal_boundary azimuth is none') + self.assertIsNotNone(thermal_boundary.inclination, 'thermal_boundary inclination is none') + self.assertIsNotNone(thermal_boundary.thickness, 'thermal_boundary thickness is none') + self.assertIsNotNone(thermal_boundary.type, 'thermal_boundary type is none') + if thermal_boundary.type is not cte.GROUND: + self.assertIsNotNone(thermal_boundary.outside_solar_absorptance, 'outside_solar_absorptance is none') + self.assertIsNotNone(thermal_boundary.shortwave_reflectance, 'shortwave_reflectance is none') + else: + self.assertIsNone(thermal_boundary.outside_solar_absorptance, 'outside_solar_absorptance is not none') + self.assertIsNone(thermal_boundary.shortwave_reflectance, 'shortwave_reflectance is not none') + self.assertIsNotNone(thermal_boundary.thermal_openings, 'thermal_openings is none') + self.assertIsNotNone(thermal_boundary.construction_name, 'construction_name is none') + self.assertIsNotNone(thermal_boundary.window_ratio, 'window_ratio is none') + self.assertIsNone(thermal_boundary.windows_areas, 'windows_areas is not none') + self.assertIsNotNone(thermal_boundary.u_value, 'u_value is none') + self.assertIsNotNone(thermal_boundary.hi, 'hi is none') + self.assertIsNotNone(thermal_boundary.he, 'he is none') + self.assertIsNotNone(thermal_boundary.virtual_internal_surface, 'virtual_internal_surface is none') + self.assertIsNone(thermal_boundary.inside_emissivity, 'inside_emissivity is not none') + self.assertIsNone(thermal_boundary.alpha_coefficient, 'alpha_coefficient is not none') + self.assertIsNone(thermal_boundary.radiative_coefficient, 'radiative_coefficient is not none') + + def _check_thermal_openings(self, thermal_boundary): + for thermal_opening in thermal_boundary.thermal_openings: + self.assertIsNotNone(thermal_opening.id, 'thermal opening id is not none') + self.assertIsNotNone(thermal_opening.area, 'thermal opening area is not none') + self.assertRaises(Exception, lambda: thermal_opening.openable_ratio, + 'thermal_opening openable_ratio is not raising an exception') + self.assertIsNotNone(thermal_opening.frame_ratio, 'thermal opening frame_ratio is not none') + self.assertIsNotNone(thermal_opening.g_value, 'thermal opening g_value is not none') + self.assertIsNotNone(thermal_opening.overall_u_value, 'thermal opening overall_u_value is not none') + self.assertIsNotNone(thermal_opening.hi, 'thermal opening hi is not none') + self.assertIsNotNone(thermal_opening.he, 'thermal opening he is not none') + self.assertIsNone(thermal_opening.inside_emissivity, 'thermal opening inside_emissivity is not none') + self.assertIsNone(thermal_opening.alpha_coefficient, 'thermal opening alpha_coefficient is not none') + self.assertIsNone(thermal_opening.radiative_coefficient, 'thermal opening radiative_coefficient is not none') + + def test_citygml_function(self): + """ + Test city objects' functions in the city + """ + # case 1: hft + file = 'one_building_in_kelowna.gml' + function_format = 'hft' + city = self._get_citygml(file) for building in city.buildings: - self.assertIsNotNone(building.average_storey_height, 'average_storey_height is none') - self.assertIsNotNone(building.storeys_above_ground, 'storeys_above_ground is none') - self.assertIsNot(len(building.thermal_zones), 0, 'no building thermal_zones defined') - for thermal_zone in building.thermal_zones: - self.assertIsNotNone(thermal_zone.effective_thermal_capacity, 'effective_thermal_capacity is none') - self.assertIsNotNone(thermal_zone.additional_thermal_bridge_u_value, - 'additional_thermal_bridge_u_value is none') - self.assertIsNotNone(thermal_zone.indirectly_heated_area_ratio, 'indirectly_heated_area_ratio is none') - self.assertIsNotNone(thermal_zone.infiltration_rate_system_on, 'infiltration_rate_system_on is none') - self.assertIsNotNone(thermal_zone.infiltration_rate_system_off, 'infiltration_rate_system_off is none') - self.assertIsNotNone(thermal_zone.thermal_boundaries, 'thermal_boundaries is none') - for thermal_boundary in thermal_zone.thermal_boundaries: - if thermal_boundary.surface.type != 'Ground': - self.assertIsNotNone(thermal_boundary.outside_solar_absorptance, 'outside_solar_absorptance is none') - self.assertIsNotNone(thermal_boundary.window_ratio, 'window_ratio is none') - for layer in thermal_boundary.layers: - self.assertTrue(city.lca_material(layer.material.lca_id) is not None) + building.function = self._internal_function(function_format, building.function) + self.assertEqual(building.function, 'residential', 'format hft') + + # case 2: Pluto + file = 'pluto_building.gml' + function_format = 'pluto' + city = self._get_citygml(file) + for building in city.buildings: + building.function = self._internal_function(function_format, building.function) + self.assertEqual(building.function, 'secondary school', 'format pluto') + + # case 3: Alkis + file = 'one_building_in_kelowna_alkis.gml' + function_format = 'alkis' + city = self._get_citygml(file) + for building in city.buildings: + self.assertRaises(Exception, lambda: self._internal_function(function_format, building.function)) def test_city_with_construction_reduced_library(self): """ @@ -72,21 +165,69 @@ class TestConstructionFactory(TestCase): city = self._get_citygml(file) for building in city.buildings: building.function = GeometryHelper.hft_to_function[building.function] - # case 2: NRCAN ConstructionFactory('nrcan', city).enrich() + + self._check_buildings(city) for building in city.buildings: - self.assertIsNotNone(building.average_storey_height, 'average_storey_height is none') - self.assertIsNotNone(building.storeys_above_ground, 'storeys_above_ground is none') - self.assertIsNot(len(building.thermal_zones), 0, 'no building thermal_zones defined') + self._check_thermal_zones(building) for thermal_zone in building.thermal_zones: - self.assertIsNotNone(thermal_zone.effective_thermal_capacity, 'effective_thermal_capacity is none') - self.assertIsNotNone(thermal_zone.additional_thermal_bridge_u_value, - 'additional_thermal_bridge_u_value is none') - self.assertIsNotNone(thermal_zone.indirectly_heated_area_ratio, 'indirectly_heated_area_ratio is none') - self.assertIsNotNone(thermal_zone.infiltration_rate_system_on, 'infiltration_rate_system_on is none') - self.assertIsNotNone(thermal_zone.infiltration_rate_system_off, 'infiltration_rate_system_off is none') - self.assertIsNotNone(thermal_zone.thermal_boundaries, 'thermal_boundaries is none') - self.assertIsNot(len(thermal_zone.thermal_boundaries), 0, 'no boundaries of thermal_zone defined') + self._check_thermal_boundaries(thermal_zone) for thermal_boundary in thermal_zone.thermal_boundaries: - self.assertIsNotNone(thermal_boundary.outside_solar_absorptance, 'outside_solar_absorptance is none') - self.assertIsNotNone(thermal_boundary.window_ratio, 'window_ratio is none') + self.assertIsNone(thermal_boundary.outside_thermal_absorptance, 'outside_thermal_absorptance is not none') + self.assertIsNone(thermal_boundary.outside_visible_absorptance, 'outside_visible_absorptance is not none') + self.assertIsNone(thermal_boundary.layers, 'layers is not none') + + self._check_thermal_openings(thermal_boundary) + for thermal_opening in thermal_boundary.thermal_openings: + self.assertIsNone(thermal_opening.conductivity, 'thermal_opening conductivity is not none') + self.assertIsNone(thermal_opening.thickness, 'thermal opening thickness is not none') + self.assertIsNone(thermal_opening.front_side_solar_transmittance_at_normal_incidence, + 'thermal opening front_side_solar_transmittance_at_normal_incidence is not none') + self.assertIsNone(thermal_opening.back_side_solar_transmittance_at_normal_incidence, + 'thermal opening back_side_solar_transmittance_at_normal_incidence is not none') + + def test_city_with_construction_extended_library(self): + """ + Enrich the city with the construction information and verify it + """ + file = 'pluto_building.gml' + city = self._get_citygml(file) + for building in city.buildings: + building.function = GeometryHelper.pluto_to_function[building.function] + ConstructionFactory('nrel', city).enrich() + + self._check_buildings(city) + for building in city.buildings: + self._check_thermal_zones(building) + for thermal_zone in building.thermal_zones: + self._check_thermal_boundaries(thermal_zone) + for thermal_boundary in thermal_zone.thermal_boundaries: + if thermal_boundary.type is not cte.GROUND: + self.assertIsNotNone(thermal_boundary.outside_thermal_absorptance, 'outside_thermal_absorptance is none') + self.assertIsNotNone(thermal_boundary.outside_visible_absorptance, 'outside_visible_absorptance is none') + else: + self.assertIsNone(thermal_boundary.outside_thermal_absorptance, 'outside_thermal_absorptance is not none') + self.assertIsNone(thermal_boundary.outside_visible_absorptance, 'outside_visible_absorptance is not none') + self.assertIsNotNone(thermal_boundary.layers, 'layers is none') + + self._check_thermal_openings(thermal_boundary) + for thermal_opening in thermal_boundary.thermal_openings: + self.assertIsNotNone(thermal_opening.conductivity, 'thermal_opening conductivity is none') + self.assertIsNotNone(thermal_opening.thickness, 'thermal opening thickness is none') + self.assertIsNotNone(thermal_opening.front_side_solar_transmittance_at_normal_incidence, + 'thermal opening front_side_solar_transmittance_at_normal_incidence is none') + self.assertIsNotNone(thermal_opening.back_side_solar_transmittance_at_normal_incidence, + 'thermal opening back_side_solar_transmittance_at_normal_incidence is none') + + @staticmethod + def _internal_function(function_format, original_function): + if function_format == 'hft': + new_function = GeometryHelper.hft_to_function[original_function] + elif function_format == 'pluto': + new_function = GeometryHelper.pluto_to_function[original_function] + elif function_format == 'alkis': + # todo: not implemented yet!! + raise NotImplementedError + else: + raise Exception('Function key not recognized. Implemented only "hft" and "pluto"') + return new_function diff --git a/unittests/test_geometry_factory.py b/unittests/test_geometry_factory.py index 77d17256..b9142591 100644 --- a/unittests/test_geometry_factory.py +++ b/unittests/test_geometry_factory.py @@ -6,8 +6,6 @@ Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.mons from pathlib import Path from unittest import TestCase from imports.geometry_factory import GeometryFactory -from imports.geometry.helpers.geometry_helper import GeometryHelper -from city_model_structure.building_demand.thermal_opening import ThermalOpening class TestGeometryFactory(TestCase): @@ -36,20 +34,65 @@ class TestGeometryFactory(TestCase): self.assertIsNotNone(self._city, 'city is none') return self._city - @staticmethod - def _internal_function(function_format, original_function): - new_function = original_function - if function_format == 'hft': - new_function = GeometryHelper.hft_to_function[original_function] - elif function_format == 'pluto': - new_function = GeometryHelper.pluto_to_function[original_function] - elif function_format == 'alkis': - # todo: not implemented yet!! - raise NotImplementedError - return new_function + def _check_buildings(self, city): + for building in city.buildings: + self.assertIsNotNone(building.name, 'building name is none') + self.assertIsNotNone(building.lod, 'building lod is none') + self.assertIsNotNone(building.type, 'building type is none') + self.assertIsNotNone(building.volume, 'building volume is none') + self.assertIsNotNone(building.detailed_polyhedron, 'building detailed polyhedron is none') + self.assertIsNotNone(building.simplified_polyhedron, 'building simplified polyhedron is none') + self.assertIsNotNone(building.surfaces, 'building surfaces is none') + self.assertIsNotNone(building.centroid, 'building centroid is none') + self.assertIsNotNone(building.max_height, 'building max_height is none') + self.assertEqual(len(building.external_temperature), 0, 'building external temperature is calculated') + self.assertEqual(len(building.global_horizontal), 0, 'building global horizontal is calculated') + self.assertEqual(len(building.diffuse), 0, 'building diffuse is calculated') + self.assertEqual(len(building.beam), 0, 'building beam is calculated') + self.assertIsNotNone(building.lower_corner, 'building lower corner is none') + self.assertEqual(len(building.sensors), 0, 'building sensors are assigned') + self.assertIsNotNone(building.geometrical_zones, 'no geometrical zones created') + self.assertIsNotNone(building.grounds, 'building grounds is none') + self.assertIsNotNone(building.walls, 'building walls is none') + self.assertIsNotNone(building.roofs, 'building roofs is none') + self.assertIsNone(building.usage_zones, 'usage zones are defined') + self.assertIsNone(building.thermal_zones, 'thermal zones are defined') + self.assertIsNone(building.basement_heated, 'building basement_heated is not none') + self.assertIsNone(building.attic_heated, 'building attic_heated is not none') + self.assertIsNone(building.terrains, 'building terrains is not none') + self.assertIsNotNone(building.year_of_construction, 'building year_of_construction is none') + self.assertIsNotNone(building.function, 'building function is none') + self.assertIsNone(building.average_storey_height, 'building average_storey_height is not none') + self.assertIsNone(building.storeys_above_ground, 'building storeys_above_ground is not none') + self.assertEqual(len(building.heating), 0, 'building heating is not none') + self.assertEqual(len(building.cooling), 0, 'building cooling is not none') + self.assertIsNotNone(building.eave_height, 'building eave height is none') + self.assertIsNone(building.storeys, 'building storeys are defined') + self.assertIsNotNone(building.roof_type, 'building roof type is none') + self.assertIsNotNone(building.floor_area, 'building floor_area is none') + self.assertIsNone(building.households, 'building households is not none') + self.assertFalse(building.is_conditioned, 'building is_conditioned is conditioned') + + def _check_surfaces(self, building): + for surface in building.surfaces: + self.assertIsNotNone(surface.name, 'surface name is none') + self.assertIsNotNone(surface.id, 'surface id is none') + self.assertIsNone(surface.swr, 'surface swr is not none') + self.assertIsNotNone(surface.lower_corner, 'surface envelope_lower_corner is none') + self.assertIsNotNone(surface.upper_corner, 'surface envelope_upper_corner is none') + self.assertIsNotNone(surface.area_below_ground, 'surface area_below_ground is none') + self.assertIsNotNone(surface.area_above_ground, 'surface area_above_ground is none') + self.assertIsNotNone(surface.azimuth, 'surface azimuth is none') + self.assertIsNotNone(surface.type, 'surface type is none') + self.assertIsNotNone(surface.inclination, 'surface inclination is none') + self.assertEqual(len(surface.global_irradiance), 0, 'global irradiance is calculated') + self.assertIsNotNone(surface.perimeter_polygon, 'surface perimeter_polygon is none') + self.assertIsNone(surface.holes_polygons, 'surface hole_polygons is not none') + self.assertIsNotNone(surface.solid_polygon, 'surface solid_polygon is none') + self.assertIsNone(surface.pv_system_installed, 'surface PV system installed is not none') # citygml_classes - def test_citygml_buildings(self): + def test_import_citygml(self): """ Test city objects in the city :return: None @@ -57,175 +100,9 @@ class TestGeometryFactory(TestCase): file = 'one_building_in_kelowna.gml' city = self._get_citygml(file) self.assertTrue(len(city.buildings) == 1) + self._check_buildings(city) for building in city.buildings: - self.assertIsNotNone(building.name, 'building name is none') - self.assertIsNotNone(building.lod, 'building lod is none') - self.assertIsNotNone(building.centroid, 'building centroid is none') - self.assertIsNotNone(building.year_of_construction, 'building year_of_construction is none') - self.assertIsNotNone(building.function, 'building function is none') - self.assertIsNotNone(building.volume, 'building volume is none') - self.assertIsNotNone(building.surfaces, 'building surfaces is none') - self.assertIsNotNone(building.surfaces[0].name, 'surface not found') - self.assertIsNone(building.basement_heated, 'building basement_heated is not none') - self.assertIsNone(building.attic_heated, 'building attic_heated is not none') - self.assertIsNotNone(building.terrains, 'building terrains is none') - self.assertIsNone(building.average_storey_height, 'building average_storey_height is not none') - self.assertIsNotNone(building.type, 'building type is none') - self.assertIsNotNone(building.max_height, 'building max_height is none') - self.assertIsNotNone(building.floor_area, 'building floor_area is none') - - def test_citygml_surfaces(self): - """ - Test surfaces in city objects - :return: None - """ - file = 'one_building_in_kelowna.gml' - city = self._get_citygml(file) - for building in city.buildings: - for surface in building.surfaces: - self.assertIsNotNone(surface.name, 'surface name is none') - self.assertIsNotNone(surface.type, 'surface type is none') - self.assertIsNotNone(surface.azimuth, 'surface azimuth is none') - self.assertIsNotNone(surface.inclination, 'surface inclination is none') - self.assertIsNotNone(surface.area_below_ground, 'surface area_below_ground is none') - self.assertIsNotNone(surface.area_above_ground, 'surface area_above_ground is none') - self.assertIsNotNone(surface.global_irradiance, 'monthly irradiance is none') - self.assertIsNone(surface.swr, 'surface swr is not none') - self.assertIsNotNone(surface.lower_corner, 'surface envelope_lower_corner is none') - self.assertIsNotNone(surface.upper_corner, 'surface envelope_upper_corner is none') - self.assertIsNotNone(surface.area_above_ground, 'surface area_above_ground is none') - self.assertIsNotNone(surface.perimeter_polygon, 'surface perimeter_polygon is none') - self.assertIsNone(surface.holes_polygons, 'surface hole_polygons is not none') - self.assertIsNotNone(surface.solid_polygon, 'surface solid_polygon is none') - - def test_citygml_thermal_zone(self): - """ - Test thermal zones in city objects - :return: None - """ - file = 'one_building_in_kelowna.gml' - city = self._get_citygml(file) - for building in city.buildings: - self.assertIsNot(len(building.thermal_zones), 0, 'no building thermal_zones defined') - for thermal_zone in building.thermal_zones: - self.assertIsNotNone(thermal_zone.thermal_boundaries, 'thermal_zone thermal_boundaries is none') - self.assertIsNotNone(thermal_zone.floor_area, 'thermal_zone floor_area is none') - self.assertIsNone(thermal_zone.additional_thermal_bridge_u_value, - 'thermal_zone additional_thermal_bridge_u_value is not none') - self.assertIsNone(thermal_zone.effective_thermal_capacity, - 'thermal_zone effective_thermal_capacity is not none') - self.assertIsNone(thermal_zone.indirectly_heated_area_ratio, - 'thermal_zone indirectly_heated_area_ratio is not none') - self.assertIsNone(thermal_zone.infiltration_rate_system_off, - 'thermal_zone infiltration_rate_system_off is not none') - self.assertIsNone(thermal_zone.infiltration_rate_system_on, - 'thermal_zone infiltration_rate_system_on is not none') - self.assertIsNone(thermal_zone.usage_zones, - 'thermal_zone usage_zones are not none') - - def test_citygml_thermal_boundary(self): - """ - Test thermal boundaries in thermal zones - :return: None - """ - file = 'one_building_in_kelowna.gml' - city = self._get_citygml(file) - for building in city.buildings: - self.assertIsNot(len(building.thermal_zones), 0, 'no building thermal_zones defined') - for thermal_zone in building.thermal_zones: - self.assertIsNot(len(thermal_zone.thermal_boundaries), 0, 'no building thermal_boundaries defined') - for thermal_boundary in thermal_zone.thermal_boundaries: - print(thermal_boundary.surface.type) - print(thermal_boundary.surface.area_above_ground) - self.assertIsNotNone(thermal_boundary.surface, 'thermal_boundary surface is none') - self.assertIsNotNone(thermal_boundary.type, 'thermal_boundary type is none') - self.assertIsNotNone(thermal_boundary.area, 'thermal_boundary area is none') - self.assertIsNotNone(thermal_boundary.azimuth, 'thermal_boundary azimuth is none') - self.assertIsNotNone(thermal_boundary.thermal_zones, 'thermal_boundary delimits is none') - self.assertIsNotNone(thermal_boundary.inclination, 'thermal_boundary inclination is none') - - def test_citygml_thermal_opening(self): - """ - Test thermal openings in thermal zones - :return: None - """ - file = 'one_building_in_kelowna.gml' - city = self._get_citygml(file) - for building in city.buildings: - self.assertIsNot(len(building.thermal_zones), 0, 'no building thermal_zones defined') - for thermal_zone in building.thermal_zones: - self.assertIsNot(len(thermal_zone.thermal_boundaries), 0, 'no building thermal_boundaries defined') - for thermal_boundary in thermal_zone.thermal_boundaries: - thermal_opening: ThermalOpening - for thermal_opening in thermal_boundary.thermal_openings: - self.assertIsNone(thermal_opening.frame_ratio, 'thermal_opening frame_ratio was initialized') - self.assertIsNone(thermal_opening.g_value, 'thermal_opening g_value was initialized') - self.assertIsNone(thermal_opening.conductivity, 'thermal_opening conductivity_w_mk was initialized') - self.assertIsNone(thermal_opening.back_side_solar_transmittance_at_normal_incidence, - 'thermal_opening back_side_solar_transmittance_at_normal_incidence was initialized') - self.assertRaises(Exception, lambda: thermal_opening.openable_ratio, - 'thermal_opening openable_ratio is not raising an exception') - self.assertIsNone(thermal_opening.front_side_solar_transmittance_at_normal_incidence, - 'thermal_opening front_side_solar_transmittance_at_normal_incidence was initialized') - self.assertIsNone(thermal_opening.thickness, 'thermal_opening thickness_m was initialized') - self.assertRaises(Exception, lambda: thermal_opening.overall_u_value, - 'thermal_opening u_value was initialized') - self.assertIsNone(thermal_opening.overall_u_value, 'thermal_opening overall_u_value was initialized') - self.assertIsNone(thermal_opening.hi, 'thermal_opening hi was initialized') - self.assertIsNone(thermal_opening.he, 'thermal_opening he was initialized') - - def test_citygml_function(self): - """ - Test city objects' functions in the city - :return: None - """ - # case 1: hft - file = 'one_building_in_kelowna.gml' - function_format = 'hft' - city = self._get_citygml(file) - for building in city.buildings: - building.function = self._internal_function(function_format, building.function) - self.assertEqual(building.function, 'residential', 'format hft') - - # case 2: Pluto - file = 'pluto_building.gml' - function_format = 'pluto' - city = self._get_citygml(file) - for building in city.buildings: - building.function = self._internal_function(function_format, building.function) - self.assertEqual(building.function, 'secondary school', 'format pluto') - - # case 3: Alkis - file = 'one_building_in_kelowna_alkis.gml' - function_format = 'alkis' - city = self._get_citygml(file) - for building in city.buildings: - self.assertRaises(Exception, lambda: self._internal_function(function_format, building.function)) - - def test_citygml_storeys(self): - """ - Test division by storeys of buildings - :return: None - """ - - file = 'one_building_in_kelowna.gml' - city = self._get_citygml(file) - for building in city.buildings: - print('building') - for surface in building.surfaces: - print(surface.name) - print(surface.type) - print(surface.perimeter_polygon.area) - building.average_storey_height = 1.5 - building.storeys_above_ground = 2 - storeys = building.storeys - for storey in storeys: - print(storey.name) - print(storey.neighbours) - for surface in storey.surfaces: - print(surface.name) - print(surface.type) - print(surface.perimeter_polygon.area) + self._check_surfaces(building) # obj def test_import_obj(self): @@ -235,8 +112,10 @@ class TestGeometryFactory(TestCase): file = 'kelowna.obj' city = self._get_obj(file) self.assertIsNotNone(city, 'city is none') + self.assertTrue(len(city.buildings) == 1) + self._check_buildings(city) for building in city.buildings: - self.assertIsNotNone(building, 'building is none') + self._check_surfaces(building) # osm def test_subway(self): diff --git a/unittests/test_life_cycle_assessment_factory.py b/unittests/test_life_cycle_assessment_factory.py index 9e2e3e62..f7134f20 100644 --- a/unittests/test_life_cycle_assessment_factory.py +++ b/unittests/test_life_cycle_assessment_factory.py @@ -47,10 +47,8 @@ class TestLifeCycleAssessment(TestCase): city_file = "../unittests/tests_data/C40_Final.gml" city = GeometryFactory('citygml', city_file).city LifeCycleAssessment('material', city).enrich() - self.assertTrue(len(city.lca_materials) > 0) - for lca_material in city.lca_materials: - lca_mat = city.lca_material(lca_material.id) - self.assertTrue(lca_mat is not None) + for material in city.materials: + self.assertTrue(len(city.materials) > 0) diff --git a/unittests/test_schedules_factory.py b/unittests/test_schedules_factory.py index 349c5c08..0506abae 100644 --- a/unittests/test_schedules_factory.py +++ b/unittests/test_schedules_factory.py @@ -67,3 +67,4 @@ class TestSchedulesFactory(TestCase): for usage_zone in building.usage_zones: for schedule in usage_zone.schedules: print(schedule) + print(usage_zone.schedules[schedule]) diff --git a/unittests/test_sensors_factory.py b/unittests/test_sensors_factory.py index f0de55c6..55b13e01 100644 --- a/unittests/test_sensors_factory.py +++ b/unittests/test_sensors_factory.py @@ -50,8 +50,8 @@ class TestSensorsFactory(TestCase): Load concordia sensors and verify it """ SensorsFactory('cec', self._city, self._end_point).enrich() - # SensorsFactory('cgf', self._city, self._end_point).enrich() - # SensorsFactory('ct', self._city, self._end_point).enrich() + SensorsFactory('cgf', self._city, self._end_point).enrich() + SensorsFactory('ct', self._city, self._end_point).enrich() for city_object in self._city.city_objects: print(city_object.name, len(city_object.sensors)) for sensor in city_object.sensors: diff --git a/unittests/test_usage_factory.py b/unittests/test_usage_factory.py index abb434f9..3f8f38ea 100644 --- a/unittests/test_usage_factory.py +++ b/unittests/test_usage_factory.py @@ -7,8 +7,8 @@ from pathlib import Path from unittest import TestCase from imports.geometry_factory import GeometryFactory -from imports.construction_factory import ConstructionFactory from imports.usage_factory import UsageFactory +from imports.schedules_factory import SchedulesFactory from imports.construction_factory import ConstructionFactory from imports.geometry.helpers.geometry_helper import GeometryHelper from imports.construction_factory import ConstructionFactory @@ -23,29 +23,119 @@ class TestUsageFactory(TestCase): Configure test environment :return: """ + self._city = None self._example_path = (Path(__file__).parent / 'tests_data').resolve() def _get_citygml(self, file): file_path = (self._example_path / file).resolve() - _city = GeometryFactory('citygml', file_path).city - self.assertIsNotNone(_city, 'city is none') - return _city + self._city = GeometryFactory('citygml', file_path).city + self.assertIsNotNone(self._city, 'city is none') + return self._city - def test_comnet(self): + def _check_buildings(self, city): + for building in city.buildings: + self.assertIsNotNone(building.name, 'building name is none') + self.assertIsNotNone(building.lod, 'building lod is none') + self.assertIsNotNone(building.type, 'building type is none') + self.assertIsNotNone(building.volume, 'building volume is none') + self.assertIsNotNone(building.detailed_polyhedron, 'building detailed polyhedron is none') + self.assertIsNotNone(building.simplified_polyhedron, 'building simplified polyhedron is none') + self.assertIsNotNone(building.surfaces, 'building surfaces is none') + self.assertIsNotNone(building.centroid, 'building centroid is none') + self.assertIsNotNone(building.max_height, 'building max_height is none') + self.assertEqual(len(building.external_temperature), 0, 'building external temperature is calculated') + self.assertEqual(len(building.global_horizontal), 0, 'building global horizontal is calculated') + self.assertEqual(len(building.diffuse), 0, 'building diffuse is calculated') + self.assertEqual(len(building.beam), 0, 'building beam is calculated') + self.assertIsNotNone(building.lower_corner, 'building lower corner is none') + self.assertEqual(len(building.sensors), 0, 'building sensors are assigned') + self.assertIsNotNone(building.geometrical_zones, 'no geometrical zones created') + self.assertIsNotNone(building.grounds, 'building grounds is none') + self.assertIsNotNone(building.walls, 'building walls is none') + self.assertIsNotNone(building.roofs, 'building roofs is none') + self.assertTrue(len(building.usage_zones) > 0, 'usage zones are not defined') + self.assertTrue(len(building.thermal_zones) > 0, 'thermal zones are not defined') + self.assertIsNotNone(building.basement_heated, 'building basement_heated is none') + self.assertIsNotNone(building.attic_heated, 'building attic_heated is none') + self.assertIsNone(building.terrains, 'building terrains is not none') + self.assertIsNotNone(building.year_of_construction, 'building year_of_construction is none') + self.assertIsNotNone(building.function, 'building function is none') + self.assertIsNotNone(building.average_storey_height, 'building average_storey_height is none') + self.assertIsNotNone(building.storeys_above_ground, 'building storeys_above_ground is none') + self.assertEqual(len(building.heating), 0, 'building heating is not none') + self.assertEqual(len(building.cooling), 0, 'building cooling is not none') + self.assertIsNotNone(building.eave_height, 'building eave height is none') + self.assertIsNotNone(building.storeys, 'building storeys are not defined') + self.assertIsNotNone(building.roof_type, 'building roof type is none') + self.assertIsNotNone(building.floor_area, 'building floor_area is none') + self.assertIsNone(building.households, 'building households is not none') + self.assertTrue(building.is_conditioned, 'building is not conditioned') + + def _check_thermal_zones(self, building): + for thermal_zone in building.thermal_zones: + self.assertIsNotNone(thermal_zone.id, 'thermal_zone id is none') + self.assertIsNotNone(thermal_zone.floor_area, 'thermal_zone floor area is none') + self.assertTrue(len(thermal_zone.thermal_boundaries) > 0, 'thermal_zone thermal_boundaries not defined') + self.assertIsNotNone(thermal_zone.additional_thermal_bridge_u_value, 'additional_thermal_bridge_u_value is none') + self.assertIsNotNone(thermal_zone.effective_thermal_capacity, 'thermal_zone effective_thermal_capacity is none') + self.assertIsNotNone(thermal_zone.indirectly_heated_area_ratio, + 'thermal_zone indirectly_heated_area_ratio is none') + self.assertIsNotNone(thermal_zone.infiltration_rate_system_off, + 'thermal_zone infiltration_rate_system_off is none') + self.assertIsNotNone(thermal_zone.infiltration_rate_system_on, 'thermal_zone infiltration_rate_system_on is none') + self.assertIsNotNone(thermal_zone.usage_zones, 'thermal_zone usage_zones is none') + self.assertIsNotNone(thermal_zone.volume, 'thermal_zone volume is none') + self.assertIsNone(thermal_zone.ordinate_number, 'thermal_zone ordinate number is not none') + self.assertIsNotNone(thermal_zone.thermal_control, 'thermal_zone thermal_control is not none') + self.assertIsNotNone(thermal_zone.hvac_system, 'thermal_zone hvac_system is not none') + + def _check_usage_zones(self, building): + for usage_zone in building.usage_zones: + self.assertIsNotNone(usage_zone.usage, 'usage is none') + self.assertIsNotNone(usage_zone.not_detailed_source_mean_annual_internal_gains, 'usage is none') + self.assertIsNotNone(usage_zone.cooling_setpoint, 'usage is none') + self.assertIsNotNone(usage_zone.heating_setback, 'usage is none') + self.assertIsNotNone(usage_zone.heating_setpoint, 'usage is none') + self.assertIsNotNone(usage_zone.occupancy_density, 'usage is none') + self.assertIsNotNone(usage_zone.hours_day, 'usage is none') + self.assertIsNotNone(usage_zone.days_year, 'usage is none') + self.assertIsNotNone(usage_zone.dhw_average_volume_pers_day, 'usage is none') + self.assertIsNotNone(usage_zone.dhw_preparation_temperature, 'usage is none') + self.assertIsNotNone(usage_zone.electrical_app_average_consumption_sqm_year, 'usage is none') + self.assertIsNotNone(usage_zone.is_heated, 'thermal_zone heated is none') + self.assertIsNotNone(usage_zone.is_cooled, 'thermal_zone cooled is none') + + def _check_hvac(self, thermal_zone): + self.assertIsNotNone(None, 'hvac') + + def _check_control(self, thermal_zone): + self.assertIsNotNone(None, 'control') + + def test_import_comnet(self): + """ + Enrich the city with the usage information from comnet and verify it + """ file = 'pluto_building.gml' city = self._get_citygml(file) for building in city.buildings: building.function = GeometryHelper.pluto_to_function[building.function] - ConstructionFactory('nrel', city).enrich() - UsageFactory('comnet', city).enrich() - def test_city_with_usage(self): + UsageFactory('comnet', city).enrich() + SchedulesFactory('comnet', city).enrich() + self._check_buildings(city) + for building in city.buildings: + self._check_thermal_zones(building) + for thermal_zone in building.thermal_zones: + self._check_hvac(thermal_zone) + self._check_control(thermal_zone) + self.assertIsNotNone(building.usage_zones, 'building usage zones is none') + self._check_usage_zones(building) + + def test_import_hft(self): """ - Enrich the city with the usage information and verify it - :return: None + Enrich the city with the usage information from hft and verify it """ - # case 1: HFT file = 'pluto_building.gml' city = self._get_citygml(file) for building in city.buildings: @@ -56,7 +146,7 @@ class TestUsageFactory(TestCase): self.assertIsNot(len(building.usage_zones), 0, 'no building usage_zones defined') for usage_zone in building.usage_zones: self.assertIsNotNone(usage_zone.usage, 'usage is none') - self.assertIsNotNone(usage_zone.internal_gains, 'usage is none') + self.assertIsNotNone(usage_zone.not_detailed_source_mean_annual_internal_gains, 'usage is none') self.assertIsNotNone(usage_zone.cooling_setpoint, 'usage is none') self.assertIsNotNone(usage_zone.heating_setback, 'usage is none') self.assertIsNotNone(usage_zone.heating_setpoint, 'usage is none') @@ -69,8 +159,10 @@ class TestUsageFactory(TestCase): self.assertIsNotNone(usage_zone.is_heated, 'thermal_zone heated is none') self.assertIsNotNone(usage_zone.is_cooled, 'thermal_zone cooled is none') - - # case 2: CA + def test_import_ca(self): + """ + Enrich the city with the usage information from hft and verify it + """ file = 'one_building_in_kelowna.gml' city = self._get_citygml(file) ConstructionFactory('nrel', city).enrich() @@ -79,7 +171,7 @@ class TestUsageFactory(TestCase): self.assertIsNot(len(building.usage_zones), 0, 'no building usage_zones defined') for usage_zone in building.usage_zones: self.assertIsNotNone(usage_zone.usage, 'usage is none') - self.assertIsNotNone(usage_zone.internal_gains, 'usage is none') + self.assertIsNotNone(usage_zone.not_detailed_source_mean_annual_internal_gains, 'usage is none') self.assertIsNotNone(usage_zone.cooling_setpoint, 'usage is none') self.assertIsNotNone(usage_zone.heating_setback, 'usage is none') self.assertIsNotNone(usage_zone.heating_setpoint, 'usage is none') From 2585cd3245224e4f6397620fe1343dbae23f73f6 Mon Sep 17 00:00:00 2001 From: Pilar Date: Tue, 8 Mar 2022 20:08:03 -0500 Subject: [PATCH 09/19] partially finished usage factory test --- city_model_structure/building.py | 31 +++-- .../building_demand/internal_zone.py | 84 ++++++++++++++ .../building_demand/occupant.py | 24 ---- imports/usage/ca_usage_parameters.py | 17 ++- imports/usage/comnet_usage_parameters.py | 28 ++--- imports/usage/hft_usage_parameters.py | 20 ++-- imports/usage_factory.py | 4 - unittests/test_usage_factory.py | 109 +++++++----------- 8 files changed, 173 insertions(+), 144 deletions(-) create mode 100644 city_model_structure/building_demand/internal_zone.py diff --git a/city_model_structure/building.py b/city_model_structure/building.py index f7d2dc16..1aff3b84 100644 --- a/city_model_structure/building.py +++ b/city_model_structure/building.py @@ -12,6 +12,7 @@ from city_model_structure.building_demand.usage_zone import UsageZone from city_model_structure.building_demand.storey import Storey from city_model_structure.city_object import CityObject from city_model_structure.building_demand.household import Household +from city_model_structure.building_demand.internal_zone import InternalZone from city_model_structure.attributes.polyhedron import Polyhedron ThermalZone = TypeVar('ThermalZone') @@ -34,7 +35,8 @@ class Building(CityObject): self._floor_area = None self._roof_type = None self._storeys = None - self._geometrical_zones = None + self._internal_zones = None + self._shell = None self._thermal_zones = None self._usage_zones = None self._type = 'building' @@ -61,13 +63,26 @@ class Building(CityObject): self._internal_walls.append(surface) @property - def geometrical_zones(self) -> List[Polyhedron]: - if self._geometrical_zones is None: - polygons = [] - for surface in self.surfaces: - polygons.append(surface.perimeter_polygon) - self._geometrical_zones = [Polyhedron(polygons)] - return self._geometrical_zones + def shell(self) -> Polyhedron: + """ + Get building shell + :return: [Polyhedron] + """ + if self._shell is None: + self._shell = Polyhedron(self.surfaces) + return self._shell + + @property + def internal_zones(self) -> List[InternalZone]: + """ + Get building internal zones + For Lod up to 3, there is only one internal zone which corresponds to the building shell. + In LoD 4 there can be more than one. In this case the definition of surfaces and floor area must be redefined. + :return: [InternalZone] + """ + if self._internal_zones is None: + self._internal_zones = [InternalZone(self.surfaces, self.floor_area)] + return self._internal_zones @property def grounds(self) -> List[Surface]: diff --git a/city_model_structure/building_demand/internal_zone.py b/city_model_structure/building_demand/internal_zone.py new file mode 100644 index 00000000..222174bb --- /dev/null +++ b/city_model_structure/building_demand/internal_zone.py @@ -0,0 +1,84 @@ +""" +InternalZone module. It saves the original geometrical information from interiors together with some attributes of those +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2022 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +""" +import uuid +from city_model_structure.building_demand.usage_zone import UsageZone +from city_model_structure.attributes.polyhedron import Polyhedron + + +class InternalZone: + """ + InternalZone class + """ + def __init__(self, surfaces, area): + self._surfaces = surfaces + self._id = None + self._geometry = None + self._volume = None + self._area = area + self._usage_zones = [] + + @property + def id(self): + """ + Get internal zone id, an universally unique identifier randomly generated + :return: str + """ + if self._id is None: + self._id = uuid.uuid4() + return self._id + + @property + def geometry(self) -> Polyhedron: + """ + Get internal zone geometry + :return: Polyhedron + """ + if self._geometry is None: + polygons = [] + for surface in self.surfaces: + polygons.append(surface.perimeter_polygon) + self._geometry = Polyhedron(polygons) + return self._geometry + + @property + def surfaces(self): + """ + Get internal zone surfaces + :return: [Surface] + """ + return self._surfaces + + @property + def volume(self): + """ + Get internal zone volume in cubic meters + :return: float + """ + return self.geometry.volume + + @property + def area(self): + """ + Get internal zone area in square meters + :return: float + """ + return self._area + + @property + def usage_zones(self) -> [UsageZone]: + """ + Get internal zone usage zones + :return: [UsageZone] + """ + return self._usage_zones + + @usage_zones.setter + def usage_zones(self, value): + """ + Set internal zone usage zones + :param value: [UsageZone] + """ + self._usage_zones = value diff --git a/city_model_structure/building_demand/occupant.py b/city_model_structure/building_demand/occupant.py index 8a7dd533..878d278c 100644 --- a/city_model_structure/building_demand/occupant.py +++ b/city_model_structure/building_demand/occupant.py @@ -144,27 +144,3 @@ class Occupant: """ # todo @Sanam: what format are you expecting here?? self._pd_of_meetings_duration = value - - def get_complete_year_schedule(self, schedules): - """ - Get the a non-leap year (8760 h), starting on Monday schedules out of archetypal days of week - :return: [float] - """ - if self._complete_year_schedule is None: - self._complete_year_schedule = [] - for i in range(1, 13): - month_range = cal.monthrange(2015, i)[1] - for day in range(1, month_range+1): - if cal.weekday(2015, i, day) < 5: - for j in range(0, 24): - week_schedule = schedules['WD'][j] - self._complete_year_schedule.append(week_schedule) - elif cal.weekday(2015, i, day) == 5: - for j in range(0, 24): - week_schedule = schedules['Sat'][j] - self._complete_year_schedule.append(week_schedule) - else: - for j in range(0, 24): - week_schedule = schedules['Sun'][j] - self._complete_year_schedule.append(week_schedule) - return self._complete_year_schedule diff --git a/imports/usage/ca_usage_parameters.py b/imports/usage/ca_usage_parameters.py index cd921332..cea058f6 100644 --- a/imports/usage/ca_usage_parameters.py +++ b/imports/usage/ca_usage_parameters.py @@ -35,15 +35,13 @@ class CaUsageParameters(HftUsageInterface): f' {building.function}, that assigns building usage as ' f'{gh.usage_from_function(building.function)}\n') continue - # todo: what to do with mix-usage usage from gml? - mix_usage = False - if not mix_usage: - # just one usage_zone - for thermal_zone in building.thermal_zones: + + for internal_zone in building.internal_zones: usage_zone = UsageZone() + usage_zone.usage = building.function self._assign_values(usage_zone, archetype) - usage_zone.volume = thermal_zone.volume - thermal_zone.usage_zones = [usage_zone] + usage_zone.percentage = 1 + internal_zone.usage_zones = [usage_zone] def _search_archetype(self, building_usage): for building_archetype in self._usage_archetypes: @@ -53,19 +51,18 @@ class CaUsageParameters(HftUsageInterface): @staticmethod def _assign_values(usage_zone, archetype): - usage_zone.usage = archetype.usage # Due to the fact that python is not a typed language, the wrong object type is assigned to # usage_zone.internal_gains when writing usage_zone.internal_gains = archetype.internal_gains. # Therefore, this walk around has been done. internal_gains = [] - for archetype_internal_gain in archetype.not_detailed_source_mean_annual_internal_gains: + for archetype_internal_gain in archetype.internal_gains: internal_gain = InternalGains() internal_gain.average_internal_gain = archetype_internal_gain.average_internal_gain internal_gain.convective_fraction = archetype_internal_gain.convective_fraction internal_gain.radiative_fraction = archetype_internal_gain.radiative_fraction internal_gain.latent_fraction = archetype_internal_gain.latent_fraction internal_gains.append(internal_gain) - usage_zone.not_detailed_source_mean_annual_internal_gains = internal_gains + usage_zone.internal_gains = internal_gains usage_zone.heating_setpoint = archetype.heating_setpoint usage_zone.heating_setback = archetype.heating_setback usage_zone.cooling_setpoint = archetype.cooling_setpoint diff --git a/imports/usage/comnet_usage_parameters.py b/imports/usage/comnet_usage_parameters.py index 0d0a835d..ed5d187e 100644 --- a/imports/usage/comnet_usage_parameters.py +++ b/imports/usage/comnet_usage_parameters.py @@ -69,7 +69,7 @@ class ComnetUsageParameters: 'process': process_data} @staticmethod - def _parse_zone_usage_type(usage, height, data): + def _parse_zone_usage_type(usage, data): _usage_zone = UsageZone() _usage_zone.usage = usage @@ -91,7 +91,7 @@ class ComnetUsageParameters: # occupancy _occupancy = Occupancy() - _occupancy.occupancy_density = data['occupancy'][usage][0] * cte.METERS_TO_FEET**2 + _occupancy.occupancy_density = data['occupancy'][usage][0] _occupancy.sensible_convective_internal_gain = data['occupancy'][usage][1] \ * ch().comnet_occupancy_sensible_convective _occupancy.sensible_radiant_internal_gain = data['occupancy'][usage][1] * ch().comnet_occupancy_sensible_radiant @@ -100,8 +100,7 @@ class ComnetUsageParameters: if _occupancy.occupancy_density <= 0: _usage_zone.mechanical_air_change = 0 else: - _usage_zone.mechanical_air_change = data['ventilation rate'][usage][0] / _occupancy.occupancy_density \ - * cte.HOUR_TO_MINUTES / cte.METERS_TO_FEET**3 / height + _usage_zone.mechanical_air_change = data['ventilation rate'][usage][0] / _occupancy.occupancy_density _usage_zone.occupancy = _occupancy _usage_zone.lighting = _lighting @@ -116,11 +115,6 @@ class ComnetUsageParameters: city = self._city for building in city.buildings: usage = GeometryHelper.usage_from_function(building.function) - height = building.average_storey_height - if height is None: - raise Exception('Average storey height not defined, ACH cannot be calculated') - if height <= 0: - raise Exception('Average storey height is zero, ACH cannot be calculated') archetype = self._search_archetype(UsageHelper.comnet_from_usage(usage)) if archetype is None: sys.stderr.write(f'Building {building.name} has unknown archetype for building function:' @@ -128,13 +122,12 @@ class ComnetUsageParameters: f'{GeometryHelper.usage_from_function(building.function)}\n') continue - # just one usage_zone - for thermal_zone in building.thermal_zones: + for internal_zone in building.internal_zones: usage_zone = UsageZone() usage_zone.usage = usage - self._assign_values(usage_zone, archetype, height) - usage_zone.volume = thermal_zone.volume - thermal_zone.usage_zones = [usage_zone] + self._assign_values(usage_zone, archetype, volume_per_area) + usage_zone.percentage = 1 + internal_zone.usage_zones = [usage_zone] def _search_archetype(self, building_usage): for building_archetype in self._usage_archetypes: @@ -143,7 +136,7 @@ class ComnetUsageParameters: return None @staticmethod - def _assign_values(usage_zone, archetype, height): + def _assign_values(usage_zone, archetype, volume_per_area): # Due to the fact that python is not a typed language, the wrong object type is assigned to # usage_zone.internal_gains when writing usage_zone.internal_gains = archetype.internal_gains. # Therefore, this walk around has been done. @@ -157,5 +150,6 @@ class ComnetUsageParameters: internal_gain.latent_fraction = archetype_internal_gain.latent_fraction internal_gains.append(internal_gain) usage_zone.internal_gains = internal_gains - usage_zone.occupancy_density = archetype.occupancy_density - usage_zone.mechanical_air_change = archetype.mechanical_air_change \ No newline at end of file + usage_zone.occupancy_density = archetype.occupancy_density * cte.METERS_TO_FEET**2 + usage_zone.mechanical_air_change = archetype.mechanical_air_change * cte.METERS_TO_FEET**2 \ + * cte.HOUR_TO_MINUTES / cte.METERS_TO_FEET**3 / volume_per_area \ No newline at end of file diff --git a/imports/usage/hft_usage_parameters.py b/imports/usage/hft_usage_parameters.py index d2deeeae..7411891e 100644 --- a/imports/usage/hft_usage_parameters.py +++ b/imports/usage/hft_usage_parameters.py @@ -18,9 +18,6 @@ class HftUsageParameters(HftUsageInterface): def __init__(self, city, base_path): super().__init__(base_path, 'de_library.xml') self._city = city - # todo: this is a wrong location for self._min_air_change -> re-think where to place this info - # and where it comes from - self._min_air_change = 0 def enrich_buildings(self): """ @@ -35,15 +32,13 @@ class HftUsageParameters(HftUsageInterface): f' {building.function}, that assigns building usage as ' f'{gh.usage_from_function(building.function)}\n') continue - # todo: what to do with mix-usage usage from gml? - mix_usage = False - if not mix_usage: - # just one usage_zone - for thermal_zone in building.thermal_zones: + + for internal_zone in building.internal_zones: usage_zone = UsageZone() + usage_zone.usage = building.function self._assign_values(usage_zone, archetype) - usage_zone.volume = thermal_zone.volume - thermal_zone.usage_zones = [usage_zone] + usage_zone.percentage = 1 + internal_zone.usage_zones = [usage_zone] def _search_archetype(self, building_usage): for building_archetype in self._usage_archetypes: @@ -53,19 +48,18 @@ class HftUsageParameters(HftUsageInterface): @staticmethod def _assign_values(usage_zone, archetype): - usage_zone.usage = archetype.usage # Due to the fact that python is not a typed language, the wrong object type is assigned to # usage_zone.internal_gains when writing usage_zone.internal_gains = archetype.internal_gains. # Therefore, this walk around has been done. internal_gains = [] - for archetype_internal_gain in archetype.not_detailed_source_mean_annual_internal_gains: + for archetype_internal_gain in archetype.internal_gains: internal_gain = InternalGains() internal_gain.average_internal_gain = archetype_internal_gain.average_internal_gain internal_gain.convective_fraction = archetype_internal_gain.convective_fraction internal_gain.radiative_fraction = archetype_internal_gain.radiative_fraction internal_gain.latent_fraction = archetype_internal_gain.latent_fraction internal_gains.append(internal_gain) - usage_zone.not_detailed_source_mean_annual_internal_gains = internal_gains + usage_zone.internal_gains = internal_gains usage_zone.heating_setpoint = archetype.heating_setpoint usage_zone.heating_setback = archetype.heating_setback usage_zone.cooling_setpoint = archetype.cooling_setpoint diff --git a/imports/usage_factory.py b/imports/usage_factory.py index 0e2875de..165e7f45 100644 --- a/imports/usage_factory.py +++ b/imports/usage_factory.py @@ -22,10 +22,6 @@ class UsageFactory: self._handler = '_' + handler.lower().replace(' ', '_') self._city = city self._base_path = base_path - for building in city.buildings: - if len(building.thermal_zones) == 0: - raise Exception('It seems that the usage factory is being called before the construction factory. ' - 'Please ensure that the construction factory is called first.') def _hft(self): """ diff --git a/unittests/test_usage_factory.py b/unittests/test_usage_factory.py index 3f8f38ea..b884259d 100644 --- a/unittests/test_usage_factory.py +++ b/unittests/test_usage_factory.py @@ -71,39 +71,6 @@ class TestUsageFactory(TestCase): self.assertIsNone(building.households, 'building households is not none') self.assertTrue(building.is_conditioned, 'building is not conditioned') - def _check_thermal_zones(self, building): - for thermal_zone in building.thermal_zones: - self.assertIsNotNone(thermal_zone.id, 'thermal_zone id is none') - self.assertIsNotNone(thermal_zone.floor_area, 'thermal_zone floor area is none') - self.assertTrue(len(thermal_zone.thermal_boundaries) > 0, 'thermal_zone thermal_boundaries not defined') - self.assertIsNotNone(thermal_zone.additional_thermal_bridge_u_value, 'additional_thermal_bridge_u_value is none') - self.assertIsNotNone(thermal_zone.effective_thermal_capacity, 'thermal_zone effective_thermal_capacity is none') - self.assertIsNotNone(thermal_zone.indirectly_heated_area_ratio, - 'thermal_zone indirectly_heated_area_ratio is none') - self.assertIsNotNone(thermal_zone.infiltration_rate_system_off, - 'thermal_zone infiltration_rate_system_off is none') - self.assertIsNotNone(thermal_zone.infiltration_rate_system_on, 'thermal_zone infiltration_rate_system_on is none') - self.assertIsNotNone(thermal_zone.usage_zones, 'thermal_zone usage_zones is none') - self.assertIsNotNone(thermal_zone.volume, 'thermal_zone volume is none') - self.assertIsNone(thermal_zone.ordinate_number, 'thermal_zone ordinate number is not none') - self.assertIsNotNone(thermal_zone.thermal_control, 'thermal_zone thermal_control is not none') - self.assertIsNotNone(thermal_zone.hvac_system, 'thermal_zone hvac_system is not none') - - def _check_usage_zones(self, building): - for usage_zone in building.usage_zones: - self.assertIsNotNone(usage_zone.usage, 'usage is none') - self.assertIsNotNone(usage_zone.not_detailed_source_mean_annual_internal_gains, 'usage is none') - self.assertIsNotNone(usage_zone.cooling_setpoint, 'usage is none') - self.assertIsNotNone(usage_zone.heating_setback, 'usage is none') - self.assertIsNotNone(usage_zone.heating_setpoint, 'usage is none') - self.assertIsNotNone(usage_zone.occupancy_density, 'usage is none') - self.assertIsNotNone(usage_zone.hours_day, 'usage is none') - self.assertIsNotNone(usage_zone.days_year, 'usage is none') - self.assertIsNotNone(usage_zone.dhw_average_volume_pers_day, 'usage is none') - self.assertIsNotNone(usage_zone.dhw_preparation_temperature, 'usage is none') - self.assertIsNotNone(usage_zone.electrical_app_average_consumption_sqm_year, 'usage is none') - self.assertIsNotNone(usage_zone.is_heated, 'thermal_zone heated is none') - self.assertIsNotNone(usage_zone.is_cooled, 'thermal_zone cooled is none') def _check_hvac(self, thermal_zone): self.assertIsNotNone(None, 'hvac') @@ -119,62 +86,68 @@ class TestUsageFactory(TestCase): city = self._get_citygml(file) for building in city.buildings: building.function = GeometryHelper.pluto_to_function[building.function] - ConstructionFactory('nrel', city).enrich() UsageFactory('comnet', city).enrich() SchedulesFactory('comnet', city).enrich() self._check_buildings(city) for building in city.buildings: - self._check_thermal_zones(building) - for thermal_zone in building.thermal_zones: - self._check_hvac(thermal_zone) - self._check_control(thermal_zone) - self.assertIsNotNone(building.usage_zones, 'building usage zones is none') - self._check_usage_zones(building) + for internal_zone in building.internal_zones: + self.assertIsNot(len(internal_zone.usage_zones), 0, 'no building usage_zones defined') + for usage_zone in internal_zone.usage_zones: + self._check_extended_usage(usage_zone) def test_import_hft(self): """ Enrich the city with the usage information from hft and verify it """ + # todo: read schedules!! file = 'pluto_building.gml' city = self._get_citygml(file) for building in city.buildings: building.function = GeometryHelper.pluto_to_function[building.function] - ConstructionFactory('nrel', city).enrich() + UsageFactory('hft', city).enrich() for building in city.buildings: - self.assertIsNot(len(building.usage_zones), 0, 'no building usage_zones defined') - for usage_zone in building.usage_zones: - self.assertIsNotNone(usage_zone.usage, 'usage is none') - self.assertIsNotNone(usage_zone.not_detailed_source_mean_annual_internal_gains, 'usage is none') - self.assertIsNotNone(usage_zone.cooling_setpoint, 'usage is none') - self.assertIsNotNone(usage_zone.heating_setback, 'usage is none') - self.assertIsNotNone(usage_zone.heating_setpoint, 'usage is none') - self.assertIsNotNone(usage_zone.occupancy_density, 'usage is none') - self.assertIsNotNone(usage_zone.hours_day, 'usage is none') - self.assertIsNotNone(usage_zone.days_year, 'usage is none') - self.assertIsNotNone(usage_zone.dhw_average_volume_pers_day, 'usage is none') - self.assertIsNotNone(usage_zone.dhw_preparation_temperature, 'usage is none') - self.assertIsNotNone(usage_zone.electrical_app_average_consumption_sqm_year, 'usage is none') - self.assertIsNotNone(usage_zone.is_heated, 'thermal_zone heated is none') - self.assertIsNotNone(usage_zone.is_cooled, 'thermal_zone cooled is none') + for internal_zone in building.internal_zones: + self.assertIsNot(len(internal_zone.usage_zones), 0, 'no building usage_zones defined') + for usage_zone in internal_zone.usage_zones: + self._check_extended_usage(usage_zone) def test_import_ca(self): """ - Enrich the city with the usage information from hft and verify it + Enrich the city with the usage information from canada and verify it """ file = 'one_building_in_kelowna.gml' city = self._get_citygml(file) - ConstructionFactory('nrel', city).enrich() UsageFactory('ca', city).enrich() for building in city.buildings: - self.assertIsNot(len(building.usage_zones), 0, 'no building usage_zones defined') - for usage_zone in building.usage_zones: - self.assertIsNotNone(usage_zone.usage, 'usage is none') - self.assertIsNotNone(usage_zone.not_detailed_source_mean_annual_internal_gains, 'usage is none') - self.assertIsNotNone(usage_zone.cooling_setpoint, 'usage is none') - self.assertIsNotNone(usage_zone.heating_setback, 'usage is none') - self.assertIsNotNone(usage_zone.heating_setpoint, 'usage is none') - self.assertIsNotNone(usage_zone.occupancy_density, 'usage is none') - self.assertIsNotNone(usage_zone.hours_day, 'usage is none') - self.assertIsNotNone(usage_zone.days_year, 'usage is none') + for internal_zone in building.internal_zones: + self.assertIsNot(len(internal_zone.usage_zones), 0, 'no building usage_zones defined') + for usage_zone in internal_zone.usage_zones: + self._check_reduced_usage(usage_zone) + + def _check_extended_usage(self, usage_zone): + self.assertIsNotNone(usage_zone.usage, 'usage is none') + self.assertIsNotNone(usage_zone.not_detailed_source_mean_annual_internal_gains, 'usage is none') + self.assertIsNotNone(usage_zone.cooling_setpoint, 'usage is none') + self.assertIsNotNone(usage_zone.heating_setback, 'usage is none') + self.assertIsNotNone(usage_zone.heating_setpoint, 'usage is none') + self.assertIsNotNone(usage_zone.occupancy_density, 'usage is none') + self.assertIsNotNone(usage_zone.hours_day, 'usage is none') + self.assertIsNotNone(usage_zone.days_year, 'usage is none') + self.assertIsNotNone(usage_zone.dhw_average_volume_pers_day, 'usage is none') + self.assertIsNotNone(usage_zone.dhw_preparation_temperature, 'usage is none') + self.assertIsNotNone(usage_zone.electrical_app_average_consumption_sqm_year, 'usage is none') + self.assertIsNotNone(usage_zone.is_heated, 'thermal_zone heated is none') + self.assertIsNotNone(usage_zone.is_cooled, 'thermal_zone cooled is none') + + + def _check_reduced_usage(self, usage_zone): + self.assertIsNotNone(usage_zone.usage, 'usage is none') + self.assertIsNotNone(usage_zone.internal_gains, 'usage is none') + self.assertIsNotNone(usage_zone.cooling_setpoint, 'usage is none') + self.assertIsNotNone(usage_zone.heating_setback, 'usage is none') + self.assertIsNotNone(usage_zone.heating_setpoint, 'usage is none') + self.assertIsNotNone(usage_zone.occupancy_density, 'usage is none') + self.assertIsNotNone(usage_zone.hours_day, 'usage is none') + self.assertIsNotNone(usage_zone.days_year, 'usage is none') From 62dcc3805b2e80b431818065afd5618c29fef60b Mon Sep 17 00:00:00 2001 From: Pilar Date: Thu, 17 Mar 2022 18:49:44 -0400 Subject: [PATCH 10/19] complete modification of the structure of the classes that define the buildig for a better adjustment to the different data sources and the connected tools. NOT finished --- city_model_structure/attributes/schedule.py | 136 +++--- city_model_structure/building.py | 51 +- .../building_demand/appliances.py | 50 +- .../building_demand/internal_gains.py | 18 +- .../building_demand/internal_zone.py | 41 +- .../building_demand/lighting.py | 34 +- .../building_demand/occupancy.py | 32 +- .../building_demand/thermal_control.py | 112 +++-- .../building_demand/thermal_zone.py | 61 ++- .../building_demand/usage_zone.py | 86 +++- data/usage/comnet_schedules_archetypes.xlsx | Bin 0 -> 348793 bytes data/usage/de_library.xml | 4 +- helpers/constants.py | 1 + imports/construction/ca_physics_parameters.py | 75 +-- .../helpers/construction_helper.py | 26 +- imports/construction/us_physics_parameters.py | 119 ++--- imports/geometry/helpers/geometry_helper.py | 459 ++++++++---------- imports/schedules/helpers/schedules_helper.py | 16 +- imports/usage/ca_usage_parameters.py | 66 +-- imports/usage/comnet_usage_parameters.py | 211 ++++++-- imports/usage/helpers/usage_helper.py | 51 +- imports/usage/hft_usage_interface.py | 297 ++++++++---- imports/usage/hft_usage_parameters.py | 61 ++- unittests/test_construction_factory.py | 95 ++-- unittests/test_enrichement.py | 145 ++++++ unittests/test_geometry_factory.py | 2 +- unittests/test_usage_factory.py | 160 +++--- 27 files changed, 1465 insertions(+), 944 deletions(-) create mode 100644 data/usage/comnet_schedules_archetypes.xlsx create mode 100644 unittests/test_enrichement.py diff --git a/city_model_structure/attributes/schedule.py b/city_model_structure/attributes/schedule.py index 1924dd7b..2d009f7f 100644 --- a/city_model_structure/attributes/schedule.py +++ b/city_model_structure/attributes/schedule.py @@ -9,131 +9,131 @@ from typing import Union, List class Schedule: - """ + """ Schedule class """ - def __init__(self): - self._id = None - self._type = None - self._values = None - self._data_type = None - self._time_step = None - self._time_range = None - self._day_types = None + def __init__(self): + self._id = None + self._type = None + self._values = None + self._data_type = None + self._time_step = None + self._time_range = None + self._day_types = None - @property - def id(self): - """ + @property + def id(self): + """ Get schedule id, an universally unique identifier randomly generated :return: str """ - if self._id is None: - self._id = uuid.uuid4() - return self._id + if self._id is None: + self._id = uuid.uuid4() + return self._id - @property - def type(self) -> Union[None, str]: - """ + @property + def type(self) -> Union[None, str]: + """ Get schedule type :return: None or str """ - return self._type + return self._type - @type.setter - def type(self, value): - """ + @type.setter + def type(self, value): + """ Set schedule type :param: str """ - if value is not None: - self._type = str(value) + if value is not None: + self._type = str(value) - @property - def values(self): - """ + @property + def values(self): + """ Get schedule values :return: [Any] """ - return self._values + return self._values - @values.setter - def values(self, value): - """ + @values.setter + def values(self, value): + """ Set schedule values :param: [Any] """ - self._values = value + self._values = value - @property - def data_type(self) -> Union[None, str]: - """ + @property + def data_type(self) -> Union[None, str]: + """ Get schedule data type from: ['any_number', 'fraction', 'on_off', 'temperature', 'humidity', 'control_type'] :return: None or str """ - return self._data_type + return self._data_type - @data_type.setter - def data_type(self, value): - """ + @data_type.setter + def data_type(self, value): + """ Set schedule data type :param: str """ - if value is not None: - self._data_type = str(value) + if value is not None: + self._data_type = str(value) - @property - def time_step(self) -> Union[None, str]: - """ + @property + def time_step(self) -> Union[None, str]: + """ Get schedule time step from: ['second', 'minute', 'hour', 'day', 'week', 'month'] :return: None or str """ - return self._time_step + return self._time_step - @time_step.setter - def time_step(self, value): - """ + @time_step.setter + def time_step(self, value): + """ Set schedule time step :param: str """ - if value is not None: - self._time_step = str(value) + if value is not None: + self._time_step = str(value) - @property - def time_range(self) -> Union[None, str]: - """ + @property + def time_range(self) -> Union[None, str]: + """ Get schedule time range from: ['minute', 'hour', 'day', 'week', 'month', 'year'] :return: None or str """ - return self._time_range + return self._time_range - @time_range.setter - def time_range(self, value): - """ + @time_range.setter + def time_range(self, value): + """ Set schedule time range :param: str """ - if value is not None: - self._time_range = str(value) + if value is not None: + self._time_range = str(value) - @property - def day_types(self) -> Union[None, List[str]]: - """ + @property + def day_types(self) -> Union[None, List[str]]: + """ Get schedule day types, as many as needed from: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday', 'holiday', 'winter_design_day', 'summer_design_day'] :return: None or [str] """ - return self._day_types + return self._day_types - @day_types.setter - def day_types(self, value): - """ + @day_types.setter + def day_types(self, value): + """ Set schedule day types :param: [str] """ - if value is not None: - self._day_types = [str(i) for i in value] + if value is not None: + self._day_types = [str(i) for i in value] diff --git a/city_model_structure/building.py b/city_model_structure/building.py index 1aff3b84..a94e55a6 100644 --- a/city_model_structure/building.py +++ b/city_model_structure/building.py @@ -5,18 +5,15 @@ Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@conc contributors: Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ -from typing import List, Union, TypeVar +from typing import List, Union import numpy as np from city_model_structure.building_demand.surface import Surface -from city_model_structure.building_demand.usage_zone import UsageZone from city_model_structure.building_demand.storey import Storey from city_model_structure.city_object import CityObject from city_model_structure.building_demand.household import Household from city_model_structure.building_demand.internal_zone import InternalZone from city_model_structure.attributes.polyhedron import Polyhedron -ThermalZone = TypeVar('ThermalZone') - class Building(CityObject): """ @@ -36,9 +33,7 @@ class Building(CityObject): self._roof_type = None self._storeys = None self._internal_zones = None - self._shell = None - self._thermal_zones = None - self._usage_zones = None + self._shell = None self._type = 'building' self._heating = dict() self._cooling = dict() @@ -108,22 +103,6 @@ class Building(CityObject): """ return self._walls - @property - def usage_zones(self) -> Union[None, List[UsageZone]]: - """ - Get city object usage zones - :return: [UsageZone] - """ - return self._usage_zones - - @usage_zones.setter - def usage_zones(self, value): - """ - Set city object usage zones - :param value: [UsageZone] - """ - self._usage_zones = value - @property def terrains(self) -> Union[None, List[Surface]]: """ @@ -166,22 +145,6 @@ class Building(CityObject): if value is not None: self._basement_heated = int(value) - @property - def thermal_zones(self) -> Union[None, List[ThermalZone]]: - """ - Get building thermal zones - :return: [ThermalZone] - """ - return self._thermal_zones - - @thermal_zones.setter - def thermal_zones(self, value): - """ - Set city object thermal zones - :param value: [ThermalZone] - """ - self._thermal_zones = value - @property def heated_volume(self): """ @@ -368,9 +331,11 @@ class Building(CityObject): Get building heated flag :return: Boolean """ - if self.thermal_zones is None: + if self.internal_zones is None: return False - for thermal_zone in self.thermal_zones: - if thermal_zone.hvac_system is not None: - return True + for internal_zone in self.internal_zones: + if internal_zone.usage_zones is not None: + for usage_zone in internal_zone.usage_zones: + if usage_zone.thermal_control is not None: + return True return False diff --git a/city_model_structure/building_demand/appliances.py b/city_model_structure/building_demand/appliances.py index ed09f795..8fd4f0f6 100644 --- a/city_model_structure/building_demand/appliances.py +++ b/city_model_structure/building_demand/appliances.py @@ -3,7 +3,7 @@ Appliances module SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2022 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ -from typing import Union +from typing import Union, List from city_model_structure.attributes.schedule import Schedule @@ -12,28 +12,28 @@ class Appliances: Appliances class """ def __init__(self): - self._lighting_density = None + self._appliances_density = None self._convective_fraction = None - self._radiant_fraction = None + self._radiative_fraction = None self._latent_fraction = None - self._schedule = None + self._schedules = None @property - def lighting_density(self) -> Union[None, float]: + def appliances_density(self) -> Union[None, float]: """ - Get lighting density in Watts per m2 + Get appliances density in Watts per m2 :return: None or float """ - return self._lighting_density + return self._appliances_density - @lighting_density.setter - def lighting_density(self, value): + @appliances_density.setter + def appliances_density(self, value): """ - Set lighting density in Watts per m2 + Set appliances density in Watts per m2 :param value: float """ if value is not None: - self._lighting_density = float(value) + self._appliances_density = float(value) @property def convective_fraction(self) -> Union[None, float]: @@ -53,21 +53,21 @@ class Appliances: self._convective_fraction = float(value) @property - def radiant_fraction(self) -> Union[None, float]: + def radiative_fraction(self) -> Union[None, float]: """ Get radiant fraction :return: None or float """ - return self._radiant_fraction + return self._radiative_fraction - @radiant_fraction.setter - def radiant_fraction(self, value): + @radiative_fraction.setter + def radiative_fraction(self, value): """ Set radiant fraction :param value: float """ if value is not None: - self._radiant_fraction = float(value) + self._radiative_fraction = float(value) @property def latent_fraction(self) -> Union[None, float]: @@ -87,17 +87,17 @@ class Appliances: self._latent_fraction = float(value) @property - def schedule(self) -> Union[None, Schedule]: + def schedules(self) -> Union[None, List[Schedule]]: """ - Get schedule - :return: None or Schedule + Get schedules + :return: None or [Schedule] """ - return self._schedule + return self._schedules - @schedule.setter - def schedule(self, value): + @schedules.setter + def schedules(self, value): """ - Set schedule - :param value: Schedule + Set schedules + :param value: [Schedule] """ - self._schedule = value + self._schedules = value diff --git a/city_model_structure/building_demand/internal_gains.py b/city_model_structure/building_demand/internal_gains.py index 4cfeabb9..9384cf21 100644 --- a/city_model_structure/building_demand/internal_gains.py +++ b/city_model_structure/building_demand/internal_gains.py @@ -4,7 +4,7 @@ SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@concordia.ca """ -from typing import Union +from typing import Union, List from city_model_structure.attributes.schedule import Schedule @@ -19,7 +19,7 @@ class InternalGains: self._convective_fraction = None self._radiative_fraction = None self._latent_fraction = None - self._schedule = None + self._schedules = None @property def type(self) -> Union[None, str]: @@ -107,17 +107,17 @@ class InternalGains: self._latent_fraction = float(value) @property - def schedule(self) -> Union[None, Schedule]: + def schedules(self) -> Union[None, List[Schedule]]: """ Get internal gain schedule - :return: Schedule + :return: [Schedule] """ - return self._schedule + return self._schedules - @schedule.setter - def schedule(self, value): + @schedules.setter + def schedules(self, value): """ Set internal gain schedule - :param value: Schedule + :param value: [Schedule] """ - self._schedule = value + self._schedules = value diff --git a/city_model_structure/building_demand/internal_zone.py b/city_model_structure/building_demand/internal_zone.py index 222174bb..74678473 100644 --- a/city_model_structure/building_demand/internal_zone.py +++ b/city_model_structure/building_demand/internal_zone.py @@ -3,9 +3,13 @@ InternalZone module. It saves the original geometrical information from interior SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2022 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ + import uuid +from typing import Union, List from city_model_structure.building_demand.usage_zone import UsageZone +from city_model_structure.building_demand.thermal_zone import ThermalZone from city_model_structure.attributes.polyhedron import Polyhedron +from city_model_structure.energy_systems.hvac_system import HvacSystem class InternalZone: @@ -18,7 +22,9 @@ class InternalZone: self._geometry = None self._volume = None self._area = area - self._usage_zones = [] + self._thermal_zones = None + self._usage_zones = None + self._hvac_system = None @property def id(self): @@ -82,3 +88,36 @@ class InternalZone: :param value: [UsageZone] """ self._usage_zones = value + + @property + def hvac_system(self) -> Union[None, HvacSystem]: + """ + Get HVAC system installed for this thermal zone + :return: None or HvacSystem + """ + return self._hvac_system + + @hvac_system.setter + def hvac_system(self, value): + """ + Set HVAC system installed for this thermal zone + :param value: HvacSystem + """ + self._hvac_system = value + + @property + def thermal_zones(self) -> Union[None, List[ThermalZone]]: + """ + Get building thermal zones + :return: [ThermalZone] + """ + return self._thermal_zones + + @thermal_zones.setter + def thermal_zones(self, value): + """ + Set city object thermal zones + :param value: [ThermalZone] + """ + self._thermal_zones = value + diff --git a/city_model_structure/building_demand/lighting.py b/city_model_structure/building_demand/lighting.py index 0407a432..0e72249f 100644 --- a/city_model_structure/building_demand/lighting.py +++ b/city_model_structure/building_demand/lighting.py @@ -3,7 +3,7 @@ Lighting module SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2022 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ -from typing import Union +from typing import Union, List from city_model_structure.attributes.schedule import Schedule @@ -14,9 +14,9 @@ class Lighting: def __init__(self): self._lighting_density = None self._convective_fraction = None - self._radiant_fraction = None + self._radiative_fraction = None self._latent_fraction = None - self._schedule = None + self._schedules = None @property def lighting_density(self) -> Union[None, float]: @@ -53,21 +53,21 @@ class Lighting: self._convective_fraction = float(value) @property - def radiant_fraction(self) -> Union[None, float]: + def radiative_fraction(self) -> Union[None, float]: """ Get radiant fraction :return: None or float """ - return self._radiant_fraction + return self._radiative_fraction - @radiant_fraction.setter - def radiant_fraction(self, value): + @radiative_fraction.setter + def radiative_fraction(self, value): """ Set radiant fraction :param value: float """ if value is not None: - self._radiant_fraction = float(value) + self._radiative_fraction = float(value) @property def latent_fraction(self) -> Union[None, float]: @@ -87,17 +87,17 @@ class Lighting: self._latent_fraction = float(value) @property - def schedule(self) -> Union[None, Schedule]: + def schedules(self) -> Union[None, List[Schedule]]: """ - Get schedule - :return: None or Schedule + Get schedules + :return: None or [Schedule] """ - return self._schedule + return self._schedules - @schedule.setter - def schedule(self, value): + @schedules.setter + def schedules(self, value): """ - Set schedule - :param value: Schedule + Set schedules + :param value: [Schedule] """ - self._schedule = value + self._schedules = value diff --git a/city_model_structure/building_demand/occupancy.py b/city_model_structure/building_demand/occupancy.py index 443030ca..ec9ebf1e 100644 --- a/city_model_structure/building_demand/occupancy.py +++ b/city_model_structure/building_demand/occupancy.py @@ -15,9 +15,9 @@ class Occupancy: def __init__(self): self._occupancy_density = None self._sensible_convective_internal_gain = None - self._sensible_radiant_internal_gain = None + self._sensible_radiative_internal_gain = None self._latent_internal_gain = None - self._occupancy_schedule = None + self._occupancy_schedules = None self._occupants = None @property @@ -55,21 +55,21 @@ class Occupancy: self._sensible_convective_internal_gain = float(value) @property - def sensible_radiant_internal_gain(self) -> Union[None, float]: + def sensible_radiative_internal_gain(self) -> Union[None, float]: """ Get sensible radiant internal gain in Watts per m2 :return: None or float """ - return self._sensible_radiant_internal_gain + return self._sensible_radiative_internal_gain - @sensible_radiant_internal_gain.setter - def sensible_radiant_internal_gain(self, value): + @sensible_radiative_internal_gain.setter + def sensible_radiative_internal_gain(self, value): """ Set sensible radiant internal gain in Watts per m2 :param value: float """ if value is not None: - self._sensible_radiant_internal_gain = float(value) + self._sensible_radiative_internal_gain = float(value) @property def latent_internal_gain(self) -> Union[None, float]: @@ -89,20 +89,20 @@ class Occupancy: self._latent_internal_gain = float(value) @property - def occupancy_schedule(self) -> Union[None, Schedule]: + def occupancy_schedules(self) -> Union[None, List[Schedule]]: """ - Get occupancy schedule - :return: None or Schedule + Get occupancy schedules + :return: None or [Schedule] """ - return self._occupancy_schedule + return self._occupancy_schedules - @occupancy_schedule.setter - def occupancy_schedule(self, value): + @occupancy_schedules.setter + def occupancy_schedules(self, value): """ - Set occupancy schedule - :param value: Schedule + Set occupancy schedules + :param value: [Schedule] """ - self._occupancy_schedule = value + self._occupancy_schedules = value @property def occupants(self) -> Union[None, List[Occupant]]: diff --git a/city_model_structure/building_demand/thermal_control.py b/city_model_structure/building_demand/thermal_control.py index 20888023..0c4c8a62 100644 --- a/city_model_structure/building_demand/thermal_control.py +++ b/city_model_structure/building_demand/thermal_control.py @@ -3,7 +3,7 @@ ThermalControl module SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2022 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ -from typing import Union +from typing import Union, List from city_model_structure.attributes.schedule import Schedule @@ -12,35 +12,59 @@ class ThermalControl: ThermalControl class """ def __init__(self): - self._heating_set_point = None - self._cooling_set_point = None - self._hvac_availability = None + self._mean_heating_set_point = None self._heating_set_back = None + self._mean_cooling_set_point = None + self._hvac_availability_schedules = None + self._heating_set_point_schedules = None + self._cooling_set_point_schedules = None + + @staticmethod + def _maximum_value(schedules): + maximum = -1000 + for schedule in schedules: + for value in schedule.values: + if value > maximum: + maximum = value + return maximum + + @staticmethod + def _minimum_value(schedules): + minimum = 1000 + for schedule in schedules: + for value in schedule.values: + if value < minimum: + minimum = value + return minimum @property - def heating_set_point(self) -> Union[None, Schedule]: + def mean_heating_set_point(self) -> Union[None, float]: """ Get heating set point defined for a thermal zone in Celsius - :return: None or Schedule + :return: None or float """ - return self._heating_set_point + if self._mean_heating_set_point is None: + if self.heating_set_point_schedules is not None: + self._mean_heating_set_point = self._maximum_value(self.heating_set_point_schedules) + return self._mean_heating_set_point - @heating_set_point.setter - def heating_set_point(self, value): + @mean_heating_set_point.setter + def mean_heating_set_point(self, value): """ Set heating set point defined for a thermal zone in Celsius - :param value: Schedule + :param value: float """ - self._heating_set_point = value + self._mean_heating_set_point = value @property def heating_set_back(self) -> Union[None, float]: """ Get heating set back defined for a thermal zone in Celsius - Heating set back is the only parameter which is not a schedule as it is either one value or it is implicit in the - set point schedule :return: None or float """ + if self._heating_set_back is None: + if self.heating_set_point_schedules is not None: + self._heating_set_back = self._minimum_value(self.heating_set_point_schedules) return self._heating_set_back @heating_set_back.setter @@ -53,34 +77,68 @@ class ThermalControl: self._heating_set_back = float(value) @property - def cooling_set_point(self) -> Union[None, Schedule]: + def mean_cooling_set_point(self) -> Union[None, float]: """ Get cooling set point defined for a thermal zone in Celsius - :return: None or Schedule + :return: None or float """ - return self._cooling_set_point + if self._mean_cooling_set_point is None: + if self.cooling_set_point_schedules is not None: + self._mean_cooling_set_point = self._minimum_value(self.cooling_set_point_schedules) + return self._mean_cooling_set_point - @cooling_set_point.setter - def cooling_set_point(self, value): + @mean_cooling_set_point.setter + def mean_cooling_set_point(self, value): """ Set cooling set point defined for a thermal zone in Celsius - :param value: Schedule + :param value: float """ - self._cooling_set_point = value + self._mean_cooling_set_point = value @property - def hvac_availability(self) -> Union[None, Schedule]: + def hvac_availability_schedules(self) -> Union[None, List[Schedule]]: """ Get the availability of the conditioning system defined for a thermal zone - :return: None or Schedule + :return: None or [Schedule] """ - return self._hvac_availability + return self._hvac_availability_schedules - @hvac_availability.setter - def hvac_availability(self, value): + @hvac_availability_schedules.setter + def hvac_availability_schedules(self, value): """ Set the availability of the conditioning system defined for a thermal zone - :param value: Schedule + :param value: [Schedule] """ - self._hvac_availability = value + self._hvac_availability_schedules = value + @property + def heating_set_point_schedules(self) -> Union[None, List[Schedule]]: + """ + Get heating set point schedule defined for a thermal zone in Celsius + :return: None or [Schedule] + """ + return self._heating_set_point_schedules + + @heating_set_point_schedules.setter + def heating_set_point_schedules(self, value): + """ + Set heating set point schedule defined for a thermal zone in Celsius + :param value: [Schedule] + """ + self._heating_set_point_schedules = value + + @property + def cooling_set_point_schedules(self) -> Union[None, List[Schedule]]: + """ + Get cooling set point schedule defined for a thermal zone in Celsius + :return: None or [Schedule] + """ + return self._cooling_set_point_schedules + + @cooling_set_point_schedules.setter + def cooling_set_point_schedules(self, value): + """ + Set cooling set point schedule defined for a thermal zone in Celsius + :param value: [Schedule] + """ + self._cooling_set_point_schedules = value diff --git a/city_model_structure/building_demand/thermal_zone.py b/city_model_structure/building_demand/thermal_zone.py index dd20bc28..10f8ac0b 100644 --- a/city_model_structure/building_demand/thermal_zone.py +++ b/city_model_structure/building_demand/thermal_zone.py @@ -5,11 +5,11 @@ Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@conc Contributors Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ import uuid -from typing import List, Union, Tuple, TypeVar +from typing import List, Union, TypeVar from city_model_structure.building_demand.usage_zone import UsageZone +from city_model_structure.attributes.schedule import Schedule from city_model_structure.building_demand.thermal_control import ThermalControl from city_model_structure.energy_systems.hvac_system import HvacSystem -from city_model_structure.attributes.schedule import Schedule ThermalBoundary = TypeVar('ThermalBoundary') @@ -27,11 +27,12 @@ class ThermalZone: self._indirectly_heated_area_ratio = None self._infiltration_rate_system_on = None self._infiltration_rate_system_off = None - self._usage_zones = None self._volume = volume self._ordinate_number = None + self._usage_zones = None self._thermal_control = None self._hvac_system = None + self._view_factors_matrix = None @property def id(self): @@ -142,22 +143,6 @@ class ThermalZone: """ self._infiltration_rate_system_off = value - @property - def usage_zones(self) -> Tuple[float, UsageZone]: - """ - Get list of usage zones and the percentage of thermal zone's volume affected by that usage - :return: [UsageZone] - """ - return self._usage_zones - - @usage_zones.setter - def usage_zones(self, values): - """ - Set list of usage zones and the percentage of thermal zone's volume affected by that usage - :param values: Tuple[float, UsageZone] - """ - self._usage_zones = values - @property def volume(self): """ @@ -183,10 +168,29 @@ class ThermalZone: if value is not None: self._ordinate_number = int(value) + @property + def usage_zones(self) -> [UsageZone]: + """ + Get list of usage zones and the percentage of thermal zone's volume affected by that usage + From internal_zone + :return: [UsageZone] + """ + return self._usage_zones + + @usage_zones.setter + def usage_zones(self, values): + """ + Set list of usage zones and the percentage of thermal zone's volume affected by that usage + From internal_zone + :param values: [UsageZone] + """ + self._usage_zones = values + @property def thermal_control(self) -> Union[None, ThermalControl]: """ Get thermal control of this thermal zone + From internal_zone :return: None or ThermalControl """ return self._thermal_control @@ -195,6 +199,7 @@ class ThermalZone: def thermal_control(self, value): """ Set thermal control for this thermal zone + From internal_zone :param value: ThermalControl """ self._thermal_control = value @@ -203,6 +208,7 @@ class ThermalZone: def hvac_system(self) -> Union[None, HvacSystem]: """ Get HVAC system installed for this thermal zone + From internal_zone :return: None or HvacSystem """ return self._hvac_system @@ -211,6 +217,23 @@ class ThermalZone: def hvac_system(self, value): """ Set HVAC system installed for this thermal zone + From internal_zone :param value: HvacSystem """ self._hvac_system = value + + @property + def view_factors_matrix(self): + """ + Get thermal zone view factors matrix + :return: [[float]] + """ + return self._view_factors_matrix + + @view_factors_matrix.setter + def view_factors_matrix(self, value): + """ + Set thermal zone view factors matrix + :param value: [[float]] + """ + self._view_factors_matrix = value diff --git a/city_model_structure/building_demand/usage_zone.py b/city_model_structure/building_demand/usage_zone.py index 590ce6d5..f8d77890 100644 --- a/city_model_structure/building_demand/usage_zone.py +++ b/city_model_structure/building_demand/usage_zone.py @@ -11,6 +11,7 @@ from city_model_structure.building_demand.internal_gains import InternalGains from city_model_structure.building_demand.occupancy import Occupancy from city_model_structure.building_demand.lighting import Lighting from city_model_structure.building_demand.appliances import Appliances +from city_model_structure.building_demand.thermal_control import ThermalControl class UsageZone: @@ -20,6 +21,7 @@ class UsageZone: def __init__(self): self._id = None self._usage = None + self._percentage = None self._not_detailed_source_mean_annual_internal_gains = None self._hours_day = None self._days_year = None @@ -29,6 +31,7 @@ class UsageZone: self._lighting = None self._appliances = None self._internal_gains = None + self._thermal_control = None @property def id(self): @@ -40,6 +43,40 @@ class UsageZone: self._id = uuid.uuid4() return self._id + @property + def usage(self) -> Union[None, str]: + """ + Get usage zone usage + :return: None or str + """ + return self._usage + + @usage.setter + def usage(self, value): + """ + Set usage zone usage + :param value: str + """ + if value is not None: + self._usage = str(value) + + @property + def percentage(self): + """ + Get usage zone percentage in range[0,1] + :return: float + """ + return self._percentage + + @percentage.setter + def percentage(self, value): + """ + Set usage zone percentage in range[0,1] + :param value: float + """ + if value is not None: + self._percentage = float(value) + @property def not_detailed_source_mean_annual_internal_gains(self) -> List[InternalGains]: """ @@ -107,23 +144,6 @@ class UsageZone: if value is not None: self._mechanical_air_change = float(value) - @property - def usage(self) -> Union[None, str]: - """ - Get usage zone usage - :return: None or str - """ - return self._usage - - @usage.setter - def usage(self, value): - """ - Set usage zone usage - :param value: str - """ - if value is not None: - self._usage = str(value) - @property def electrical_app_average_consumption_sqm_year(self) -> Union[None, float]: """ @@ -199,22 +219,22 @@ class UsageZone: _internal_gain = InternalGains() _internal_gain.type = cte.OCCUPANCY _total_heat_gain = (self.occupancy.sensible_convective_internal_gain - + self.occupancy.sensible_radiant_internal_gain + + self.occupancy.sensible_radiative_internal_gain + self.occupancy.latent_internal_gain) _internal_gain.average_internal_gain = _total_heat_gain _internal_gain.latent_fraction = self.occupancy.latent_internal_gain / _total_heat_gain - _internal_gain.radiative_fraction = self.occupancy.sensible_radiant_internal_gain / _total_heat_gain + _internal_gain.radiative_fraction = self.occupancy.sensible_radiative_internal_gain / _total_heat_gain _internal_gain.convective_fraction = self.occupancy.sensible_convective_internal_gain / _total_heat_gain - _internal_gain.schedule = self.occupancy.occupancy_schedule + _internal_gain.schedules = self.occupancy.occupancy_schedules self._internal_gains = [_internal_gain] if self.lighting is not None: _internal_gain = InternalGains() _internal_gain.type = cte.LIGHTING _internal_gain.average_internal_gain = self.lighting.lighting_density _internal_gain.latent_fraction = self.lighting.latent_fraction - _internal_gain.radiative_fraction = self.lighting.radiant_fraction + _internal_gain.radiative_fraction = self.lighting.radiative_fraction _internal_gain.convective_fraction = self.lighting.convective_fraction - _internal_gain.schedule = self.lighting.schedule + _internal_gain.schedules = self.lighting.schedules if self._internal_gains is not None: self._internal_gains.append(_internal_gain) else: @@ -222,13 +242,29 @@ class UsageZone: if self.appliances is not None: _internal_gain = InternalGains() _internal_gain.type = cte.APPLIANCES - _internal_gain.average_internal_gain = self.appliances.lighting_density + _internal_gain.average_internal_gain = self.appliances.appliances_density _internal_gain.latent_fraction = self.appliances.latent_fraction - _internal_gain.radiative_fraction = self.appliances.radiant_fraction + _internal_gain.radiative_fraction = self.appliances.radiative_fraction _internal_gain.convective_fraction = self.appliances.convective_fraction - _internal_gain.schedule = self.appliances.schedule + _internal_gain.schedules = self.appliances.schedules if self._internal_gains is not None: self._internal_gains.append(_internal_gain) else: self._internal_gains = [_internal_gain] return self._internal_gains + + @property + def thermal_control(self) -> Union[None, ThermalControl]: + """ + Get thermal control of this thermal zone + :return: None or ThermalControl + """ + return self._thermal_control + + @thermal_control.setter + def thermal_control(self, value): + """ + Set thermal control for this thermal zone + :param value: ThermalControl + """ + self._thermal_control = value diff --git a/data/usage/comnet_schedules_archetypes.xlsx b/data/usage/comnet_schedules_archetypes.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..8fce488c35d61f8850ef4ecbc7a22e9283c1bb83 GIT binary patch literal 348793 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

+gyg4|YFIo;ZOwNpRw60$Scn&J}G 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�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

Q)Lz*ie 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 DIN 18599-10 16.0 16.0 16.0 16.0 16.0 16.0 20.0 20.0 20.0 20.0 20.0 20.0 20.0 20.0 20.0 20.0 20.0 - 20.0 20.0 20.0 20.0 20.0 20.0 0.0 + 20.0 20.0 20.0 20.0 20.0 20.0 20.0 @@ -117,7 +117,7 @@ 16.0 16.0 16.0 16.0 16.0 16.0 20.0 20.0 20.0 20.0 20.0 20.0 20.0 20.0 20.0 20.0 20.0 - 20.0 20.0 20.0 20.0 20.0 20.0 0.0 + 20.0 20.0 20.0 20.0 20.0 20.0 20.0 diff --git a/helpers/constants.py b/helpers/constants.py index b55cf955..684a367f 100644 --- a/helpers/constants.py +++ b/helpers/constants.py @@ -11,6 +11,7 @@ KELVIN = 273.15 HOUR_TO_MINUTES = 60 METERS_TO_FEET = 3.28084 BTU_H_TO_WATTS = 0.29307107 +KILO_WATTS_HOUR_TO_JULES = 3600000 # time SECOND = 'second' diff --git a/imports/construction/ca_physics_parameters.py b/imports/construction/ca_physics_parameters.py index d04c81ea..5b2ff50c 100644 --- a/imports/construction/ca_physics_parameters.py +++ b/imports/construction/ca_physics_parameters.py @@ -25,23 +25,25 @@ class CaPhysicsParameters(NrelPhysicsInterface): city = self._city # it is assumed that all buildings have the same archetypes' keys for building in city.buildings: - archetype = self._search_archetype(ConstructionHelper.nrcan_from_function(building.function), - building.year_of_construction) - if archetype is None: + try: + archetype = self._search_archetype(ConstructionHelper.nrcan_from_function(building.function), + building.year_of_construction) + except KeyError: sys.stderr.write(f'Building {building.name} has unknown archetype for building function: ' f'{ConstructionHelper.nrcan_from_function(building.function)} ' f'and building year of construction: {building.year_of_construction}\n') - continue + return # if building has no thermal zones defined from geometry, one thermal zone per storey is assigned - if building.thermal_zones is None: - self._create_storeys(building, archetype) - thermal_zones = [] - for storey in building.storeys: - thermal_zones.append(storey.thermal_zone) - building.thermal_zones = thermal_zones + if len(building.internal_zones) == 1: + if building.internal_zones[0].thermal_zones is None: + self._create_storeys(building, archetype) + thermal_zones = [] + for storey in building.storeys: + thermal_zones.append(storey.thermal_zone) + building.internal_zones[0].thermal_zones = thermal_zones - self._assign_values(building, archetype) + self._assign_values(building.internal_zones, archetype) def _search_archetype(self, function, year_of_construction): for building_archetype in self._building_archetypes: @@ -54,31 +56,32 @@ class CaPhysicsParameters(NrelPhysicsInterface): return building_archetype return None - def _assign_values(self, building, archetype): - for thermal_zone in building.thermal_zones: - thermal_zone.additional_thermal_bridge_u_value = archetype.additional_thermal_bridge_u_value - thermal_zone.effective_thermal_capacity = archetype.effective_thermal_capacity - thermal_zone.indirectly_heated_area_ratio = archetype.indirectly_heated_area_ratio - thermal_zone.infiltration_rate_system_on = archetype.infiltration_rate_system_on - thermal_zone.infiltration_rate_system_off = archetype.infiltration_rate_system_off - for thermal_boundary in thermal_zone.thermal_boundaries: - construction_type = ConstructionHelper.nrcan_construction_types[thermal_boundary.type] - thermal_boundary_archetype = self._search_construction_in_archetype(archetype, construction_type) - thermal_boundary.u_value = thermal_boundary_archetype.overall_u_value - thermal_boundary.outside_solar_absorptance = thermal_boundary_archetype.outside_solar_absorptance - thermal_boundary.construction_name = thermal_boundary_archetype.construction_name - try: - thermal_boundary.window_ratio = thermal_boundary_archetype.window_ratio - except ValueError: - # This is the normal operation way when the windows are defined in the geometry - continue - if thermal_boundary.thermal_openings is not None: - for thermal_opening in thermal_boundary.thermal_openings: - if thermal_boundary_archetype.thermal_opening_archetype is not None: - thermal_opening_archetype = thermal_boundary_archetype.thermal_opening_archetype - thermal_opening.frame_ratio = thermal_opening_archetype.frame_ratio - thermal_opening.g_value = thermal_opening_archetype.g_value - thermal_opening.overall_u_value = thermal_opening_archetype.overall_u_value + def _assign_values(self, internal_zones, archetype): + for internal_zone in internal_zones: + for thermal_zone in internal_zone.thermal_zones: + thermal_zone.additional_thermal_bridge_u_value = archetype.additional_thermal_bridge_u_value + thermal_zone.effective_thermal_capacity = archetype.effective_thermal_capacity + thermal_zone.indirectly_heated_area_ratio = archetype.indirectly_heated_area_ratio + thermal_zone.infiltration_rate_system_on = archetype.infiltration_rate_system_on + thermal_zone.infiltration_rate_system_off = archetype.infiltration_rate_system_off + for thermal_boundary in thermal_zone.thermal_boundaries: + construction_type = ConstructionHelper.nrcan_construction_types[thermal_boundary.type] + thermal_boundary_archetype = self._search_construction_in_archetype(archetype, construction_type) + thermal_boundary.u_value = thermal_boundary_archetype.overall_u_value + thermal_boundary.outside_solar_absorptance = thermal_boundary_archetype.outside_solar_absorptance + thermal_boundary.construction_name = thermal_boundary_archetype.construction_name + try: + thermal_boundary.window_ratio = thermal_boundary_archetype.window_ratio + except ValueError: + # This is the normal operation way when the windows are defined in the geometry + continue + if thermal_boundary.thermal_openings is not None: + for thermal_opening in thermal_boundary.thermal_openings: + if thermal_boundary_archetype.thermal_opening_archetype is not None: + thermal_opening_archetype = thermal_boundary_archetype.thermal_opening_archetype + thermal_opening.frame_ratio = thermal_opening_archetype.frame_ratio + thermal_opening.g_value = thermal_opening_archetype.g_value + thermal_opening.overall_u_value = thermal_opening_archetype.overall_u_value @staticmethod def _create_storeys(building, archetype): diff --git a/imports/construction/helpers/construction_helper.py b/imports/construction/helpers/construction_helper.py index 0cf05326..32402984 100644 --- a/imports/construction/helpers/construction_helper.py +++ b/imports/construction/helpers/construction_helper.py @@ -12,7 +12,7 @@ class ConstructionHelper: Construction helper """ # NREL - function_to_nrel = { + _function_to_nrel = { cte.RESIDENTIAL: 'residential', cte.SFH: 'single family house', cte.MFH: 'multifamily house', @@ -27,12 +27,12 @@ class ConstructionHelper: cte.OFFICE: 'office', cte.LARGE_OFFICE: 'large office' } - nrel_function_default_value = 'residential' - nrel_standards = { + + _nrel_standards = { 'ASHRAE Std189': 1, 'ASHRAE 90.1_2004': 2 } - reference_city_to_nrel_climate_zone = { + _reference_city_to_nrel_climate_zone = { 'Miami': 'ASHRAE_2004:1A', 'Houston': 'ASHRAE_2004:2A', 'Phoenix': 'ASHRAE_2004:2B', @@ -51,6 +51,7 @@ class ConstructionHelper: 'Fairbanks': 'ASHRAE_2004:8A' } nrel_window_types = [cte.WINDOW, cte.DOOR, cte.SKYLIGHT] + nrel_construction_types = { cte.WALL: 'exterior wall', cte.INTERIOR_WALL: 'interior wall', @@ -62,7 +63,7 @@ class ConstructionHelper: } # NRCAN - function_to_nrcan = { + _function_to_nrcan = { cte.RESIDENTIAL: 'residential', cte.SFH: 'single family house', cte.MFH: 'multifamily house', @@ -78,8 +79,9 @@ class ConstructionHelper: cte.LARGE_OFFICE: 'large office', cte.OFFICE_WORKSHOP: 'residential' } - nrcan_function_default_value = 'residential' + nrcan_window_types = [cte.WINDOW] + nrcan_construction_types = { cte.WALL: 'wall', cte.GROUND_WALL: 'basement_wall', @@ -97,10 +99,9 @@ class ConstructionHelper: :return: str """ try: - return ConstructionHelper.function_to_nrel[function] + return ConstructionHelper._function_to_nrel[function] except KeyError: - sys.stderr.write('Error: keyword not found. Returned default NREL function "residential"\n') - return ConstructionHelper.nrel_function_default_value + sys.stderr.write('Error: keyword not found.\n') @staticmethod def yoc_to_nrel_standard(year_of_construction): @@ -136,7 +137,7 @@ class ConstructionHelper: :return: str """ reference_city = ConstructionHelper.city_to_reference_city(city) - return ConstructionHelper.reference_city_to_nrel_climate_zone[reference_city] + return ConstructionHelper._reference_city_to_nrel_climate_zone[reference_city] @staticmethod def nrcan_from_function(function): @@ -146,7 +147,6 @@ class ConstructionHelper: :return: str """ try: - return ConstructionHelper.function_to_nrcan[function] + return ConstructionHelper._function_to_nrcan[function] except KeyError: - sys.stderr.write('Error: keyword not found. Returned default NRCAN function "residential"\n') - return ConstructionHelper.nrcan_function_default_value + sys.stderr.write('Error: keyword not found.\n') diff --git a/imports/construction/us_physics_parameters.py b/imports/construction/us_physics_parameters.py index 6e19e96d..de79697b 100644 --- a/imports/construction/us_physics_parameters.py +++ b/imports/construction/us_physics_parameters.py @@ -33,23 +33,25 @@ class UsPhysicsParameters(NrelPhysicsInterface): building_type = ConstructionHelper.nrel_from_function(building.function) if building_type is None: return - archetype = self._search_archetype(building_type, - ConstructionHelper.yoc_to_nrel_standard(building.year_of_construction), - self._climate_zone) - if archetype is None: + try: + archetype = self._search_archetype(building_type, + ConstructionHelper.yoc_to_nrel_standard(building.year_of_construction), + self._climate_zone) + except KeyError: sys.stderr.write(f'Building {building.name} has unknown archetype for building function: {building.function} ' f'and building year of construction: {building.year_of_construction}\n') - continue + return # if building has no thermal zones defined from geometry, one thermal zone per storey is assigned - if building.thermal_zones is None: - self._create_storeys(building, archetype) - thermal_zones = [] - for storey in building.storeys: - thermal_zones.append(storey.thermal_zone) - building.thermal_zones = thermal_zones + if len(building.internal_zones) == 1: + if building.internal_zones[0].thermal_zones is None: + self._create_storeys(building, archetype) + thermal_zones = [] + for storey in building.storeys: + thermal_zones.append(storey.thermal_zone) + building.internal_zones[0].thermal_zones = thermal_zones - self._assign_values(building, archetype) + self._assign_values(building.internal_zones, archetype) def _search_archetype(self, building_type, standard, climate_zone): for building_archetype in self._building_archetypes: @@ -60,52 +62,53 @@ class UsPhysicsParameters(NrelPhysicsInterface): return building_archetype return None - def _assign_values(self, building, archetype): - for thermal_zone in building.thermal_zones: - thermal_zone.additional_thermal_bridge_u_value = archetype.additional_thermal_bridge_u_value - thermal_zone.effective_thermal_capacity = archetype.effective_thermal_capacity - thermal_zone.indirectly_heated_area_ratio = archetype.indirectly_heated_area_ratio - thermal_zone.infiltration_rate_system_on = archetype.infiltration_rate_system_on - thermal_zone.infiltration_rate_system_off = archetype.infiltration_rate_system_off - for thermal_boundary in thermal_zone.thermal_boundaries: - construction_type = ConstructionHelper.nrel_construction_types[thermal_boundary.type] - thermal_boundary_archetype = self._search_construction_in_archetype(archetype, construction_type) - thermal_boundary.outside_solar_absorptance = thermal_boundary_archetype.outside_solar_absorptance - thermal_boundary.outside_thermal_absorptance = thermal_boundary_archetype.outside_thermal_absorptance - thermal_boundary.outside_visible_absorptance = thermal_boundary_archetype.outside_visible_absorptance - thermal_boundary.construction_name = thermal_boundary_archetype.construction_name - try: - thermal_boundary.window_ratio = thermal_boundary_archetype.window_ratio - except ValueError: - # This is the normal operation way when the windows are defined in the geometry - continue - thermal_boundary.layers = [] - for layer_archetype in thermal_boundary_archetype.layers: - layer = Layer() - layer.thickness = layer_archetype.thickness - material = Material() - material.name = layer_archetype.name - material.no_mass = layer_archetype.no_mass - material.density = layer_archetype.density - material.conductivity = layer_archetype.conductivity - material.specific_heat = layer_archetype.specific_heat - material.solar_absorptance = layer_archetype.solar_absorptance - material.thermal_absorptance = layer_archetype.thermal_absorptance - material.visible_absorptance = layer_archetype.visible_absorptance - material.thermal_resistance = layer_archetype.thermal_resistance - layer.material = material - thermal_boundary.layers.append(layer) - for thermal_opening in thermal_boundary.thermal_openings: - if thermal_boundary_archetype.thermal_opening_archetype is not None: - thermal_opening_archetype = thermal_boundary_archetype.thermal_opening_archetype - thermal_opening.frame_ratio = thermal_opening_archetype.frame_ratio - thermal_opening.g_value = thermal_opening_archetype.g_value - thermal_opening.conductivity = thermal_opening_archetype.conductivity - thermal_opening.thickness = thermal_opening_archetype.thickness - thermal_opening.back_side_solar_transmittance_at_normal_incidence = \ - thermal_opening_archetype.back_side_solar_transmittance_at_normal_incidence - thermal_opening.front_side_solar_transmittance_at_normal_incidence = \ - thermal_opening_archetype.front_side_solar_transmittance_at_normal_incidence + def _assign_values(self, internal_zones, archetype): + for internal_zone in internal_zones: + for thermal_zone in internal_zone.thermal_zones: + thermal_zone.additional_thermal_bridge_u_value = archetype.additional_thermal_bridge_u_value + thermal_zone.effective_thermal_capacity = archetype.effective_thermal_capacity + thermal_zone.indirectly_heated_area_ratio = archetype.indirectly_heated_area_ratio + thermal_zone.infiltration_rate_system_on = archetype.infiltration_rate_system_on + thermal_zone.infiltration_rate_system_off = archetype.infiltration_rate_system_off + for thermal_boundary in thermal_zone.thermal_boundaries: + construction_type = ConstructionHelper.nrel_construction_types[thermal_boundary.type] + thermal_boundary_archetype = self._search_construction_in_archetype(archetype, construction_type) + thermal_boundary.outside_solar_absorptance = thermal_boundary_archetype.outside_solar_absorptance + thermal_boundary.outside_thermal_absorptance = thermal_boundary_archetype.outside_thermal_absorptance + thermal_boundary.outside_visible_absorptance = thermal_boundary_archetype.outside_visible_absorptance + thermal_boundary.construction_name = thermal_boundary_archetype.construction_name + try: + thermal_boundary.window_ratio = thermal_boundary_archetype.window_ratio + except ValueError: + # This is the normal operation way when the windows are defined in the geometry + continue + thermal_boundary.layers = [] + for layer_archetype in thermal_boundary_archetype.layers: + layer = Layer() + layer.thickness = layer_archetype.thickness + material = Material() + material.name = layer_archetype.name + material.no_mass = layer_archetype.no_mass + material.density = layer_archetype.density + material.conductivity = layer_archetype.conductivity + material.specific_heat = layer_archetype.specific_heat + material.solar_absorptance = layer_archetype.solar_absorptance + material.thermal_absorptance = layer_archetype.thermal_absorptance + material.visible_absorptance = layer_archetype.visible_absorptance + material.thermal_resistance = layer_archetype.thermal_resistance + layer.material = material + thermal_boundary.layers.append(layer) + for thermal_opening in thermal_boundary.thermal_openings: + if thermal_boundary_archetype.thermal_opening_archetype is not None: + thermal_opening_archetype = thermal_boundary_archetype.thermal_opening_archetype + thermal_opening.frame_ratio = thermal_opening_archetype.frame_ratio + thermal_opening.g_value = thermal_opening_archetype.g_value + thermal_opening.conductivity = thermal_opening_archetype.conductivity + thermal_opening.thickness = thermal_opening_archetype.thickness + thermal_opening.back_side_solar_transmittance_at_normal_incidence = \ + thermal_opening_archetype.back_side_solar_transmittance_at_normal_incidence + thermal_opening.front_side_solar_transmittance_at_normal_incidence = \ + thermal_opening_archetype.front_side_solar_transmittance_at_normal_incidence @staticmethod def _create_storeys(building, archetype): diff --git a/imports/geometry/helpers/geometry_helper.py b/imports/geometry/helpers/geometry_helper.py index 9c432e15..104792d5 100644 --- a/imports/geometry/helpers/geometry_helper.py +++ b/imports/geometry/helpers/geometry_helper.py @@ -12,229 +12,207 @@ class GeometryHelper: Geometry helper """ # function - pluto_to_function = { - 'A0': 'single family house', - 'A1': 'single family house', - 'A2': 'single family house', - 'A3': 'single family house', - 'A4': 'single family house', - 'A5': 'single family house', - 'A6': 'single family house', - 'A7': 'single family house', - 'A8': 'single family house', - 'A9': 'single family house', - 'B1': 'multifamily house', - 'B2': 'multifamily house', - 'B3': 'multifamily house', - 'B9': 'multifamily house', - 'C0': 'residential', - 'C1': 'residential', - 'C2': 'residential', - 'C3': 'residential', - 'C4': 'residential', - 'C5': 'residential', - 'C6': 'residential', - 'C7': 'residential', - 'C8': 'residential', - 'C9': 'residential', - 'D0': 'residential', - 'D1': 'residential', - 'D2': 'residential', - 'D3': 'residential', - 'D4': 'residential', - 'D5': 'residential', - 'D6': 'residential', - 'D7': 'residential', - 'D8': 'residential', - 'D9': 'residential', - 'E1': 'warehouse', - 'E3': 'warehouse', - 'E4': 'warehouse', - 'E5': 'warehouse', - 'E7': 'warehouse', - 'E9': 'warehouse', - 'F1': 'warehouse', - 'F2': 'warehouse', - 'F4': 'warehouse', - 'F5': 'warehouse', - 'F8': 'warehouse', - 'F9': 'warehouse', - 'G0': 'office', - 'G1': 'office', - 'G2': 'office', - 'G3': 'office', - 'G4': 'office', - 'G5': 'office', - 'G6': 'office', - 'G7': 'office', - 'G8': 'office', - 'G9': 'office', - 'H1': 'hotel', - 'H2': 'hotel', - 'H3': 'hotel', - 'H4': 'hotel', - 'H5': 'hotel', - 'H6': 'hotel', - 'H7': 'hotel', - 'H8': 'hotel', - 'H9': 'hotel', - 'HB': 'hotel', - 'HH': 'hotel', - 'HR': 'hotel', - 'HS': 'hotel', - 'I1': 'hospital', - 'I2': 'outpatient', - 'I3': 'outpatient', - 'I4': 'residential', - 'I5': 'outpatient', - 'I6': 'outpatient', - 'I7': 'outpatient', - 'I9': 'outpatient', - 'J1': 'large office', - 'J2': 'large office', - 'J3': 'large office', - 'J4': 'large office', - 'J5': 'large office', - 'J6': 'large office', - 'J7': 'large office', - 'J8': 'large office', - 'J9': 'large office', - 'K1': 'strip mall', - 'K2': 'strip mall', - 'K3': 'strip mall', - 'K4': 'residential', - 'K5': 'restaurant', - 'K6': 'commercial', - 'K7': 'commercial', - 'K8': 'commercial', - 'K9': 'commercial', - 'L1': 'residential', - 'L2': 'residential', - 'L3': 'residential', - 'L8': 'residential', - 'L9': 'residential', - 'M1': 'large office', - 'M2': 'large office', - 'M3': 'large office', - 'M4': 'large office', - 'M9': 'large office', - 'N1': 'residential', - 'N2': 'residential', - 'N3': 'residential', - 'N4': 'residential', - 'N9': 'residential', - 'O1': 'office', - 'O2': 'office', - 'O3': 'office', - 'O4': 'office', - 'O5': 'office', - 'O6': 'office', - 'O7': 'office', - 'O8': 'office', - 'O9': 'office', - 'P1': 'large office', - 'P2': 'hotel', - 'P3': 'office', - 'P4': 'office', - 'P5': 'office', - 'P6': 'office', - 'P7': 'large office', - 'P8': 'large office', - 'P9': 'office', - 'Q0': 'office', - 'Q1': 'office', - 'Q2': 'office', - 'Q3': 'office', - 'Q4': 'office', - 'Q5': 'office', - 'Q6': 'office', - 'Q7': 'office', - 'Q8': 'office', - 'Q9': 'office', - 'R0': 'residential', - 'R1': 'residential', - 'R2': 'residential', - 'R3': 'residential', - 'R4': 'residential', - 'R5': 'residential', - 'R6': 'residential', - 'R7': 'residential', - 'R8': 'residential', - 'R9': 'residential', - 'RA': 'residential', - 'RB': 'residential', - 'RC': 'residential', - 'RD': 'residential', - 'RG': 'residential', - 'RH': 'residential', - 'RI': 'residential', - 'RK': 'residential', - 'RM': 'residential', - 'RR': 'residential', - 'RS': 'residential', - 'RW': 'residential', - 'RX': 'residential', - 'RZ': 'residential', - 'S0': 'residential', - 'S1': 'residential', - 'S2': 'residential', - 'S3': 'residential', - 'S4': 'residential', - 'S5': 'residential', - 'S9': 'residential', - 'T1': 'na', - 'T2': 'na', - 'T9': 'na', - 'U0': 'warehouse', - 'U1': 'warehouse', - 'U2': 'warehouse', - 'U3': 'warehouse', - 'U4': 'warehouse', - 'U5': 'warehouse', - 'U6': 'warehouse', - 'U7': 'warehouse', - 'U8': 'warehouse', - 'U9': 'warehouse', - 'V0': 'na', - 'V1': 'na', - 'V2': 'na', - 'V3': 'na', - 'V4': 'na', - 'V5': 'na', - 'V6': 'na', - 'V7': 'na', - 'V8': 'na', - 'V9': 'na', - 'W1': 'primary school', - 'W2': 'primary school', - 'W3': 'secondary school', - 'W4': 'secondary school', - 'W5': 'secondary school', - 'W6': 'secondary school', - 'W7': 'secondary school', - 'W8': 'primary school', - 'W9': 'secondary school', - 'Y1': 'large office', - 'Y2': 'large office', - 'Y3': 'large office', - 'Y4': 'large office', - 'Y5': 'large office', - 'Y6': 'large office', - 'Y7': 'large office', - 'Y8': 'large office', - 'Y9': 'large office', - 'Z0': 'na', - 'Z1': 'large office', - 'Z2': 'na', - 'Z3': 'na', - 'Z4': 'na', - 'Z5': 'na', - 'Z6': 'na', - 'Z7': 'na', - 'Z8': 'na', - 'Z9': 'na' + _pluto_to_function = { + 'A0': cte.SFH, + 'A1': cte.SFH, + 'A2': cte.SFH, + 'A3': cte.SFH, + 'A4': cte.SFH, + 'A5': cte.SFH, + 'A6': cte.SFH, + 'A7': cte.SFH, + 'A8': cte.SFH, + 'A9': cte.SFH, + 'B1': cte.MFH, + 'B2': cte.MFH, + 'B3': cte.MFH, + 'B9': cte.MFH, + 'C0': cte.RESIDENTIAL, + 'C1': cte.RESIDENTIAL, + 'C2': cte.RESIDENTIAL, + 'C3': cte.RESIDENTIAL, + 'C4': cte.RESIDENTIAL, + 'C5': cte.RESIDENTIAL, + 'C6': cte.RESIDENTIAL, + 'C7': cte.RESIDENTIAL, + 'C8': cte.RESIDENTIAL, + 'C9': cte.RESIDENTIAL, + 'D0': cte.RESIDENTIAL, + 'D1': cte.RESIDENTIAL, + 'D2': cte.RESIDENTIAL, + 'D3': cte.RESIDENTIAL, + 'D4': cte.RESIDENTIAL, + 'D5': cte.RESIDENTIAL, + 'D6': cte.RESIDENTIAL, + 'D7': cte.RESIDENTIAL, + 'D8': cte.RESIDENTIAL, + 'D9': cte.RESIDENTIAL, + 'E1': cte.WAREHOUSE, + 'E3': cte.WAREHOUSE, + 'E4': cte.WAREHOUSE, + 'E5': cte.WAREHOUSE, + 'E7': cte.WAREHOUSE, + 'E9': cte.WAREHOUSE, + 'F1': cte.WAREHOUSE, + 'F2': cte.WAREHOUSE, + 'F4': cte.WAREHOUSE, + 'F5': cte.WAREHOUSE, + 'F8': cte.WAREHOUSE, + 'F9': cte.WAREHOUSE, + 'G0': cte.OFFICE, + 'G1': cte.OFFICE, + 'G2': cte.OFFICE, + 'G3': cte.OFFICE, + 'G4': cte.OFFICE, + 'G5': cte.OFFICE, + 'G6': cte.OFFICE, + 'G7': cte.OFFICE, + 'G8': cte.OFFICE, + 'G9': cte.OFFICE, + 'H1': cte.HOTEL, + 'H2': cte.HOTEL, + 'H3': cte.HOTEL, + 'H4': cte.HOTEL, + 'H5': cte.HOTEL, + 'H6': cte.HOTEL, + 'H7': cte.HOTEL, + 'H8': cte.HOTEL, + 'H9': cte.HOTEL, + 'HB': cte.HOTEL, + 'HH': cte.HOTEL, + 'HR': cte.HOTEL, + 'HS': cte.HOTEL, + 'I1': cte.HOSPITAL, + 'I2': cte.OUTPATIENT, + 'I3': cte.OUTPATIENT, + 'I4': cte.RESIDENTIAL, + 'I5': cte.OUTPATIENT, + 'I6': cte.OUTPATIENT, + 'I7': cte.OUTPATIENT, + 'I9': cte.OUTPATIENT, + 'J1': cte.LARGE_OFFICE, + 'J2': cte.LARGE_OFFICE, + 'J3': cte.LARGE_OFFICE, + 'J4': cte.LARGE_OFFICE, + 'J5': cte.LARGE_OFFICE, + 'J6': cte.LARGE_OFFICE, + 'J7': cte.LARGE_OFFICE, + 'J8': cte.LARGE_OFFICE, + 'J9': cte.LARGE_OFFICE, + 'K1': cte.STRIP_MALL, + 'K2': cte.STRIP_MALL, + 'K3': cte.STRIP_MALL, + 'K4': cte.RESIDENTIAL, + 'K5': cte.RESTAURANT, + 'K6': cte.COMMERCIAL, + 'K7': cte.COMMERCIAL, + 'K8': cte.COMMERCIAL, + 'K9': cte.COMMERCIAL, + 'L1': cte.RESIDENTIAL, + 'L2': cte.RESIDENTIAL, + 'L3': cte.RESIDENTIAL, + 'L8': cte.RESIDENTIAL, + 'L9': cte.RESIDENTIAL, + 'M1': cte.LARGE_OFFICE, + 'M2': cte.LARGE_OFFICE, + 'M3': cte.LARGE_OFFICE, + 'M4': cte.LARGE_OFFICE, + 'M9': cte.LARGE_OFFICE, + 'N1': cte.RESIDENTIAL, + 'N2': cte.RESIDENTIAL, + 'N3': cte.RESIDENTIAL, + 'N4': cte.RESIDENTIAL, + 'N9': cte.RESIDENTIAL, + 'O1': cte.OFFICE, + 'O2': cte.OFFICE, + 'O3': cte.OFFICE, + 'O4': cte.OFFICE, + 'O5': cte.OFFICE, + 'O6': cte.OFFICE, + 'O7': cte.OFFICE, + 'O8': cte.OFFICE, + 'O9': cte.OFFICE, + 'P1': cte.LARGE_OFFICE, + 'P2': cte.HOTEL, + 'P3': cte.OFFICE, + 'P4': cte.OFFICE, + 'P5': cte.OFFICE, + 'P6': cte.OFFICE, + 'P7': cte.LARGE_OFFICE, + 'P8': cte.LARGE_OFFICE, + 'P9': cte.OFFICE, + 'Q0': cte.OFFICE, + 'Q1': cte.OFFICE, + 'Q2': cte.OFFICE, + 'Q3': cte.OFFICE, + 'Q4': cte.OFFICE, + 'Q5': cte.OFFICE, + 'Q6': cte.OFFICE, + 'Q7': cte.OFFICE, + 'Q8': cte.OFFICE, + 'Q9': cte.OFFICE, + 'R0': cte.RESIDENTIAL, + 'R1': cte.RESIDENTIAL, + 'R2': cte.RESIDENTIAL, + 'R3': cte.RESIDENTIAL, + 'R4': cte.RESIDENTIAL, + 'R5': cte.RESIDENTIAL, + 'R6': cte.RESIDENTIAL, + 'R7': cte.RESIDENTIAL, + 'R8': cte.RESIDENTIAL, + 'R9': cte.RESIDENTIAL, + 'RA': cte.RESIDENTIAL, + 'RB': cte.RESIDENTIAL, + 'RC': cte.RESIDENTIAL, + 'RD': cte.RESIDENTIAL, + 'RG': cte.RESIDENTIAL, + 'RH': cte.RESIDENTIAL, + 'RI': cte.RESIDENTIAL, + 'RK': cte.RESIDENTIAL, + 'RM': cte.RESIDENTIAL, + 'RR': cte.RESIDENTIAL, + 'RS': cte.RESIDENTIAL, + 'RW': cte.RESIDENTIAL, + 'RX': cte.RESIDENTIAL, + 'RZ': cte.RESIDENTIAL, + 'S0': cte.RESIDENTIAL, + 'S1': cte.RESIDENTIAL, + 'S2': cte.RESIDENTIAL, + 'S3': cte.RESIDENTIAL, + 'S4': cte.RESIDENTIAL, + 'S5': cte.RESIDENTIAL, + 'S9': cte.RESIDENTIAL, + 'U0': cte.WAREHOUSE, + 'U1': cte.WAREHOUSE, + 'U2': cte.WAREHOUSE, + 'U3': cte.WAREHOUSE, + 'U4': cte.WAREHOUSE, + 'U5': cte.WAREHOUSE, + 'U6': cte.WAREHOUSE, + 'U7': cte.WAREHOUSE, + 'U8': cte.WAREHOUSE, + 'U9': cte.WAREHOUSE, + 'W1': cte.PRIMARY_SCHOOL, + 'W2': cte.PRIMARY_SCHOOL, + 'W3': cte.SECONDARY_SCHOOL, + 'W4': cte.SECONDARY_SCHOOL, + 'W5': cte.SECONDARY_SCHOOL, + 'W6': cte.SECONDARY_SCHOOL, + 'W7': cte.SECONDARY_SCHOOL, + 'W8': cte.PRIMARY_SCHOOL, + 'W9': cte.SECONDARY_SCHOOL, + 'Y1': cte.LARGE_OFFICE, + 'Y2': cte.LARGE_OFFICE, + 'Y3': cte.LARGE_OFFICE, + 'Y4': cte.LARGE_OFFICE, + 'Y5': cte.LARGE_OFFICE, + 'Y6': cte.LARGE_OFFICE, + 'Y7': cte.LARGE_OFFICE, + 'Y8': cte.LARGE_OFFICE, + 'Y9': cte.LARGE_OFFICE, + 'Z1': cte.LARGE_OFFICE } - hft_to_function = { + _hft_to_function = { 'residential': cte.RESIDENTIAL, 'single family house': cte.SFH, 'multifamily house': cte.MFH, @@ -251,25 +229,18 @@ class GeometryHelper: } # usage - function_to_usage = { - 'full service restaurant': 'restaurant', - 'highrise apartment': cte.RESIDENTIAL, - 'hospital': 'health care', - 'large hotel': 'hotel', - 'large office': 'office and administration', - 'medium office': 'office and administration', - 'midrise apartment': cte.RESIDENTIAL, - 'outpatient healthcare': 'health care', - 'primary school': 'education', - 'quick service restaurant': 'restaurant', - 'secondary school': 'education', - 'small hotel': 'hotel', - 'small office': 'office and administration', - 'stand alone retail': 'retail', - 'strip mall': 'hall', - 'supermarket': 'retail', - 'warehouse': 'industry', - 'residential': cte.RESIDENTIAL + _function_to_usage = { + cte.RESTAURANT: cte.RESTAURANT, + cte.RESIDENTIAL: cte.RESIDENTIAL, + cte.HOSPITAL: cte.HEALTH_CARE, + cte.HOTEL: cte.HOTEL, + cte.LARGE_OFFICE: cte.OFFICE_ADMINISTRATION, + cte.OFFICE: cte.OFFICE_ADMINISTRATION, + cte.PRIMARY_SCHOOL: cte.EDUCATION, + cte.SECONDARY_SCHOOL: cte.EDUCATION, + cte.RETAIL: cte.RETAIL, + cte.STRIP_MALL: cte.HALL, + cte.WAREHOUSE: cte.INDUSTRY } @staticmethod @@ -279,7 +250,7 @@ class GeometryHelper: :param building_hft_function: str :return: str """ - return GeometryHelper.hft_to_function[building_hft_function] + return GeometryHelper._hft_to_function[building_hft_function] @staticmethod def function_from_pluto(building_pluto_function): @@ -288,7 +259,7 @@ class GeometryHelper: :param building_pluto_function: str :return: str """ - return GeometryHelper.pluto_to_function[building_pluto_function] + return GeometryHelper._pluto_to_function[building_pluto_function] @staticmethod def usage_from_function(building_function): @@ -297,7 +268,7 @@ class GeometryHelper: :param building_function: str :return: str """ - return GeometryHelper.function_to_usage[building_function] + return GeometryHelper._function_to_usage[building_function] @staticmethod def to_points_matrix(points): diff --git a/imports/schedules/helpers/schedules_helper.py b/imports/schedules/helpers/schedules_helper.py index fa47b343..9aca6fab 100644 --- a/imports/schedules/helpers/schedules_helper.py +++ b/imports/schedules/helpers/schedules_helper.py @@ -12,7 +12,7 @@ class SchedulesHelper: """ Schedules helper """ - usage_to_comnet = { + _usage_to_comnet = { cte.RESIDENTIAL: 'C-12 Residential', cte.INDUSTRY: 'C-10 Warehouse', cte.OFFICE_ADMINISTRATION: 'C-5 Office', @@ -23,16 +23,15 @@ class SchedulesHelper: cte.RESTAURANT: 'C-7 Restaurant', cte.EDUCATION: 'C-9 School' } - comnet_default_value = 'C-12 Residential' - comnet_to_data_type = { + _comnet_to_data_type = { 'Fraction': cte.FRACTION, 'OnOff': cte.ON_OFF, 'Temperature': cte.TEMPERATURE } # usage - function_to_usage = { + _function_to_usage = { 'full service restaurant': cte.RESTAURANT, 'high-rise apartment': cte.RESIDENTIAL, 'hospital': cte.HEALTH_CARE, @@ -61,10 +60,9 @@ class SchedulesHelper: :return: str """ try: - return SchedulesHelper.usage_to_comnet[usage] + return SchedulesHelper._usage_to_comnet[usage] except KeyError: - sys.stderr.write('Error: keyword not found. Returned default Comnet schedules "residential"\n') - return SchedulesHelper.comnet_default_value + sys.stderr.write('Error: keyword not found.\n') @staticmethod def data_type_from_comnet(comnet_data_type): @@ -74,7 +72,7 @@ class SchedulesHelper: :return: str """ try: - return SchedulesHelper.comnet_to_data_type[comnet_data_type] + return SchedulesHelper._comnet_to_data_type[comnet_data_type] except KeyError: raise ValueError(f"Error: comnet data type keyword not found.") @@ -85,4 +83,4 @@ class SchedulesHelper: :param building_function: str :return: str """ - return SchedulesHelper.function_to_usage[building_function] + return SchedulesHelper._function_to_usage[building_function] diff --git a/imports/usage/ca_usage_parameters.py b/imports/usage/ca_usage_parameters.py index cea058f6..c9034b85 100644 --- a/imports/usage/ca_usage_parameters.py +++ b/imports/usage/ca_usage_parameters.py @@ -5,10 +5,12 @@ Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.mons """ import sys -from imports.geometry.helpers.geometry_helper import GeometryHelper as gh from imports.usage.hft_usage_interface import HftUsageInterface from city_model_structure.building_demand.usage_zone import UsageZone from city_model_structure.building_demand.internal_gains import InternalGains +from city_model_structure.building_demand.occupancy import Occupancy +from city_model_structure.building_demand.appliances import Appliances +from city_model_structure.building_demand.thermal_control import ThermalControl class CaUsageParameters(HftUsageInterface): @@ -18,9 +20,6 @@ class CaUsageParameters(HftUsageInterface): def __init__(self, city, base_path): super().__init__(base_path, 'ca_archetypes_reduced.xml') self._city = city - # todo: this is a wrong location for self._min_air_change -> re-think where to place this info - # and where it comes from - self._min_air_change = 0 def enrich_buildings(self): """ @@ -29,19 +28,20 @@ class CaUsageParameters(HftUsageInterface): """ city = self._city for building in city.buildings: - archetype = self._search_archetype(building.function) - if archetype is None: + try: + print(building.function) + archetype = self._search_archetype(building.function) + except KeyError: sys.stderr.write(f'Building {building.name} has unknown archetype for building function:' - f' {building.function}, that assigns building usage as ' - f'{gh.usage_from_function(building.function)}\n') - continue + f' {building.function}\n') + return for internal_zone in building.internal_zones: usage_zone = UsageZone() - usage_zone.usage = building.function - self._assign_values(usage_zone, archetype) - usage_zone.percentage = 1 - internal_zone.usage_zones = [usage_zone] + usage_zone.usage = building.function + usage_zone.percentage = 1 + self._assign_values_usage_zone(usage_zone, archetype) + internal_zone.usage_zones = [usage_zone] def _search_archetype(self, building_usage): for building_archetype in self._usage_archetypes: @@ -50,26 +50,30 @@ class CaUsageParameters(HftUsageInterface): return None @staticmethod - def _assign_values(usage_zone, archetype): + def _assign_values_usage_zone(usage_zone, archetype): # Due to the fact that python is not a typed language, the wrong object type is assigned to # usage_zone.internal_gains when writing usage_zone.internal_gains = archetype.internal_gains. # Therefore, this walk around has been done. - internal_gains = [] - for archetype_internal_gain in archetype.internal_gains: - internal_gain = InternalGains() - internal_gain.average_internal_gain = archetype_internal_gain.average_internal_gain - internal_gain.convective_fraction = archetype_internal_gain.convective_fraction - internal_gain.radiative_fraction = archetype_internal_gain.radiative_fraction - internal_gain.latent_fraction = archetype_internal_gain.latent_fraction - internal_gains.append(internal_gain) - usage_zone.internal_gains = internal_gains - usage_zone.heating_setpoint = archetype.heating_setpoint - usage_zone.heating_setback = archetype.heating_setback - usage_zone.cooling_setpoint = archetype.cooling_setpoint - usage_zone.occupancy_density = archetype.occupancy_density + usage_zone.mechanical_air_change = archetype.mechanical_air_change + _occupancy = Occupancy() + _occupancy.occupancy_density = archetype.occupancy.occupancy_density + usage_zone.occupancy = _occupancy usage_zone.hours_day = archetype.hours_day usage_zone.days_year = archetype.days_year - usage_zone.dhw_average_volume_pers_day = archetype.dhw_average_volume_pers_day - usage_zone.dhw_preparation_temperature = archetype.dhw_preparation_temperature - usage_zone.electrical_app_average_consumption_sqm_year = archetype.electrical_app_average_consumption_sqm_year - usage_zone.mechanical_air_change = archetype.mechanical_air_change + _appliances = Appliances() + _appliances.appliances_density = archetype.appliances.appliances_density + usage_zone.appliances = _appliances + _control = ThermalControl() + _control.mean_heating_set_point = archetype.thermal_control.mean_heating_set_point + _control.heating_set_back = archetype.thermal_control.heating_set_back + _control.mean_cooling_set_point = archetype.thermal_control.mean_cooling_set_point + usage_zone.thermal_control = _control + _internal_gains = [] + for archetype_internal_gain in archetype.not_detailed_source_mean_annual_internal_gains: + _internal_gain = InternalGains() + _internal_gain.average_internal_gain = archetype_internal_gain.average_internal_gain + _internal_gain.convective_fraction = archetype_internal_gain.convective_fraction + _internal_gain.radiative_fraction = archetype_internal_gain.radiative_fraction + _internal_gain.latent_fraction = archetype_internal_gain.latent_fraction + _internal_gains.append(_internal_gain) + usage_zone.not_detailed_source_mean_annual_internal_gains = _internal_gains diff --git a/imports/usage/comnet_usage_parameters.py b/imports/usage/comnet_usage_parameters.py index ed5d187e..851473da 100644 --- a/imports/usage/comnet_usage_parameters.py +++ b/imports/usage/comnet_usage_parameters.py @@ -3,6 +3,7 @@ ComnetUsageParameters model the usage properties SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2021 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ + import sys from typing import Dict import pandas as pd @@ -11,11 +12,13 @@ import helpers.constants as cte from helpers.configuration_helper import ConfigurationHelper as ch from imports.geometry.helpers.geometry_helper import GeometryHelper from imports.usage.helpers.usage_helper import UsageHelper +from imports.schedules.helpers.schedules_helper import SchedulesHelper from city_model_structure.building_demand.usage_zone import UsageZone from city_model_structure.building_demand.lighting import Lighting from city_model_structure.building_demand.occupancy import Occupancy from city_model_structure.building_demand.appliances import Appliances -from city_model_structure.building_demand.internal_gains import InternalGains +from city_model_structure.building_demand.thermal_control import ThermalControl +from city_model_structure.attributes.schedule import Schedule class ComnetUsageParameters: @@ -25,24 +28,19 @@ class ComnetUsageParameters: def __init__(self, city, base_path): self._city = city self._base_path = str(base_path / 'comnet_archetypes.xlsx') - self._usage_archetypes = [] - data = self._read_file() - for item in data['lighting']: - for usage in UsageHelper.usage_to_comnet: - comnet_usage = UsageHelper.usage_to_comnet[usage] - if comnet_usage == item: - usage_archetype = self._parse_zone_usage_type(comnet_usage, data) - self._usage_archetypes.append(usage_archetype) + self._data = self._read_file() + self._comnet_schedules_path = str(base_path / 'comnet_schedules_archetypes.xlsx') + self._xls = pd.ExcelFile(self._comnet_schedules_path) def _read_file(self) -> Dict: """ - reads xlsx file containing usage information into a dictionary + reads xlsx files containing usage information into a dictionary :return : Dict """ number_usage_types = 33 xl_file = pd.ExcelFile(self._base_path) file_data = pd.read_excel(xl_file, sheet_name="Modeling Data", skiprows=[0, 1, 2], - nrows=number_usage_types, usecols="A:Z") + nrows=number_usage_types, usecols="A:AB") lighting_data = {} plug_loads_data = {} @@ -50,6 +48,7 @@ class ComnetUsageParameters: ventilation_rate = {} water_heating = {} process_data = {} + schedules_key = {} for j in range(0, number_usage_types): usage_parameters = file_data.iloc[j] @@ -60,53 +59,130 @@ class ComnetUsageParameters: ventilation_rate[usage_type] = usage_parameters[20:21].values.tolist() water_heating[usage_type] = usage_parameters[23:24].values.tolist() process_data[usage_type] = usage_parameters[24:26].values.tolist() + schedules_key[usage_type] = usage_parameters[27:28].values.tolist() return {'lighting': lighting_data, 'plug loads': plug_loads_data, 'occupancy': occupancy_data, 'ventilation rate': ventilation_rate, 'water heating': water_heating, - 'process': process_data} + 'process': process_data, + 'schedules_key': schedules_key} @staticmethod - def _parse_zone_usage_type(usage, data): + def _parse_usage_type(comnet_usage, data, schedules_data): _usage_zone = UsageZone() - _usage_zone.usage = usage # lighting _lighting = Lighting() _lighting.latent_fraction = ch().comnet_lighting_latent _lighting.convective_fraction = ch().comnet_lighting_convective _lighting.radiative_fraction = ch().comnet_lighting_radiant - _lighting.average_internal_gain = data['lighting'][usage][4] + _lighting.lighting_density = data['lighting'][comnet_usage][4] # plug loads _appliances = None - if data['plug loads'][usage][0] != 'n.a.': + if data['plug loads'][comnet_usage][0] != 'n.a.': _appliances = Appliances() _appliances.latent_fraction = ch().comnet_plugs_latent _appliances.convective_fraction = ch().comnet_plugs_convective _appliances.radiative_fraction = ch().comnet_plugs_radiant - _appliances.average_internal_gain = data['plug loads'][usage][0] + _appliances.appliances_density = data['plug loads'][comnet_usage][0] # occupancy _occupancy = Occupancy() - _occupancy.occupancy_density = data['occupancy'][usage][0] - _occupancy.sensible_convective_internal_gain = data['occupancy'][usage][1] \ + _occupancy.occupancy_density = data['occupancy'][comnet_usage][0] + _occupancy.sensible_convective_internal_gain = data['occupancy'][comnet_usage][1] \ * ch().comnet_occupancy_sensible_convective - _occupancy.sensible_radiant_internal_gain = data['occupancy'][usage][1] * ch().comnet_occupancy_sensible_radiant - _occupancy.latent_internal_gain = data['occupancy'][usage][2] + _occupancy.sensible_radiative_internal_gain = data['occupancy'][comnet_usage][1] \ + * ch().comnet_occupancy_sensible_radiant + _occupancy.latent_internal_gain = data['occupancy'][comnet_usage][2] if _occupancy.occupancy_density <= 0: _usage_zone.mechanical_air_change = 0 else: - _usage_zone.mechanical_air_change = data['ventilation rate'][usage][0] / _occupancy.occupancy_density + _usage_zone.mechanical_air_change = data['ventilation rate'][comnet_usage][0] / _occupancy.occupancy_density + + schedules_usage = UsageHelper.schedules_key(data['schedules_key'][comnet_usage][0]) + + _extracted_data = pd.read_excel(schedules_data, sheet_name=schedules_usage, + skiprows=[0, 1, 2, 3], nrows=39, usecols="A:AA") + schedules = [] + number_of_schedule_types = 13 + schedules_per_schedule_type = 3 + day_types = dict({'week_day': 0, 'saturday': 1, 'sunday': 2}) + for schedule_types in range(0, number_of_schedule_types): + name = '' + data_type = '' + for schedule_day in range(0, schedules_per_schedule_type): + _schedule = Schedule() + _schedule.time_step = cte.HOUR + _schedule.time_range = cte.DAY + row_cells = _extracted_data.iloc[schedules_per_schedule_type * schedule_types + schedule_day] + if schedule_day == day_types['week_day']: + name = row_cells[0] + data_type = row_cells[1] + _schedule.day_types = [cte.MONDAY, cte.TUESDAY, cte.WEDNESDAY, cte.THURSDAY, cte.FRIDAY] + elif schedule_day == day_types['saturday']: + _schedule.day_types = [cte.SATURDAY] + else: + _schedule.day_types = [cte.SUNDAY] + _schedule.type = name + _schedule.data_type = SchedulesHelper.data_type_from_comnet(data_type) + if _schedule.data_type == cte.TEMPERATURE: + values = [] + for cell in row_cells[schedules_per_schedule_type:].to_numpy(): + values.append((float(cell) - 32.) * 5 / 9) + _schedule.values = values + else: + _schedule.values = row_cells[schedules_per_schedule_type:].to_numpy() + schedules.append(_schedule) + + schedules_types = dict({'Occupancy': 0, 'Lights': 3, 'Receptacle': 6, 'Infiltration': 9, 'HVAC Avail': 12, + 'ClgSetPt': 15, 'HtgSetPt': 18}) + + _schedules = [] + for pointer in range(0, 3): + _schedules.append(schedules[schedules_types['Occupancy']+pointer]) + _occupancy.occupancy_schedules = _schedules + _schedules = [] + for pointer in range(0, 3): + _schedules.append(schedules[schedules_types['Lights']+pointer]) + _lighting.schedules = _schedules + _schedules = [] + for pointer in range(0, 3): + _schedules.append(schedules[schedules_types['Receptacle']+pointer]) + _appliances.schedules = _schedules _usage_zone.occupancy = _occupancy _usage_zone.lighting = _lighting _usage_zone.appliances = _appliances + + _control = ThermalControl() + _schedules = [] + for pointer in range(0, 3): + _schedules.append(schedules[schedules_types['HtgSetPt']+pointer]) + _control.heating_set_point_schedules = _schedules + _schedules = [] + for pointer in range(0, 3): + _schedules.append(schedules[schedules_types['ClgSetPt']+pointer]) + _control.cooling_set_point_schedules = _schedules + _schedules = [] + for pointer in range(0, 3): + _schedules.append(schedules[schedules_types['HVAC Avail']+pointer]) + _control.hvac_availability_schedules = _schedules + _usage_zone.thermal_control = _control + return _usage_zone + def _search_archetypes(self, usage): + for item in self._data['lighting']: + comnet_usage = UsageHelper.comnet_from_usage(usage) + if comnet_usage == item: + usage_archetype = self._parse_usage_type(comnet_usage, self._data, self._xls) + return usage_archetype + return None, None + def enrich_buildings(self): """ Returns the city with the usage parameters assigned to the buildings @@ -115,41 +191,80 @@ class ComnetUsageParameters: city = self._city for building in city.buildings: usage = GeometryHelper.usage_from_function(building.function) - archetype = self._search_archetype(UsageHelper.comnet_from_usage(usage)) - if archetype is None: + try: + archetype_usage = self._search_archetypes(usage) + except KeyError: sys.stderr.write(f'Building {building.name} has unknown archetype for building function:' f' {building.function}, that assigns building usage as ' f'{GeometryHelper.usage_from_function(building.function)}\n') - continue + return for internal_zone in building.internal_zones: + if internal_zone.area is None: + raise Exception('Internal zone area not defined, ACH cannot be calculated') + if internal_zone.volume is None: + raise Exception('Internal zone volume not defined, ACH cannot be calculated') + if internal_zone.area <= 0: + raise Exception('Internal zone area is zero, ACH cannot be calculated') + if internal_zone.volume <= 0: + raise Exception('Internal zone volume is zero, ACH cannot be calculated') + volume_per_area = internal_zone.volume / internal_zone.area usage_zone = UsageZone() usage_zone.usage = usage - self._assign_values(usage_zone, archetype, volume_per_area) + self._assign_values_usage_zone(usage_zone, archetype_usage, volume_per_area) usage_zone.percentage = 1 + self._calculate_reduced_values_from_extended_library(usage_zone, archetype_usage) + internal_zone.usage_zones = [usage_zone] - def _search_archetype(self, building_usage): - for building_archetype in self._usage_archetypes: - if building_archetype.usage == building_usage: - return building_archetype - return None + @staticmethod + def _assign_values_usage_zone(usage_zone, archetype, volume_per_area): + # Due to the fact that python is not a typed language, the wrong object type is assigned to + # usage_zone.occupancy when writing usage_zone.occupancy = archetype.occupancy. + # Same happens for lighting and appliances. Therefore, this walk around has been done. + usage_zone.mechanical_air_change = archetype.mechanical_air_change * cte.METERS_TO_FEET ** 2 \ + * cte.HOUR_TO_MINUTES / cte.METERS_TO_FEET ** 3 / volume_per_area + _occupancy = Occupancy() + _occupancy.occupancy_density = archetype.occupancy.occupancy_density * cte.METERS_TO_FEET**2 + _occupancy.sensible_radiative_internal_gain = archetype.occupancy.sensible_radiative_internal_gain + _occupancy.latent_internal_gain = archetype.occupancy.latent_internal_gain + _occupancy.sensible_convective_internal_gain = archetype.occupancy.sensible_convective_internal_gain + _occupancy.occupancy_schedules = archetype.occupancy.occupancy_schedules + usage_zone.occupancy = _occupancy + _lighting = Lighting() + _lighting.lighting_density = archetype.lighting.lighting_density / cte.METERS_TO_FEET**2 + _lighting.convective_fraction = archetype.lighting.convective_fraction + _lighting.radiative_fraction = archetype.lighting.radiative_fraction + _lighting.latent_fraction = archetype.lighting.latent_fraction + _lighting.schedules = archetype.lighting.schedules + usage_zone.lighting = _lighting + _appliances = Appliances() + _appliances.appliances_density = archetype.appliances.appliances_density / cte.METERS_TO_FEET**2 + _appliances.convective_fraction = archetype.appliances.convective_fraction + _appliances.radiative_fraction = archetype.appliances.radiative_fraction + _appliances.latent_fraction = archetype.appliances.latent_fraction + _appliances.schedules = archetype.appliances.schedules + usage_zone.appliances = _appliances + _control = ThermalControl() + _control.cooling_set_point_schedules = archetype.thermal_control.cooling_set_point_schedules + _control.heating_set_point_schedules = archetype.thermal_control.heating_set_point_schedules + _control.hvac_availability_schedules = archetype.thermal_control.hvac_availability_schedules + usage_zone.thermal_control = _control @staticmethod - def _assign_values(usage_zone, archetype, volume_per_area): - # Due to the fact that python is not a typed language, the wrong object type is assigned to - # usage_zone.internal_gains when writing usage_zone.internal_gains = archetype.internal_gains. - # Therefore, this walk around has been done. - internal_gains = [] - for archetype_internal_gain in archetype.internal_gains: - internal_gain = InternalGains() - internal_gain.type = archetype_internal_gain.type - internal_gain.average_internal_gain = archetype_internal_gain.average_internal_gain - internal_gain.convective_fraction = archetype_internal_gain.convective_fraction - internal_gain.radiative_fraction = archetype_internal_gain.radiative_fraction - internal_gain.latent_fraction = archetype_internal_gain.latent_fraction - internal_gains.append(internal_gain) - usage_zone.internal_gains = internal_gains - usage_zone.occupancy_density = archetype.occupancy_density * cte.METERS_TO_FEET**2 - usage_zone.mechanical_air_change = archetype.mechanical_air_change * cte.METERS_TO_FEET**2 \ - * cte.HOUR_TO_MINUTES / cte.METERS_TO_FEET**3 / volume_per_area \ No newline at end of file + def _calculate_reduced_values_from_extended_library(usage_zone, archetype): + number_of_days_per_type = {'WD': 251, 'Sat': 52, 'Sun': 62} + total = 0 + for schedule in archetype.thermal_control.hvac_availability_schedules: + if schedule.day_types[0] == cte.SATURDAY: + for value in schedule.values: + total += value * number_of_days_per_type['Sat'] + elif schedule.day_types[0] == cte.SUNDAY: + for value in schedule.values: + total += value * number_of_days_per_type['Sun'] + else: + for value in schedule.values: + total += value * number_of_days_per_type['WD'] + + usage_zone.hours_day = total / 365 + usage_zone.days_year = 365 diff --git a/imports/usage/helpers/usage_helper.py b/imports/usage/helpers/usage_helper.py index 310b4888..f6257503 100644 --- a/imports/usage/helpers/usage_helper.py +++ b/imports/usage/helpers/usage_helper.py @@ -11,7 +11,7 @@ class UsageHelper: """ Usage helper class """ - usage_to_hft = { + _usage_to_hft = { cte.RESIDENTIAL: 'residential', cte.INDUSTRY: 'industry', cte.OFFICE_ADMINISTRATION: 'office and administration', @@ -20,9 +20,7 @@ class UsageHelper: cte.RETAIL: 'retail', cte.HALL: 'hall', cte.RESTAURANT: 'restaurant', - cte.EDUCATION: 'education' - } - hft_default_value = 'residential' + cte.EDUCATION: 'education'} @staticmethod def hft_from_usage(usage): @@ -32,12 +30,11 @@ class UsageHelper: :return: str """ try: - return UsageHelper.usage_to_hft[usage] + return UsageHelper._usage_to_hft[usage] except KeyError: - sys.stderr.write('Error: keyword not found. Returned default HfT usage "residential"\n') - return UsageHelper.hft_default_value + sys.stderr.write('Error: keyword not found.\n') - usage_to_comnet = { + _usage_to_comnet = { cte.RESIDENTIAL: 'BA Multifamily', cte.INDUSTRY: 'BA Manufacturing Facility', cte.OFFICE_ADMINISTRATION: 'BA Office', @@ -46,9 +43,23 @@ class UsageHelper: cte.RETAIL: 'BA Retail', cte.HALL: 'BA Town Hall', cte.RESTAURANT: 'BA Dining: Bar Lounge/Leisure', - cte.EDUCATION: 'BA School/University' - } - comnet_default_value = 'BA Multifamily' + cte.EDUCATION: 'BA School/University'} + + _comnet_schedules_key_to_comnet_schedules = { + 'C-1 Assembly': 'C-1 Assembly', + 'C-2 Public': 'C-2 Health', + 'C-3 Hotel Motel': 'C-3 Hotel', + 'C-4 Manufacturing': 'C-4 Manufacturing', + 'C-5 Office': 'C-5 Office', + 'C-6 Parking Garage': 'C-6 Parking', + 'C-7 Restaurant': 'C-7 Restaurant', + 'C-8 Retail': 'C-8 Retail', + 'C-9 Schools': 'C-9 School', + 'C-10 Warehouse': 'C-10 Warehouse', + 'C-11 Laboratory': 'C-11 Lab', + 'C-12 Residential': 'C-12 Residential', + 'C-13 Data Center': 'C-13 Data', + 'C-14 Gymnasium': 'C-14 Gymnasium'} @staticmethod def comnet_from_usage(usage): @@ -58,7 +69,19 @@ class UsageHelper: :return: str """ try: - return UsageHelper.usage_to_comnet[usage] + return UsageHelper._usage_to_comnet[usage] except KeyError: - sys.stderr.write('Error: keyword not found. Returned default Comnet usage "BA Multifamily"\n') - return UsageHelper.comnet_default_value + sys.stderr.write('Error: keyword not found.\n') + + @staticmethod + def schedules_key(usage): + """ + Get Comnet schedules key from the list found in the Comnet usage file + :param usage: str + :return: str + """ + try: + return UsageHelper._comnet_schedules_key_to_comnet_schedules[usage] + except KeyError: + sys.stderr.write('Error: Comnet keyword not found. An update of the Comnet files might have been ' + 'done changing the keywords.\n') diff --git a/imports/usage/hft_usage_interface.py b/imports/usage/hft_usage_interface.py index ed6dfefc..340d8a80 100644 --- a/imports/usage/hft_usage_interface.py +++ b/imports/usage/hft_usage_interface.py @@ -1,12 +1,17 @@ """ -Hft-based interface, it reads format defined within the CERC team based on that one used in SimStadt and developed by -the IAF team at hft-Stuttgart and enriches the city with usage parameters +Hft-based interface, it reads format defined within the CERC team (based on that one used in SimStadt and developed by +the IAF team at hft-Stuttgart) SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +Copyright © 2022 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ import xmltodict -from imports.usage.data_classes.usage_zone_archetype import UsageZoneArchetype as huza -from imports.usage.data_classes.hft_internal_gains_archetype import HftInternalGainsArchetype as higa +from city_model_structure.building_demand.usage_zone import UsageZone +from city_model_structure.building_demand.internal_gains import InternalGains +from city_model_structure.building_demand.occupancy import Occupancy +from city_model_structure.building_demand.appliances import Appliances +from city_model_structure.building_demand.thermal_control import ThermalControl +from city_model_structure.attributes.schedule import Schedule +import helpers.constants as cte class HftUsageInterface: @@ -28,116 +33,196 @@ class HftUsageInterface: usage = usage_zone_variant['id'] usage_archetype_variant = self._parse_zone_usage_variant(usage, usage_archetype, usage_zone_variant) self._usage_archetypes.append(usage_archetype_variant) + for usage in self._usage_archetypes: + print(usage.usage) @staticmethod def _parse_zone_usage_type(usage, zone_usage_type): - occupancy_density = zone_usage_type['occupancy']['occupancyDensity'] - hours_day = zone_usage_type['occupancy']['usageHoursPerDay'] - days_year = zone_usage_type['occupancy']['usageDaysPerYear'] - cooling_setpoint = zone_usage_type['endUses']['space_cooling']['coolingSetPointTemperature'] - heating_setpoint = zone_usage_type['endUses']['space_heating']['heatingSetPointTemperature'] - heating_setback = zone_usage_type['endUses']['space_heating']['heatingSetBackTemperature'] - mechanical_air_change = None - if 'ventilation' in zone_usage_type['endUses'] and zone_usage_type['endUses']['ventilation'] is not None: - mechanical_air_change = zone_usage_type['endUses']['ventilation']['mechanicalAirChangeRate'] - dhw_average_volume_pers_day = None - dhw_preparation_temperature = None - if 'domestic_hot_water' in zone_usage_type['endUses']: - # liters to cubic meters - dhw_average_volume_pers_day = float( - zone_usage_type['endUses']['domestic_hot_water']['averageVolumePerPersAndDay']) / 1000 - dhw_preparation_temperature = zone_usage_type['endUses']['domestic_hot_water']['preparationTemperature'] - electrical_app_average_consumption_sqm_year = None - if 'all_electrical_appliances' in zone_usage_type['endUses']: - if 'averageConsumptionPerSqmAndYear' in zone_usage_type['endUses']['all_electrical_appliances']: - # kWh to J - electrical_app_average_consumption_sqm_year = \ - float(zone_usage_type['endUses']['all_electrical_appliances']['averageConsumptionPerSqmAndYear']) / 3.6 + usage_zone_archetype = UsageZone() + usage_zone_archetype.usage = usage - # todo: for internal_gain in usage_zone_variant['schedules']['internGains']:???????????????? - # There are no more internal gains? How is it saved when more than one??? - internal_gains = [] - if 'internGains' in zone_usage_type['occupancy']: - latent_fraction = zone_usage_type['occupancy']['internGains']['latentFraction'] - convective_fraction = zone_usage_type['occupancy']['internGains']['convectiveFraction'] - average_internal_gain = zone_usage_type['occupancy']['internGains']['averageInternGainPerSqm'] - radiative_fraction = zone_usage_type['occupancy']['internGains']['radiantFraction'] - else: - latent_fraction = 0 - convective_fraction = 0 - average_internal_gain = 0 - radiative_fraction = 0 + if 'occupancy' in zone_usage_type: + _occupancy = Occupancy() + _occupancy.occupancy_density = zone_usage_type['occupancy']['occupancyDensity'] #todo: check units + usage_zone_archetype.hours_day = zone_usage_type['occupancy']['usageHoursPerDay'] + usage_zone_archetype.days_year = zone_usage_type['occupancy']['usageDaysPerYear'] + usage_zone_archetype.occupancy = _occupancy + + if 'internGains' in zone_usage_type['occupancy']: + _internal_gain = InternalGains() + _internal_gain.latent_fraction = zone_usage_type['occupancy']['internGains']['latentFraction'] + _internal_gain.convective_fraction = zone_usage_type['occupancy']['internGains']['convectiveFraction'] + _internal_gain.average_internal_gain = zone_usage_type['occupancy']['internGains']['averageInternGainPerSqm'] + _internal_gain.radiative_fraction = zone_usage_type['occupancy']['internGains']['radiantFraction'] + if 'load' in zone_usage_type['occupancy']['internGains']: + _schedule = Schedule() + _schedule.type = 'internal gains load' + _schedule.time_range = cte.DAY + _schedule.time_step = cte.HOUR + _schedule.data_type = cte.ANY_NUMBER + _schedule.day_types = [cte.MONDAY, cte.TUESDAY, cte.WEDNESDAY, cte.THURSDAY, cte.FRIDAY] + _values = zone_usage_type['occupancy']['internGains']['load']['weekDayProfile']['values'] + while ' ' in _values: + _values = _values.replace(' ', ' ') + _schedule.values = _values.split() + _internal_gain.schedules = [_schedule] + + usage_zone_archetype.not_detailed_source_mean_annual_internal_gains = [_internal_gain] + + if 'endUses' in zone_usage_type: + _thermal_control = ThermalControl() + if 'space_heating' in zone_usage_type['endUses']: + _thermal_control.mean_heating_set_point = \ + zone_usage_type['endUses']['space_heating']['heatingSetPointTemperature'] + _thermal_control.heating_set_back = zone_usage_type['endUses']['space_heating']['heatingSetBackTemperature'] + if 'schedule' in zone_usage_type['endUses']['space_heating']: + _schedule = Schedule() + _schedule.type = 'heating temperature' + _schedule.time_range = cte.DAY + _schedule.time_step = cte.HOUR + _schedule.data_type = cte.TEMPERATURE + _schedule.day_types = [cte.MONDAY, cte.TUESDAY, cte.WEDNESDAY, cte.THURSDAY, cte.FRIDAY] + _values = zone_usage_type['endUses']['space_heating']['schedule']['weekDayProfile']['values'] + while ' ' in _values: + _values = _values.replace(' ', ' ') + _schedule.values = _values.split() + _thermal_control.heating_set_point_schedules = [_schedule] + + if 'space_cooling' in zone_usage_type['endUses']: + _thermal_control.mean_cooling_set_point = \ + zone_usage_type['endUses']['space_cooling']['coolingSetPointTemperature'] + if 'schedule' in zone_usage_type['endUses']['space_cooling']: + _schedule = Schedule() + _schedule.type = 'cooling temperature' + _schedule.time_range = cte.DAY + _schedule.time_step = cte.HOUR + _schedule.data_type = cte.TEMPERATURE + _schedule.day_types = [cte.MONDAY, cte.TUESDAY, cte.WEDNESDAY, cte.THURSDAY, cte.FRIDAY] + _values = zone_usage_type['endUses']['space_cooling']['schedule']['weekDayProfile']['values'] + while ' ' in _values: + _values = _values.replace(' ', ' ') + _schedule.values = _values.split() + _thermal_control.cooling_set_point_schedules = [_schedule] + + usage_zone_archetype.thermal_control = _thermal_control + + if 'ventilation' in zone_usage_type['endUses'] and zone_usage_type['endUses']['ventilation'] is not None: + usage_zone_archetype.mechanical_air_change = \ + zone_usage_type['endUses']['ventilation']['mechanicalAirChangeRate'] + + # todo: not used or assigned anywhere + if 'domestic_hot_water' in zone_usage_type['endUses']: + # liters to cubic meters + dhw_average_volume_pers_day = float( + zone_usage_type['endUses']['domestic_hot_water']['averageVolumePerPersAndDay']) / 1000 + dhw_preparation_temperature = zone_usage_type['endUses']['domestic_hot_water']['preparationTemperature'] + + if 'all_electrical_appliances' in zone_usage_type['endUses']: + if 'averageConsumptionPerSqmAndYear' in zone_usage_type['endUses']['all_electrical_appliances']: + # kWh to J + usage_zone_archetype.electrical_app_average_consumption_sqm_year = \ + float(zone_usage_type['endUses']['all_electrical_appliances']['averageConsumptionPerSqmAndYear']) \ + * cte.KILO_WATTS_HOUR_TO_JULES + + if 'appliance' in zone_usage_type: + _appliances = Appliances() + _appliances.appliances_density = zone_usage_type['appliance']['#text'] #todo: check units + + usage_zone_archetype.appliances = _appliances - internal_gains.append(higa(average_internal_gain=average_internal_gain, convective_fraction=convective_fraction, - radiative_fraction=radiative_fraction, latent_fraction=latent_fraction)) - usage_zone_archetype = huza(usage=usage, internal_gains=internal_gains, heating_set_point=heating_setpoint, - heating_set_back=heating_setback, cooling_set_point=cooling_setpoint, - occupancy_density=occupancy_density, hours_day=hours_day, days_year=days_year, - dhw_average_volume_pers_day=dhw_average_volume_pers_day, - dhw_preparation_temperature=dhw_preparation_temperature, - electrical_app_average_consumption_sqm_year=electrical_app_average_consumption_sqm_year, - mechanical_air_change=mechanical_air_change) return usage_zone_archetype @staticmethod def _parse_zone_usage_variant(usage, usage_zone, usage_zone_variant): - # for the variants all is optional because it mimics the inheritance concept from OOP - occupancy_density = usage_zone.occupancy_density - hours_day = usage_zone.hours_day - days_year = usage_zone.days_year - cooling_setpoint = usage_zone.cooling_setpoint - heating_setpoint = usage_zone.heating_setpoint - heating_setback = usage_zone.heating_setback - mechanical_air_change = usage_zone.mechanical_air_change - dhw_average_volume_pers_day = usage_zone.dhw_average_volume_pers_day - dhw_preparation_temperature = usage_zone.dhw_preparation_temperature - electrical_app_average_consumption_sqm_year = usage_zone.electrical_app_average_consumption_sqm_year + # the variants mimic the inheritance concept from OOP + usage_zone_archetype = usage_zone + usage_zone_archetype.usage = usage - # todo: for internal_gain in usage_zone_variant['schedules']['internGains']:???????????????? - # There are no more internal gains? How is it saved when more than one??? - # for internal_gain in usage_zone.internal_gains: - internal_gains = usage_zone.not_detailed_source_mean_annual_internal_gains[0] - latent_fraction = internal_gains.latent_fraction - convective_fraction = internal_gains.convective_fraction - average_internal_gain = internal_gains.average_internal_gain - radiative_fraction = internal_gains.radiative_fraction + if 'occupancy' in usage_zone_variant: + _occupancy = Occupancy() + if 'occupancyDensity' in usage_zone_variant['occupancy']: + _occupancy.occupancy_density = usage_zone_variant['occupancy']['occupancyDensity'] # todo: check units + if 'usageHoursPerDay' in usage_zone_variant['occupancy']: + usage_zone_archetype.hours_day = usage_zone_variant['occupancy']['usageHoursPerDay'] + if 'usageDaysPerYear' in usage_zone_variant['occupancy']: + usage_zone_archetype.days_year = usage_zone_variant['occupancy']['usageDaysPerYear'] + usage_zone_archetype.occupancy = _occupancy + + if 'internGains' in usage_zone_variant['occupancy']: + _internal_gain = InternalGains() + if 'latentFraction' in usage_zone_variant['occupancy']['internGains']: + _internal_gain.latent_fraction = usage_zone_variant['occupancy']['internGains']['latentFraction'] + if 'convectiveFraction' in usage_zone_variant['occupancy']['internGains']: + _internal_gain.convective_fraction = usage_zone_variant['occupancy']['internGains']['convectiveFraction'] + if 'averageInternGainPerSqm' in usage_zone_variant['occupancy']['internGains']: + _internal_gain.average_internal_gain = \ + usage_zone_variant['occupancy']['internGains']['averageInternGainPerSqm'] + if 'radiantFraction' in usage_zone_variant['occupancy']['internGains']: + _internal_gain.radiative_fraction = usage_zone_variant['occupancy']['internGains']['radiantFraction'] + if 'load' in usage_zone_variant['occupancy']['internGains']: + _schedule = Schedule() + _schedule.type = 'internal gains load' + _schedule.time_range = cte.DAY + _schedule.time_step = cte.HOUR + _schedule.data_type = cte.ANY_NUMBER + _schedule.day_types = [cte.MONDAY, cte.TUESDAY, cte.WEDNESDAY, cte.THURSDAY, cte.FRIDAY] + _values = usage_zone_variant['occupancy']['internGains']['load']['weekDayProfile']['values'] + while ' ' in _values: + _values = _values.replace(' ', ' ') + _schedule.values = _values.split() + _internal_gain.schedules = [_schedule] + + usage_zone_archetype.not_detailed_source_mean_annual_internal_gains = [_internal_gain] + + if 'endUses' in usage_zone_variant: + _thermal_control = ThermalControl() + if 'space_heating' in usage_zone_variant['endUses']: + if 'heatingSetPointTemperature' in usage_zone_variant['endUses']['space_heating']: + _thermal_control.mean_heating_set_point = \ + usage_zone_variant['endUses']['space_heating']['heatingSetPointTemperature'] + if 'heatingSetBackTemperature' in usage_zone_variant['endUses']['space_heating']: + _thermal_control.heating_set_back = usage_zone_variant['endUses']['space_heating']['heatingSetBackTemperature'] + if 'schedule' in usage_zone_variant['endUses']['space_heating']: + _schedule = Schedule() + _schedule.type = 'heating temperature' + _schedule.time_range = cte.DAY + _schedule.time_step = cte.HOUR + _schedule.data_type = cte.TEMPERATURE + _schedule.day_types = [cte.MONDAY, cte.TUESDAY, cte.WEDNESDAY, cte.THURSDAY, cte.FRIDAY] + _values = usage_zone_variant['endUses']['space_heating']['schedule']['weekDayProfile']['values'] + while ' ' in _values: + _values = _values.replace(' ', ' ') + _schedule.values = _values.split() + _thermal_control.heating_set_point_schedules = [_schedule] + + if 'space_cooling' in usage_zone_variant['endUses'] and \ + usage_zone_variant['endUses']['space_cooling'] is not None: + if 'coolingSetPointTemperature' in usage_zone_variant['endUses']['space_cooling']: + _thermal_control.mean_cooling_set_point = \ + usage_zone_variant['endUses']['space_cooling']['coolingSetPointTemperature'] + if 'schedule' in usage_zone_variant['endUses']['space_cooling']: + _schedule = Schedule() + _schedule.type = 'cooling temperature' + _schedule.time_range = cte.DAY + _schedule.time_step = cte.HOUR + _schedule.data_type = cte.TEMPERATURE + _schedule.day_types = [cte.MONDAY, cte.TUESDAY, cte.WEDNESDAY, cte.THURSDAY, cte.FRIDAY] + _values = usage_zone_variant['endUses']['space_cooling']['schedule']['weekDayProfile']['values'] + while ' ' in _values: + _values = _values.replace(' ', ' ') + _schedule.values = _values.split() + _thermal_control.cooling_set_point_schedules = [_schedule] + + usage_zone_archetype.thermal_control = _thermal_control + + if 'ventilation' in usage_zone_variant['endUses'] and usage_zone_variant['endUses']['ventilation'] is not None: + usage_zone_archetype.mechanical_air_change = \ + usage_zone_variant['endUses']['ventilation']['mechanicalAirChangeRate'] + + if 'appliance' in usage_zone_variant: + _appliances = Appliances() + _appliances.appliances_density = usage_zone_variant['appliance']['#text'] # todo: check units + + usage_zone_archetype.appliances = _appliances - if 'space_cooling' in usage_zone_variant['endUses'] and usage_zone_variant['endUses']['space_cooling'] is not None: - if 'coolingSetPointTemperature' in usage_zone_variant['endUses']['space_cooling']: - cooling_setpoint = usage_zone_variant['endUses']['space_cooling']['coolingSetPointTemperature'] - if 'space_heating' in usage_zone_variant['endUses'] and usage_zone_variant['endUses']['space_heating'] is not None: - if 'heatingSetPointTemperature' in usage_zone_variant['endUses']['space_heating']: - heating_setpoint = usage_zone_variant['endUses']['space_heating']['heatingSetPointTemperature'] - if 'heatingSetBackTemperature' in usage_zone_variant['endUses']['space_heating']: - heating_setback = usage_zone_variant['endUses']['space_heating']['heatingSetBackTemperature'] - if 'ventilation' in usage_zone_variant['endUses'] and usage_zone_variant['endUses']['ventilation'] is not None: - if 'mechanicalAirChangeRate' in usage_zone_variant['endUses']['ventilation']: - mechanical_air_change = usage_zone_variant['endUses']['ventilation']['mechanicalAirChangeRate'] - # todo: for internal_gain in usage_zone_variant['schedules']['internGains']:???????????????? - # There are no more internal gains? How is it saved when more than one??? - if 'schedules' in usage_zone_variant: - if 'usageHoursPerDay' in usage_zone_variant['schedules']: - hours_day = usage_zone_variant['schedules']['usageHoursPerDay'] - if 'usageDaysPerYear' in usage_zone_variant['schedules']: - days_year = usage_zone_variant['schedules']['usageDaysPerYear'] - if 'internalGains' in usage_zone_variant['schedules'] and usage_zone_variant['schedules'][ - 'internGains'] is not None: - internal_gains = [] - if 'latentFraction' in usage_zone_variant['schedules']['internGains']: - latent_fraction = usage_zone_variant['schedules']['internGains']['latentFraction'] - if 'convectiveFraction' in usage_zone_variant['schedules']['internGains']: - convective_fraction = usage_zone_variant['schedules']['internGains']['convectiveFraction'] - if 'averageInternGainPerSqm' in usage_zone_variant['schedules']['internGains']: - average_internal_gain = usage_zone_variant['schedules']['internGains']['averageInternGainPerSqm'] - if 'radiantFraction' in usage_zone_variant['schedules']['internGains']: - radiative_fraction = usage_zone_variant['schedules']['internGains']['radiantFraction'] - internal_gains.append(higa(average_internal_gain=average_internal_gain, convective_fraction=convective_fraction, - radiative_fraction=radiative_fraction, latent_fraction=latent_fraction)) - usage_zone_archetype = huza(usage=usage, internal_gains=internal_gains, heating_set_point=heating_setpoint, - heating_set_back=heating_setback, cooling_set_point=cooling_setpoint, - occupancy_density=occupancy_density, hours_day=hours_day, days_year=days_year, - dhw_average_volume_pers_day=dhw_average_volume_pers_day, - dhw_preparation_temperature=dhw_preparation_temperature, - electrical_app_average_consumption_sqm_year=electrical_app_average_consumption_sqm_year, - mechanical_air_change=mechanical_air_change) return usage_zone_archetype diff --git a/imports/usage/hft_usage_parameters.py b/imports/usage/hft_usage_parameters.py index 7411891e..d95a2c3c 100644 --- a/imports/usage/hft_usage_parameters.py +++ b/imports/usage/hft_usage_parameters.py @@ -9,6 +9,9 @@ from imports.geometry.helpers.geometry_helper import GeometryHelper as gh from imports.usage.hft_usage_interface import HftUsageInterface from city_model_structure.building_demand.usage_zone import UsageZone from city_model_structure.building_demand.internal_gains import InternalGains +from city_model_structure.building_demand.occupancy import Occupancy +from city_model_structure.building_demand.appliances import Appliances +from city_model_structure.building_demand.thermal_control import ThermalControl class HftUsageParameters(HftUsageInterface): @@ -26,19 +29,21 @@ class HftUsageParameters(HftUsageInterface): """ city = self._city for building in city.buildings: - archetype = self._search_archetype(gh.usage_from_function(building.function)) - if archetype is None: + usage = gh.usage_from_function(building.function) + try: + archetype = self._search_archetype(usage) + except KeyError: sys.stderr.write(f'Building {building.name} has unknown archetype for building function:' f' {building.function}, that assigns building usage as ' f'{gh.usage_from_function(building.function)}\n') - continue + return for internal_zone in building.internal_zones: usage_zone = UsageZone() - usage_zone.usage = building.function + usage_zone.usage = building.function self._assign_values(usage_zone, archetype) - usage_zone.percentage = 1 - internal_zone.usage_zones = [usage_zone] + usage_zone.percentage = 1 + internal_zone.usage_zones = [usage_zone] def _search_archetype(self, building_usage): for building_archetype in self._usage_archetypes: @@ -51,22 +56,30 @@ class HftUsageParameters(HftUsageInterface): # Due to the fact that python is not a typed language, the wrong object type is assigned to # usage_zone.internal_gains when writing usage_zone.internal_gains = archetype.internal_gains. # Therefore, this walk around has been done. - internal_gains = [] - for archetype_internal_gain in archetype.internal_gains: - internal_gain = InternalGains() - internal_gain.average_internal_gain = archetype_internal_gain.average_internal_gain - internal_gain.convective_fraction = archetype_internal_gain.convective_fraction - internal_gain.radiative_fraction = archetype_internal_gain.radiative_fraction - internal_gain.latent_fraction = archetype_internal_gain.latent_fraction - internal_gains.append(internal_gain) - usage_zone.internal_gains = internal_gains - usage_zone.heating_setpoint = archetype.heating_setpoint - usage_zone.heating_setback = archetype.heating_setback - usage_zone.cooling_setpoint = archetype.cooling_setpoint - usage_zone.occupancy_density = archetype.occupancy_density - usage_zone.hours_day = archetype.hours_day - usage_zone.days_year = archetype.days_year - usage_zone.dhw_average_volume_pers_day = archetype.dhw_average_volume_pers_day - usage_zone.dhw_preparation_temperature = archetype.dhw_preparation_temperature - usage_zone.electrical_app_average_consumption_sqm_year = archetype.electrical_app_average_consumption_sqm_year + # Due to the fact that python is not a typed language, the wrong object type is assigned to + # usage_zone.occupancy when writing usage_zone.occupancy = archetype.occupancy. + # Same happens for lighting and appliances. Therefore, this walk around has been done. usage_zone.mechanical_air_change = archetype.mechanical_air_change + _occupancy = Occupancy() + _occupancy.occupancy_density = archetype.occupancy.occupancy_density + usage_zone.occupancy = _occupancy + _appliances = Appliances() + _appliances.appliances_density = archetype.appliances.appliances_density + usage_zone.appliances = _appliances + _control = ThermalControl() + _control.mean_heating_set_point = archetype.thermal_control.mean_heating_set_point + _control.heating_set_back = archetype.thermal_control.heating_set_back + _control.mean_cooling_set_point = archetype.thermal_control.mean_cooling_set_point + _control.cooling_set_point_schedules = archetype.thermal_control.cooling_set_point_schedules + _control.heating_set_point_schedules = archetype.thermal_control.heating_set_point_schedules + usage_zone.thermal_control = _control + _internal_gains = [] + for archetype_internal_gain in archetype.not_detailed_source_mean_annual_internal_gains: + _internal_gain = InternalGains() + _internal_gain.average_internal_gain = archetype_internal_gain.average_internal_gain + _internal_gain.convective_fraction = archetype_internal_gain.convective_fraction + _internal_gain.radiative_fraction = archetype_internal_gain.radiative_fraction + _internal_gain.latent_fraction = archetype_internal_gain.latent_fraction + _internal_gain.schedules = archetype_internal_gain.schedules + _internal_gains.append(_internal_gain) + usage_zone.not_detailed_source_mean_annual_internal_gains = _internal_gains diff --git a/unittests/test_construction_factory.py b/unittests/test_construction_factory.py index e0ece40a..df7e1edd 100644 --- a/unittests/test_construction_factory.py +++ b/unittests/test_construction_factory.py @@ -47,12 +47,13 @@ class TestConstructionFactory(TestCase): self.assertEqual(len(building.beam), 0, 'building beam is calculated') self.assertIsNotNone(building.lower_corner, 'building lower corner is none') self.assertEqual(len(building.sensors), 0, 'building sensors are assigned') - self.assertIsNotNone(building.geometrical_zones, 'no geometrical zones created') + self.assertIsNotNone(building.internal_zones, 'no internal zones created') self.assertIsNotNone(building.grounds, 'building grounds is none') self.assertIsNotNone(building.walls, 'building walls is none') self.assertIsNotNone(building.roofs, 'building roofs is none') - self.assertIsNone(building.usage_zones, 'usage zones are defined') - self.assertTrue(len(building.thermal_zones) > 0, 'thermal zones are not defined') + for internal_zone in building.internal_zones: + self.assertIsNone(internal_zone.usage_zones, 'usage zones are defined') + self.assertTrue(len(internal_zone.thermal_zones) > 0, 'thermal zones are not defined') self.assertIsNone(building.basement_heated, 'building basement_heated is not none') self.assertIsNone(building.attic_heated, 'building attic_heated is not none') self.assertIsNone(building.terrains, 'building terrains is not none') @@ -69,8 +70,8 @@ class TestConstructionFactory(TestCase): self.assertIsNone(building.households, 'building households is not none') self.assertFalse(building.is_conditioned, 'building is conditioned') - def _check_thermal_zones(self, building): - for thermal_zone in building.thermal_zones: + def _check_thermal_zones(self, internal_zone): + for thermal_zone in internal_zone.thermal_zones: self.assertIsNotNone(thermal_zone.id, 'thermal_zone id is none') self.assertIsNotNone(thermal_zone.floor_area, 'thermal_zone floor area is none') self.assertTrue(len(thermal_zone.thermal_boundaries) > 0, 'thermal_zone thermal_boundaries not defined') @@ -81,9 +82,10 @@ class TestConstructionFactory(TestCase): self.assertIsNotNone(thermal_zone.infiltration_rate_system_off, 'thermal_zone infiltration_rate_system_off is none') self.assertIsNotNone(thermal_zone.infiltration_rate_system_on, 'thermal_zone infiltration_rate_system_on is none') - self.assertIsNone(thermal_zone.usage_zones, 'thermal_zone usage_zones is not none') self.assertIsNotNone(thermal_zone.volume, 'thermal_zone volume is none') self.assertIsNone(thermal_zone.ordinate_number, 'thermal_zone ordinate number is not none') + self.assertIsNotNone(thermal_zone.view_factors_matrix, 'thermal_zone view factors matrix is none') + self.assertIsNone(thermal_zone.usage_zones, 'thermal_zone usage_zones is not none') self.assertIsNone(thermal_zone.thermal_control, 'thermal_zone thermal_control is not none') self.assertIsNone(thermal_zone.hvac_system, 'thermal_zone hvac_system is not none') @@ -164,27 +166,28 @@ class TestConstructionFactory(TestCase): file = 'one_building_in_kelowna.gml' city = self._get_citygml(file) for building in city.buildings: - building.function = GeometryHelper.hft_to_function[building.function] + building.function = GeometryHelper.function_from_hft(building.function) ConstructionFactory('nrcan', city).enrich() self._check_buildings(city) for building in city.buildings: - self._check_thermal_zones(building) - for thermal_zone in building.thermal_zones: - self._check_thermal_boundaries(thermal_zone) - for thermal_boundary in thermal_zone.thermal_boundaries: - self.assertIsNone(thermal_boundary.outside_thermal_absorptance, 'outside_thermal_absorptance is not none') - self.assertIsNone(thermal_boundary.outside_visible_absorptance, 'outside_visible_absorptance is not none') - self.assertIsNone(thermal_boundary.layers, 'layers is not none') + for internal_zone in building.internal_zones: + self._check_thermal_zones(internal_zone) + for thermal_zone in internal_zone.thermal_zones: + self._check_thermal_boundaries(thermal_zone) + for thermal_boundary in thermal_zone.thermal_boundaries: + self.assertIsNone(thermal_boundary.outside_thermal_absorptance, 'outside_thermal_absorptance is not none') + self.assertIsNone(thermal_boundary.outside_visible_absorptance, 'outside_visible_absorptance is not none') + self.assertIsNone(thermal_boundary.layers, 'layers is not none') - self._check_thermal_openings(thermal_boundary) - for thermal_opening in thermal_boundary.thermal_openings: - self.assertIsNone(thermal_opening.conductivity, 'thermal_opening conductivity is not none') - self.assertIsNone(thermal_opening.thickness, 'thermal opening thickness is not none') - self.assertIsNone(thermal_opening.front_side_solar_transmittance_at_normal_incidence, - 'thermal opening front_side_solar_transmittance_at_normal_incidence is not none') - self.assertIsNone(thermal_opening.back_side_solar_transmittance_at_normal_incidence, - 'thermal opening back_side_solar_transmittance_at_normal_incidence is not none') + self._check_thermal_openings(thermal_boundary) + for thermal_opening in thermal_boundary.thermal_openings: + self.assertIsNone(thermal_opening.conductivity, 'thermal_opening conductivity is not none') + self.assertIsNone(thermal_opening.thickness, 'thermal opening thickness is not none') + self.assertIsNone(thermal_opening.front_side_solar_transmittance_at_normal_incidence, + 'thermal opening front_side_solar_transmittance_at_normal_incidence is not none') + self.assertIsNone(thermal_opening.back_side_solar_transmittance_at_normal_incidence, + 'thermal opening back_side_solar_transmittance_at_normal_incidence is not none') def test_city_with_construction_extended_library(self): """ @@ -193,41 +196,39 @@ class TestConstructionFactory(TestCase): file = 'pluto_building.gml' city = self._get_citygml(file) for building in city.buildings: - building.function = GeometryHelper.pluto_to_function[building.function] + building.function = GeometryHelper.function_from_pluto(building.function) ConstructionFactory('nrel', city).enrich() self._check_buildings(city) for building in city.buildings: - self._check_thermal_zones(building) - for thermal_zone in building.thermal_zones: - self._check_thermal_boundaries(thermal_zone) - for thermal_boundary in thermal_zone.thermal_boundaries: - if thermal_boundary.type is not cte.GROUND: - self.assertIsNotNone(thermal_boundary.outside_thermal_absorptance, 'outside_thermal_absorptance is none') - self.assertIsNotNone(thermal_boundary.outside_visible_absorptance, 'outside_visible_absorptance is none') - else: - self.assertIsNone(thermal_boundary.outside_thermal_absorptance, 'outside_thermal_absorptance is not none') - self.assertIsNone(thermal_boundary.outside_visible_absorptance, 'outside_visible_absorptance is not none') - self.assertIsNotNone(thermal_boundary.layers, 'layers is none') + for internal_zone in building.internal_zones: + self._check_thermal_zones(internal_zone) + for thermal_zone in internal_zone.thermal_zones: + self._check_thermal_boundaries(thermal_zone) + for thermal_boundary in thermal_zone.thermal_boundaries: + if thermal_boundary.type is not cte.GROUND: + self.assertIsNotNone(thermal_boundary.outside_thermal_absorptance, 'outside_thermal_absorptance is none') + self.assertIsNotNone(thermal_boundary.outside_visible_absorptance, 'outside_visible_absorptance is none') + else: + self.assertIsNone(thermal_boundary.outside_thermal_absorptance, 'outside_thermal_absorptance is not none') + self.assertIsNone(thermal_boundary.outside_visible_absorptance, 'outside_visible_absorptance is not none') + self.assertIsNotNone(thermal_boundary.layers, 'layers is none') - self._check_thermal_openings(thermal_boundary) - for thermal_opening in thermal_boundary.thermal_openings: - self.assertIsNotNone(thermal_opening.conductivity, 'thermal_opening conductivity is none') - self.assertIsNotNone(thermal_opening.thickness, 'thermal opening thickness is none') - self.assertIsNotNone(thermal_opening.front_side_solar_transmittance_at_normal_incidence, - 'thermal opening front_side_solar_transmittance_at_normal_incidence is none') - self.assertIsNotNone(thermal_opening.back_side_solar_transmittance_at_normal_incidence, - 'thermal opening back_side_solar_transmittance_at_normal_incidence is none') + self._check_thermal_openings(thermal_boundary) + for thermal_opening in thermal_boundary.thermal_openings: + self.assertIsNotNone(thermal_opening.conductivity, 'thermal_opening conductivity is none') + self.assertIsNotNone(thermal_opening.thickness, 'thermal opening thickness is none') + self.assertIsNotNone(thermal_opening.front_side_solar_transmittance_at_normal_incidence, + 'thermal opening front_side_solar_transmittance_at_normal_incidence is none') + self.assertIsNotNone(thermal_opening.back_side_solar_transmittance_at_normal_incidence, + 'thermal opening back_side_solar_transmittance_at_normal_incidence is none') @staticmethod def _internal_function(function_format, original_function): if function_format == 'hft': - new_function = GeometryHelper.hft_to_function[original_function] + new_function = GeometryHelper.function_from_hft(original_function) elif function_format == 'pluto': - new_function = GeometryHelper.pluto_to_function[original_function] - elif function_format == 'alkis': - # todo: not implemented yet!! - raise NotImplementedError + new_function = GeometryHelper.function_from_pluto(original_function) else: raise Exception('Function key not recognized. Implemented only "hft" and "pluto"') return new_function diff --git a/unittests/test_enrichement.py b/unittests/test_enrichement.py new file mode 100644 index 00000000..7c5861d5 --- /dev/null +++ b/unittests/test_enrichement.py @@ -0,0 +1,145 @@ +""" +TestGeometryFactory test and validate the city model structure geometric parameters +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +""" +from pathlib import Path +from unittest import TestCase +from imports.geometry_factory import GeometryFactory +from imports.geometry.helpers.geometry_helper import GeometryHelper +from imports.usage_factory import UsageFactory +from imports.construction_factory import ConstructionFactory + + +class TestGeometryFactory(TestCase): + """ + Non-functional TestGeometryFactory + Load testing + """ + def setUp(self) -> None: + """ + Test setup + :return: None + """ + self._city = None + self._example_path = (Path(__file__).parent / 'tests_data').resolve() + + def _get_citygml(self, file): + file_path = (self._example_path / file).resolve() + self._city = GeometryFactory('citygml', file_path).city + self.assertIsNotNone(self._city, 'city is none') + return self._city + + def _check_buildings(self, city): + for building in city.buildings: + self.assertIsNotNone(building.internal_zones, 'no internal zones created') + for internal_zone in building.internal_zones: + self.assertIsNotNone(internal_zone.usage_zones, 'usage zones are not defined') + self.assertIsNotNone(internal_zone.thermal_zones, 'thermal zones are not defined') + #self.assertIsNotNone(building.basement_heated, 'building basement_heated is none') + #self.assertIsNotNone(building.attic_heated, 'building attic_heated is none') + self.assertIsNotNone(building.average_storey_height, 'building average_storey_height is none') + self.assertIsNotNone(building.storeys_above_ground, 'building storeys_above_ground is none') + self.assertTrue(building.is_conditioned, 'building is_conditioned is not conditioned') + + def _check_usage_zone(self, usage_zone): + self.assertIsNotNone(usage_zone.id, 'usage id is none') + + def _check_thermal_zones(self, thermal_zone): + self.assertIsNotNone(thermal_zone.id, 'thermal_zone id is none') + self.assertIsNone(thermal_zone.usage_zones, 'thermal_zone usage_zones is not none') + self.assertIsNone(thermal_zone.thermal_control, 'thermal_zone thermal_control is not none') + + @staticmethod + def _prepare_case_usage_first(city, input_key, construction_key, usage_key): + if input_key == 'pluto': + for building in city.buildings: + building.function = GeometryHelper.function_from_pluto(building.function) + elif input_key == 'hft': + for building in city.buildings: + building.function = GeometryHelper.function_from_hft(building.function) + UsageFactory(usage_key, city).enrich() + ConstructionFactory(construction_key, city).enrich() + + @staticmethod + def _prepare_case_construction_first(city, input_key, construction_key, usage_key): + if input_key == 'pluto': + for building in city.buildings: + building.function = GeometryHelper.function_from_pluto(building.function) + elif input_key == 'hft': + for building in city.buildings: + building.function = GeometryHelper.function_from_hft(building.function) + ConstructionFactory(construction_key, city).enrich() + UsageFactory(usage_key, city).enrich() + + def test_enrichment(self): + """ + Test enrichment of the city with different order and all possible combinations + :return: None + """ + file_1 = 'one_building_in_kelowna.gml' + file_2 = 'pluto_building.gml' + file_3 = 'C40_Final.gml' + _construction_keys = ['nrel', 'nrcan'] + _usage_keys = ['ca', 'comnet'] # todo: add 'hft' + + for construction_key in _construction_keys: + for usage_key in _usage_keys: + city = self._get_citygml(file_1) + self.assertTrue(len(city.buildings) == 1) + self._prepare_case_construction_first(city, 'hft', construction_key, usage_key) + self._check_buildings(city) + for building in city.buildings: + for internal_zone in building.internal_zones: + self.assertIsNot(len(internal_zone.usage_zones), 0, 'no building usage_zones defined') + for usage_zone in internal_zone.usage_zones: + self._check_usage_zone(usage_zone) + for thermal_zone in internal_zone.thermal_zones: + self._check_thermal_zones(thermal_zone) + + for construction_key in _construction_keys: + for usage_key in _usage_keys: + city = self._get_citygml(file_1) + self.assertTrue(len(city.buildings) == 1) + self._prepare_case_usage_first(city, 'hft', construction_key, usage_key) + self._check_buildings(city) + for building in city.buildings: + for internal_zone in building.internal_zones: + self.assertIsNot(len(internal_zone.usage_zones), 0, 'no building usage_zones defined') + for usage_zone in internal_zone.usage_zones: + self._check_usage_zone(usage_zone) + for thermal_zone in internal_zone.thermal_zones: + self._check_thermal_zones(thermal_zone) + + for construction_key in _construction_keys: + for usage_key in _usage_keys: + if usage_key != 'ca': + city = self._get_citygml(file_2) + self.assertTrue(len(city.buildings) == 1) + self._prepare_case_construction_first(city, 'pluto', construction_key, usage_key) + self._check_buildings(city) + for building in city.buildings: + for internal_zone in building.internal_zones: + self.assertIsNot(len(internal_zone.usage_zones), 0, 'no building usage_zones defined') + for usage_zone in internal_zone.usage_zones: + self._check_usage_zone(usage_zone) + for thermal_zone in internal_zone.thermal_zones: + self._check_thermal_zones(thermal_zone) + + for construction_key in _construction_keys: + for usage_key in _usage_keys: + if usage_key != 'ca': + city = self._get_citygml(file_2) + self.assertTrue(len(city.buildings) == 1) + self._prepare_case_usage_first(city, 'pluto', construction_key, usage_key) + self._check_buildings(city) + for building in city.buildings: + for internal_zone in building.internal_zones: + self.assertIsNot(len(internal_zone.usage_zones), 0, 'no building usage_zones defined') + for usage_zone in internal_zone.usage_zones: + self._check_usage_zone(usage_zone) + for thermal_zone in internal_zone.thermal_zones: + self._check_thermal_zones(thermal_zone) + + city = self._get_citygml(file_3) + self.assertTrue(len(city.buildings) == 10) diff --git a/unittests/test_geometry_factory.py b/unittests/test_geometry_factory.py index b9142591..db5ae4bb 100644 --- a/unittests/test_geometry_factory.py +++ b/unittests/test_geometry_factory.py @@ -51,7 +51,7 @@ class TestGeometryFactory(TestCase): self.assertEqual(len(building.beam), 0, 'building beam is calculated') self.assertIsNotNone(building.lower_corner, 'building lower corner is none') self.assertEqual(len(building.sensors), 0, 'building sensors are assigned') - self.assertIsNotNone(building.geometrical_zones, 'no geometrical zones created') + self.assertIsNotNone(building.internal_zones, 'no internal zones created') self.assertIsNotNone(building.grounds, 'building grounds is none') self.assertIsNotNone(building.walls, 'building walls is none') self.assertIsNotNone(building.roofs, 'building roofs is none') diff --git a/unittests/test_usage_factory.py b/unittests/test_usage_factory.py index b884259d..948cf4e4 100644 --- a/unittests/test_usage_factory.py +++ b/unittests/test_usage_factory.py @@ -8,8 +8,6 @@ from unittest import TestCase from imports.geometry_factory import GeometryFactory from imports.usage_factory import UsageFactory -from imports.schedules_factory import SchedulesFactory -from imports.construction_factory import ConstructionFactory from imports.geometry.helpers.geometry_helper import GeometryHelper from imports.construction_factory import ConstructionFactory @@ -49,34 +47,40 @@ class TestUsageFactory(TestCase): self.assertEqual(len(building.beam), 0, 'building beam is calculated') self.assertIsNotNone(building.lower_corner, 'building lower corner is none') self.assertEqual(len(building.sensors), 0, 'building sensors are assigned') - self.assertIsNotNone(building.geometrical_zones, 'no geometrical zones created') + self.assertIsNotNone(building.internal_zones, 'no internal zones created') self.assertIsNotNone(building.grounds, 'building grounds is none') self.assertIsNotNone(building.walls, 'building walls is none') self.assertIsNotNone(building.roofs, 'building roofs is none') - self.assertTrue(len(building.usage_zones) > 0, 'usage zones are not defined') - self.assertTrue(len(building.thermal_zones) > 0, 'thermal zones are not defined') - self.assertIsNotNone(building.basement_heated, 'building basement_heated is none') - self.assertIsNotNone(building.attic_heated, 'building attic_heated is none') + for internal_zone in building.internal_zones: + self.assertTrue(len(internal_zone.usage_zones) > 0, 'usage zones are not defined') + self.assertIsNone(building.thermal_zones, 'thermal zones are defined') + self.assertIsNone(building.basement_heated, 'building basement_heated is not none') + self.assertIsNone(building.attic_heated, 'building attic_heated is not none') self.assertIsNone(building.terrains, 'building terrains is not none') self.assertIsNotNone(building.year_of_construction, 'building year_of_construction is none') self.assertIsNotNone(building.function, 'building function is none') - self.assertIsNotNone(building.average_storey_height, 'building average_storey_height is none') - self.assertIsNotNone(building.storeys_above_ground, 'building storeys_above_ground is none') + self.assertIsNone(building.average_storey_height, 'building average_storey_height is not none') + self.assertIsNone(building.storeys_above_ground, 'building storeys_above_ground is not none') self.assertEqual(len(building.heating), 0, 'building heating is not none') self.assertEqual(len(building.cooling), 0, 'building cooling is not none') self.assertIsNotNone(building.eave_height, 'building eave height is none') - self.assertIsNotNone(building.storeys, 'building storeys are not defined') + self.assertIsNone(building.storeys, 'building storeys are defined') self.assertIsNotNone(building.roof_type, 'building roof type is none') self.assertIsNotNone(building.floor_area, 'building floor_area is none') self.assertIsNone(building.households, 'building households is not none') self.assertTrue(building.is_conditioned, 'building is not conditioned') - - def _check_hvac(self, thermal_zone): - self.assertIsNotNone(None, 'hvac') - - def _check_control(self, thermal_zone): - self.assertIsNotNone(None, 'control') + def _check_usage_zone(self, usage_zone): + self.assertIsNotNone(usage_zone.usage, 'usage is none') + self.assertIsNotNone(usage_zone.percentage, 'usage percentage is none') + self.assertIsNotNone(usage_zone.get_internal_gains, 'internal gains is none') + self.assertIsNotNone(usage_zone.hours_day, 'hours per day is none') + self.assertIsNotNone(usage_zone.days_year, 'days per year is none') + self.assertIsNotNone(usage_zone.mechanical_air_change, 'mechanical air change is none') + self.assertIsNotNone(usage_zone.thermal_control, 'thermal control is none') + self.assertIsNotNone(usage_zone.thermal_control.mean_heating_set_point, 'control heating set point is none') + self.assertIsNotNone(usage_zone.thermal_control.heating_set_back, 'control heating set back is none') + self.assertIsNotNone(usage_zone.thermal_control.mean_cooling_set_point, 'control cooling set point is none') def test_import_comnet(self): """ @@ -85,33 +89,45 @@ class TestUsageFactory(TestCase): file = 'pluto_building.gml' city = self._get_citygml(file) for building in city.buildings: - building.function = GeometryHelper.pluto_to_function[building.function] + building.function = GeometryHelper.function_from_pluto(building.function) UsageFactory('comnet', city).enrich() - SchedulesFactory('comnet', city).enrich() self._check_buildings(city) for building in city.buildings: for internal_zone in building.internal_zones: self.assertIsNot(len(internal_zone.usage_zones), 0, 'no building usage_zones defined') for usage_zone in internal_zone.usage_zones: - self._check_extended_usage(usage_zone) - - def test_import_hft(self): - """ - Enrich the city with the usage information from hft and verify it - """ - # todo: read schedules!! - file = 'pluto_building.gml' - city = self._get_citygml(file) - for building in city.buildings: - building.function = GeometryHelper.pluto_to_function[building.function] - - UsageFactory('hft', city).enrich() - for building in city.buildings: - for internal_zone in building.internal_zones: - self.assertIsNot(len(internal_zone.usage_zones), 0, 'no building usage_zones defined') - for usage_zone in internal_zone.usage_zones: - self._check_extended_usage(usage_zone) + self._check_usage_zone(usage_zone) + self.assertIsNotNone(usage_zone.thermal_control.heating_set_point_schedules, + 'control heating set point schedule is none') + self.assertIsNotNone(usage_zone.thermal_control.cooling_set_point_schedules, + 'control cooling set point schedule is none') + self.assertIsNotNone(usage_zone.occupancy, 'occupancy is none') + occupancy = usage_zone.occupancy + self.assertIsNotNone(occupancy.occupancy_density, 'occupancy density is none') + self.assertIsNotNone(occupancy.latent_internal_gain, 'occupancy latent internal gain is none') + self.assertIsNotNone(occupancy.sensible_convective_internal_gain, + 'occupancy sensible convective internal gain is none') + self.assertIsNotNone(occupancy.sensible_radiative_internal_gain, + 'occupancy sensible radiant internal gain is none') + self.assertIsNotNone(occupancy.occupancy_schedules, 'occupancy schedule is none') + self.assertIsNone(occupancy.occupants, 'occupancy density is not none') + self.assertIsNotNone(usage_zone.lighting, 'lighting is none') + lighting = usage_zone.lighting + self.assertIsNotNone(lighting.lighting_density, 'lighting density is none') + self.assertIsNotNone(lighting.latent_fraction, 'lighting latent fraction is none') + self.assertIsNotNone(lighting.convective_fraction, 'lighting convective fraction is none') + self.assertIsNotNone(lighting.radiative_fraction, 'lighting radiant fraction is none') + self.assertIsNotNone(lighting.schedules, 'lighting schedule is none') + self.assertIsNotNone(usage_zone.appliances, 'appliances is none') + appliances = usage_zone.appliances + self.assertIsNotNone(appliances.appliances_density, 'appliances density is none') + self.assertIsNotNone(appliances.latent_fraction, 'appliances latent fraction is none') + self.assertIsNotNone(appliances.convective_fraction, 'appliances convective fraction is none') + self.assertIsNotNone(appliances.radiative_fraction, 'appliances radiant fraction is none') + self.assertIsNotNone(appliances.schedules, 'appliances schedule is none') + self.assertIsNotNone(usage_zone.thermal_control.hvac_availability_schedules, + 'control hvac availability is none') def test_import_ca(self): """ @@ -120,34 +136,56 @@ class TestUsageFactory(TestCase): file = 'one_building_in_kelowna.gml' city = self._get_citygml(file) UsageFactory('ca', city).enrich() + self._check_buildings(city) for building in city.buildings: for internal_zone in building.internal_zones: self.assertIsNot(len(internal_zone.usage_zones), 0, 'no building usage_zones defined') for usage_zone in internal_zone.usage_zones: - self._check_reduced_usage(usage_zone) + self._check_usage_zone(usage_zone) + self.assertIsNotNone(usage_zone.not_detailed_source_mean_annual_internal_gains, + 'not detailed internal gains is none') - def _check_extended_usage(self, usage_zone): - self.assertIsNotNone(usage_zone.usage, 'usage is none') - self.assertIsNotNone(usage_zone.not_detailed_source_mean_annual_internal_gains, 'usage is none') - self.assertIsNotNone(usage_zone.cooling_setpoint, 'usage is none') - self.assertIsNotNone(usage_zone.heating_setback, 'usage is none') - self.assertIsNotNone(usage_zone.heating_setpoint, 'usage is none') - self.assertIsNotNone(usage_zone.occupancy_density, 'usage is none') - self.assertIsNotNone(usage_zone.hours_day, 'usage is none') - self.assertIsNotNone(usage_zone.days_year, 'usage is none') - self.assertIsNotNone(usage_zone.dhw_average_volume_pers_day, 'usage is none') - self.assertIsNotNone(usage_zone.dhw_preparation_temperature, 'usage is none') - self.assertIsNotNone(usage_zone.electrical_app_average_consumption_sqm_year, 'usage is none') - self.assertIsNotNone(usage_zone.is_heated, 'thermal_zone heated is none') - self.assertIsNotNone(usage_zone.is_cooled, 'thermal_zone cooled is none') + def test_import_hft(self): + """ + Enrich the city with the usage information from hft and verify it + """ + file = 'pluto_building.gml' + city = self._get_citygml(file) + for building in city.buildings: + building.function = GeometryHelper.function_from_pluto(building.function) - - def _check_reduced_usage(self, usage_zone): - self.assertIsNotNone(usage_zone.usage, 'usage is none') - self.assertIsNotNone(usage_zone.internal_gains, 'usage is none') - self.assertIsNotNone(usage_zone.cooling_setpoint, 'usage is none') - self.assertIsNotNone(usage_zone.heating_setback, 'usage is none') - self.assertIsNotNone(usage_zone.heating_setpoint, 'usage is none') - self.assertIsNotNone(usage_zone.occupancy_density, 'usage is none') - self.assertIsNotNone(usage_zone.hours_day, 'usage is none') - self.assertIsNotNone(usage_zone.days_year, 'usage is none') + UsageFactory('hft', city).enrich() + self._check_buildings(city) + for building in city.buildings: + for internal_zone in building.internal_zones: + self.assertIsNot(len(internal_zone.usage_zones), 0, 'no building usage_zones defined') + for usage_zone in internal_zone.usage_zones: + self._check_usage_zone(usage_zone) + self.assertIsNotNone(usage_zone.thermal_control.heating_set_point_schedules, + 'control heating set point schedule is none') + self.assertIsNotNone(usage_zone.thermal_control.cooling_set_point_schedules, + 'control cooling set point schedule is none') + self.assertIsNotNone(usage_zone.occupancy, 'occupancy is none') + occupancy = usage_zone.occupancy + self.assertIsNotNone(occupancy.occupancy_density, 'occupancy density is none') + self.assertIsNotNone(occupancy.latent_internal_gain, 'occupancy latent internal gain is none') + self.assertIsNotNone(occupancy.sensible_convective_internal_gain, + 'occupancy sensible convective internal gain is none') + self.assertIsNotNone(occupancy.sensible_radiative_internal_gain, + 'occupancy sensible radiant internal gain is none') + self.assertIsNotNone(occupancy.occupancy_schedules, 'occupancy schedule is none') + self.assertIsNone(occupancy.occupants, 'occupancy density is not none') + self.assertIsNotNone(usage_zone.lighting, 'lighting is none') + lighting = usage_zone.lighting + self.assertIsNotNone(lighting.lighting_density, 'lighting density is none') + self.assertIsNotNone(lighting.latent_fraction, 'lighting latent fraction is none') + self.assertIsNotNone(lighting.convective_fraction, 'lighting convective fraction is none') + self.assertIsNotNone(lighting.radiative_fraction, 'lighting radiant fraction is none') + self.assertIsNotNone(lighting.schedules, 'lighting schedule is none') + self.assertIsNotNone(usage_zone.appliances, 'appliances is none') + appliances = usage_zone.appliances + self.assertIsNotNone(appliances.appliances_density, 'appliances density is none') + self.assertIsNotNone(appliances.latent_fraction, 'appliances latent fraction is none') + self.assertIsNotNone(appliances.convective_fraction, 'appliances convective fraction is none') + self.assertIsNotNone(appliances.radiative_fraction, 'appliances radiant fraction is none') + self.assertIsNotNone(appliances.schedules, 'appliances schedule is none') From 7a29bef2393e4f16386c545bf5953ac7eea839cd Mon Sep 17 00:00:00 2001 From: Pilar Date: Thu, 24 Mar 2022 16:51:01 -0400 Subject: [PATCH 11/19] Major changes with two objectives: homogenize the usage parameters regardless the data source, and completely uncouple geometry and construction library (before the geometry was modified after reading storey high from construction library), and construction and usage libraries (before the usage library could only be read after construction library, now ant order is accepted). --- city_model_structure/building.py | 17 -- helpers/constants.py | 60 ++++--- imports/construction/ca_physics_parameters.py | 21 +-- .../helpers/construction_helper.py | 65 ++++--- .../helpers/storeys_generation.py | 61 ++++--- .../construction/nrel_physics_interface.py | 50 ++++++ imports/construction/us_physics_parameters.py | 19 +-- .../helpers/sanam_customized_usage_helper.py | 2 +- .../sanam_customized_usage_parameters.py | 92 +++++----- imports/customized_imports_factory.py | 4 - imports/geometry/helpers/geometry_helper.py | 158 ++++++++++-------- imports/schedules/doe_idf.py | 31 ++-- imports/schedules/helpers/schedules_helper.py | 14 +- imports/schedules_factory.py | 15 +- imports/usage/ca_usage_parameters.py | 9 +- imports/usage/comnet_usage_parameters.py | 10 +- imports/usage/helpers/usage_helper.py | 59 +++++-- imports/usage/hft_usage_interface.py | 5 +- imports/usage/hft_usage_parameters.py | 29 ++-- recognized_functions_and_usages.md | 64 +++++++ unittests/test_construction_factory.py | 11 +- unittests/test_customized_imports_factory.py | 12 +- unittests/test_doe_idf.py | 31 ++-- unittests/test_enrichement.py | 78 +++++---- unittests/test_exports.py | 5 +- unittests/test_geometry_factory.py | 7 +- unittests/test_schedules_factory.py | 45 +++-- unittests/test_usage_factory.py | 40 ++--- 28 files changed, 603 insertions(+), 411 deletions(-) create mode 100644 recognized_functions_and_usages.md diff --git a/city_model_structure/building.py b/city_model_structure/building.py index a94e55a6..bab214a1 100644 --- a/city_model_structure/building.py +++ b/city_model_structure/building.py @@ -8,7 +8,6 @@ contributors: Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca from typing import List, Union import numpy as np from city_model_structure.building_demand.surface import Surface -from city_model_structure.building_demand.storey import Storey from city_model_structure.city_object import CityObject from city_model_structure.building_demand.household import Household from city_model_structure.building_demand.internal_zone import InternalZone @@ -265,22 +264,6 @@ class Building(CityObject): self._eave_height = max(self._eave_height, wall.upper_corner[2]) return self._eave_height - @property - def storeys(self) -> List[Storey]: - """ - Get building storeys - :return: [Storey] - """ - return self._storeys - - @storeys.setter - def storeys(self, value): - """ - Set building storeys - :param value: [Storey] - """ - self._storeys = value - @property def roof_type(self): """ diff --git a/helpers/constants.py b/helpers/constants.py index 684a367f..b4291102 100644 --- a/helpers/constants.py +++ b/helpers/constants.py @@ -58,31 +58,47 @@ WINDOW = 'Window' DOOR = 'Door' SKYLIGHT = 'Skylight' -# todo: homogenize function and usage!! -# function -RESIDENTIAL = 'residential' -SFH = 'single family house' -MFH = 'multifamily house' -HOTEL = 'hotel' -HOSPITAL = 'hospital' -OUTPATIENT = 'outpatient' -COMMERCIAL = 'commercial' -STRIP_MALL = 'strip mall' -WAREHOUSE = 'warehouse' +# functions and usages +SINGLE_FAMILY_HOUSE = 'single family house' +MULTI_FAMILY_HOUSE = 'multifamily house' +ROW_HOSE = 'row house' +MID_RISE_APARTMENT = 'mid rise apartment' +HIGH_RISE_APARTMENT = 'high rise apartment' +SMALL_OFFICE = 'small office' +MEDIUM_OFFICE = 'medium office' +LARGE_OFFICE = 'large office' PRIMARY_SCHOOL = 'primary school' SECONDARY_SCHOOL = 'secondary school' -OFFICE = 'office' -LARGE_OFFICE = 'large office' -OFFICE_WORKSHOP = 'office/workshop' - -# usage -INDUSTRY = 'industry' -OFFICE_ADMINISTRATION = 'office and administration' -HEALTH_CARE = 'health care' -RETAIL = 'retail' -HALL = 'hall' -RESTAURANT = 'restaurant' +STAND_ALONE_RETAIL = 'stand alone retail' +HOSPITAL = 'hospital' +OUT_PATIENT_HEALTH_CARE = 'out-patient health care' +STRIP_MALL = 'strip mall' +SUPERMARKET = 'supermarket' +WAREHOUSE = 'warehouse' +QUICK_SERVICE_RESTAURANT = 'quick service restaurant' +FULL_SERVICE_RESTAURANT = 'full service restaurant' +SMALL_HOTEL = 'small hotel' +LARGE_HOTEL = 'large hotel' +RESIDENTIAL = 'residential' EDUCATION = 'education' +SCHOOL_WITHOUT_SHOWER = 'school without shower' +SCHOOL_WITH_SHOWER = 'school with shower' +RETAIL_SHOP_WITHOUT_REFRIGERATED_FOOD = 'retail shop without refrigerated food' +RETAIL_SHOP_WITH_REFRIGERATED_FOOD = 'retail shop with refrigerated food' +HOTEL = 'hotel' +HOTEL_MEDIUM_CLASS = 'hotel medium class' +DORMITORY = 'dormitory' +INDUSTRY = 'industry' +RESTAURANT = 'restaurant' +HEALTH_CARE = 'health care' +RETIREMENT_HOME_OR_ORPHANAGE = 'retirement home or orphanage' +OFFICE_AND_ADMINISTRATION = 'office and administration' +EVENT_LOCATION = 'event location' +HALL = 'hall' +SPORTS_LOCATION = 'sports location' +LABOR = 'labor' +GREEN_HOUSE = 'green house' +NON_HEATED = 'non-heated' LIGHTING = 'Lights' OCCUPANCY = 'Occupancy' diff --git a/imports/construction/ca_physics_parameters.py b/imports/construction/ca_physics_parameters.py index 5b2ff50c..6f4120fc 100644 --- a/imports/construction/ca_physics_parameters.py +++ b/imports/construction/ca_physics_parameters.py @@ -6,7 +6,6 @@ Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.mons import sys from imports.construction.helpers.construction_helper import ConstructionHelper from imports.construction.nrel_physics_interface import NrelPhysicsInterface -from imports.construction.helpers.storeys_generation import StoreysGeneration class CaPhysicsParameters(NrelPhysicsInterface): @@ -26,11 +25,11 @@ class CaPhysicsParameters(NrelPhysicsInterface): # it is assumed that all buildings have the same archetypes' keys for building in city.buildings: try: - archetype = self._search_archetype(ConstructionHelper.nrcan_from_function(building.function), + archetype = self._search_archetype(ConstructionHelper.nrcan_from_libs_function(building.function), building.year_of_construction) except KeyError: sys.stderr.write(f'Building {building.name} has unknown archetype for building function: ' - f'{ConstructionHelper.nrcan_from_function(building.function)} ' + f'{ConstructionHelper.nrcan_from_libs_function(building.function)} ' f'and building year of construction: {building.year_of_construction}\n') return @@ -38,12 +37,11 @@ class CaPhysicsParameters(NrelPhysicsInterface): if len(building.internal_zones) == 1: if building.internal_zones[0].thermal_zones is None: self._create_storeys(building, archetype) - thermal_zones = [] - for storey in building.storeys: - thermal_zones.append(storey.thermal_zone) - building.internal_zones[0].thermal_zones = thermal_zones self._assign_values(building.internal_zones, archetype) + for internal_zone in building.internal_zones: + for thermal_zone in internal_zone.thermal_zones: + self._calculate_view_factors(thermal_zone) def _search_archetype(self, function, year_of_construction): for building_archetype in self._building_archetypes: @@ -82,12 +80,3 @@ class CaPhysicsParameters(NrelPhysicsInterface): thermal_opening.frame_ratio = thermal_opening_archetype.frame_ratio thermal_opening.g_value = thermal_opening_archetype.g_value thermal_opening.overall_u_value = thermal_opening_archetype.overall_u_value - - @staticmethod - def _create_storeys(building, archetype): - building.average_storey_height = archetype.average_storey_height - building.storeys_above_ground = archetype.storeys_above_ground - storeys_generation = StoreysGeneration(building) - storeys = storeys_generation.storeys - building.storeys = storeys - storeys_generation.assign_thermal_zones_delimited_by_thermal_boundaries() diff --git a/imports/construction/helpers/construction_helper.py b/imports/construction/helpers/construction_helper.py index 32402984..ba93d614 100644 --- a/imports/construction/helpers/construction_helper.py +++ b/imports/construction/helpers/construction_helper.py @@ -14,18 +14,26 @@ class ConstructionHelper: # NREL _function_to_nrel = { cte.RESIDENTIAL: 'residential', - cte.SFH: 'single family house', - cte.MFH: 'multifamily house', - cte.HOTEL: 'hotel', - cte.HOSPITAL: 'hospital', - cte.OUTPATIENT: 'outpatient', - cte.COMMERCIAL: 'commercial', - cte.STRIP_MALL: 'strip mall', - cte.WAREHOUSE: 'warehouse', + cte.SINGLE_FAMILY_HOUSE: 'residential', + cte.MULTI_FAMILY_HOUSE: 'residential', + cte.ROW_HOSE: 'residential', + cte.MID_RISE_APARTMENT: 'midrise apartment', + cte.HIGH_RISE_APARTMENT: 'high-rise apartment', + cte.SMALL_OFFICE: 'small office', + cte.MEDIUM_OFFICE: 'medium office', + cte.LARGE_OFFICE: 'large office', cte.PRIMARY_SCHOOL: 'primary school', cte.SECONDARY_SCHOOL: 'secondary school', - cte.OFFICE: 'office', - cte.LARGE_OFFICE: 'large office' + cte.STAND_ALONE_RETAIL: 'stand-alone retail', + cte.HOSPITAL: 'hospital', + cte.OUT_PATIENT_HEALTH_CARE: 'outpatient healthcare', + cte.STRIP_MALL: 'strip mall', + cte.SUPERMARKET: 'supermarket', + cte.WAREHOUSE: 'warehouse', + cte.QUICK_SERVICE_RESTAURANT: 'quick service restaurant', + cte.FULL_SERVICE_RESTAURANT: 'full service restaurant', + cte.SMALL_HOTEL: 'small hotel', + cte.LARGE_HOTEL: 'large hotel' } _nrel_standards = { @@ -65,19 +73,26 @@ class ConstructionHelper: # NRCAN _function_to_nrcan = { cte.RESIDENTIAL: 'residential', - cte.SFH: 'single family house', - cte.MFH: 'multifamily house', - cte.HOTEL: 'hotel', - cte.HOSPITAL: 'hospital', - cte.OUTPATIENT: 'outpatient', - cte.COMMERCIAL: 'commercial', - cte.STRIP_MALL: 'strip mall', - cte.WAREHOUSE: 'warehouse', - cte.PRIMARY_SCHOOL: 'primary school', - cte.SECONDARY_SCHOOL: 'secondary school', - cte.OFFICE: 'office', - cte.LARGE_OFFICE: 'large office', - cte.OFFICE_WORKSHOP: 'residential' + cte.SINGLE_FAMILY_HOUSE: 'residential', + cte.MULTI_FAMILY_HOUSE: 'residential', + cte.ROW_HOSE: 'residential', + cte.MID_RISE_APARTMENT: 'residential', + cte.HIGH_RISE_APARTMENT: 'residential', + cte.SMALL_OFFICE: cte.SMALL_OFFICE, + cte.MEDIUM_OFFICE: cte.MEDIUM_OFFICE, + cte.LARGE_OFFICE: cte.LARGE_OFFICE, + cte.PRIMARY_SCHOOL: cte.PRIMARY_SCHOOL, + cte.SECONDARY_SCHOOL: cte.SECONDARY_SCHOOL, + cte.STAND_ALONE_RETAIL: cte.STAND_ALONE_RETAIL, + cte.HOSPITAL: cte.HOSPITAL, + cte.OUT_PATIENT_HEALTH_CARE: cte.OUT_PATIENT_HEALTH_CARE, + cte.STRIP_MALL: cte.STRIP_MALL, + cte.SUPERMARKET: cte.SUPERMARKET, + cte.WAREHOUSE: cte.WAREHOUSE, + cte.QUICK_SERVICE_RESTAURANT: cte.QUICK_SERVICE_RESTAURANT, + cte.FULL_SERVICE_RESTAURANT: cte.FULL_SERVICE_RESTAURANT, + cte.SMALL_HOTEL: cte.SMALL_HOTEL, + cte.LARGE_HOTEL: cte.LARGE_HOTEL } nrcan_window_types = [cte.WINDOW] @@ -92,7 +107,7 @@ class ConstructionHelper: } @staticmethod - def nrel_from_function(function): + def nrel_from_libs_function(function): """ Get NREL function from the given internal function key :param function: str @@ -140,7 +155,7 @@ class ConstructionHelper: return ConstructionHelper._reference_city_to_nrel_climate_zone[reference_city] @staticmethod - def nrcan_from_function(function): + def nrcan_from_libs_function(function): """ Get NREL function from the given internal function key :param function: str diff --git a/imports/construction/helpers/storeys_generation.py b/imports/construction/helpers/storeys_generation.py index b67a820d..da0b4dcc 100644 --- a/imports/construction/helpers/storeys_generation.py +++ b/imports/construction/helpers/storeys_generation.py @@ -6,12 +6,14 @@ Copyright © 2022 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.mons import sys import math import numpy as np +from typing import List from helpers import constants as cte from city_model_structure.attributes.polygon import Polygon from city_model_structure.attributes.point import Point from city_model_structure.building_demand.storey import Storey from city_model_structure.building_demand.surface import Surface +from city_model_structure.building_demand.thermal_zone import ThermalZone class StoreysGeneration: @@ -20,14 +22,14 @@ class StoreysGeneration: """ def __init__(self, building, divide_in_storeys=False): self._building = building + self._thermal_zones = [] self._divide_in_storeys = divide_in_storeys - self._storeys = None self._floor_area = 0 for ground in building.grounds: self._floor_area += ground.perimeter_polygon.area @property - def storeys(self) -> [Storey]: + def thermal_zones(self) -> List[ThermalZone]: """ Get subsections of building trimesh by storey in case of no interiors defined :return: [Storey] @@ -37,7 +39,21 @@ class StoreysGeneration: self._building.storeys_above_ground) number_of_storeys = 1 if not self._divide_in_storeys or number_of_storeys == 1: - return [Storey('storey_0', self._building.surfaces, [None, None], self._building.volume, self._floor_area)] + storey = Storey('storey_0', self._building.surfaces, [None, None], self._building.volume, self._floor_area) + for thermal_boundary in storey.thermal_boundaries: + if thermal_boundary.type != cte.INTERIOR_WALL or thermal_boundary.type != cte.INTERIOR_SLAB: + # external thermal boundary -> only one thermal zone + thermal_zones = [storey.thermal_zone] + else: + # internal thermal boundary -> two thermal zones + grad = np.rad2deg(thermal_boundary.inclination) + if grad >= 170: + thermal_zones = [storey.thermal_zone, storey.neighbours[0]] + else: + thermal_zones = [storey.neighbours[1], storey.thermal_zone] + thermal_boundary.thermal_zones = thermal_zones + + return [storey.thermal_zone] if number_of_storeys == 0: raise Exception('Number of storeys cannot be 0') @@ -89,7 +105,25 @@ class StoreysGeneration: if volume < 0: raise Exception('Error in storeys creation, volume of last storey cannot be lower that 0') storeys.append(Storey(name, surfaces_child_last_storey, neighbours, volume, self._floor_area)) - return storeys + + for storey in storeys: + for thermal_boundary in storey.thermal_boundaries: + if thermal_boundary.type != cte.INTERIOR_WALL or thermal_boundary.type != cte.INTERIOR_SLAB: + # external thermal boundary -> only one thermal zone + thermal_zones = [storey.thermal_zone] + else: + # internal thermal boundary -> two thermal zones + grad = np.rad2deg(thermal_boundary.inclination) + if grad >= 170: + thermal_zones = [storey.thermal_zone, storey.neighbours[0]] + else: + thermal_zones = [storey.neighbours[1], storey.thermal_zone] + thermal_boundary.thermal_zones = thermal_zones + + for storey in storeys: + self._thermal_zones.append(storey.thermal_zone) + + return self._thermal_zones @staticmethod def _calculate_number_storeys_and_height(average_storey_height, eave_height, storeys_above_ground): @@ -139,22 +173,3 @@ class StoreysGeneration: for point in points: array_points.append(point.coordinates) return np.array(array_points) - - def assign_thermal_zones_delimited_by_thermal_boundaries(self): - """ - During storeys creation, the thermal boundaries and zones are also created. - It is afterwards needed to define which zones are delimited by each thermal boundary - """ - for storey in self._building.storeys: - for thermal_boundary in storey.thermal_boundaries: - if thermal_boundary.type != cte.INTERIOR_WALL or thermal_boundary.type != cte.INTERIOR_SLAB: - # external thermal boundary -> only one thermal zone - thermal_zones = [storey.thermal_zone] - else: - # internal thermal boundary -> two thermal zones - grad = np.rad2deg(thermal_boundary.inclination) - if grad >= 170: - thermal_zones = [storey.thermal_zone, storey.neighbours[0]] - else: - thermal_zones = [storey.neighbours[1], storey.thermal_zone] - thermal_boundary.thermal_zones = thermal_zones diff --git a/imports/construction/nrel_physics_interface.py b/imports/construction/nrel_physics_interface.py index 9065ca9e..6f08e536 100644 --- a/imports/construction/nrel_physics_interface.py +++ b/imports/construction/nrel_physics_interface.py @@ -10,6 +10,7 @@ from imports.construction.data_classes.building_achetype import BuildingArchetyp from imports.construction.data_classes.thermal_boundary_archetype import ThermalBoundaryArchetype as ntba from imports.construction.data_classes.thermal_opening_archetype import ThermalOpeningArchetype as ntoa from imports.construction.data_classes.layer_archetype import LayerArchetype as nla +from imports.construction.helpers.storeys_generation import StoreysGeneration class NrelPhysicsInterface: @@ -181,8 +182,57 @@ class NrelPhysicsInterface: return thermal_boundary raise Exception('Construction type not found') + # todo: verify windows + @staticmethod + def _calculate_view_factors(thermal_zone): + """ + Get thermal zone view factors matrix + :return: [[float]] + """ + total_area = 0 + for thermal_boundary in thermal_zone.thermal_boundaries: + total_area += thermal_boundary.opaque_area + for thermal_opening in thermal_boundary.thermal_openings: + total_area += thermal_opening.area + + view_factors_matrix = [] + for thermal_boundary_1 in thermal_zone.thermal_boundaries: + values = [] + for thermal_boundary_2 in thermal_zone.thermal_boundaries: + value = 0 + if thermal_boundary_1.id != thermal_boundary_2.id: + value = thermal_boundary_2.opaque_area / (total_area - thermal_boundary_1.opaque_area) + values.append(value) + for thermal_boundary in thermal_zone.thermal_boundaries: + for thermal_opening in thermal_boundary.thermal_openings: + value = thermal_opening.area / (total_area - thermal_boundary_1.opaque_area) + values.append(value) + view_factors_matrix.append(values) + + for thermal_boundary_1 in thermal_zone.thermal_boundaries: + values = [] + for thermal_opening_1 in thermal_boundary_1.thermal_openings: + for thermal_boundary_2 in thermal_zone.thermal_boundaries: + value = thermal_boundary_2.opaque_area / (total_area - thermal_opening_1.area) + values.append(value) + for thermal_boundary in thermal_zone.thermal_boundaries: + for thermal_opening_2 in thermal_boundary.thermal_openings: + value = 0 + if thermal_opening_1.id != thermal_opening_2.id: + value = thermal_opening_2.area / (total_area - thermal_opening_1.area) + values.append(value) + view_factors_matrix.append(values) + thermal_zone.view_factors_matrix = view_factors_matrix + def enrich_buildings(self): """ Raise not implemented error """ raise NotImplementedError + + @staticmethod + def _create_storeys(building, archetype): + building.average_storey_height = archetype.average_storey_height + building.storeys_above_ground = archetype.storeys_above_ground + thermal_zones = StoreysGeneration(building).thermal_zones + building.internal_zones[0].thermal_zones = thermal_zones diff --git a/imports/construction/us_physics_parameters.py b/imports/construction/us_physics_parameters.py index de79697b..7a23b318 100644 --- a/imports/construction/us_physics_parameters.py +++ b/imports/construction/us_physics_parameters.py @@ -10,7 +10,6 @@ from imports.construction.nrel_physics_interface import NrelPhysicsInterface from imports.construction.helpers.construction_helper import ConstructionHelper from city_model_structure.building_demand.layer import Layer from city_model_structure.building_demand.material import Material -from imports.construction.helpers.storeys_generation import StoreysGeneration class UsPhysicsParameters(NrelPhysicsInterface): @@ -30,7 +29,7 @@ class UsPhysicsParameters(NrelPhysicsInterface): city = self._city # it is assumed that all buildings have the same archetypes' keys for building in city.buildings: - building_type = ConstructionHelper.nrel_from_function(building.function) + building_type = ConstructionHelper.nrel_from_libs_function(building.function) if building_type is None: return try: @@ -46,12 +45,11 @@ class UsPhysicsParameters(NrelPhysicsInterface): if len(building.internal_zones) == 1: if building.internal_zones[0].thermal_zones is None: self._create_storeys(building, archetype) - thermal_zones = [] - for storey in building.storeys: - thermal_zones.append(storey.thermal_zone) - building.internal_zones[0].thermal_zones = thermal_zones self._assign_values(building.internal_zones, archetype) + for internal_zone in building.internal_zones: + for thermal_zone in internal_zone.thermal_zones: + self._calculate_view_factors(thermal_zone) def _search_archetype(self, building_type, standard, climate_zone): for building_archetype in self._building_archetypes: @@ -109,12 +107,3 @@ class UsPhysicsParameters(NrelPhysicsInterface): thermal_opening_archetype.back_side_solar_transmittance_at_normal_incidence thermal_opening.front_side_solar_transmittance_at_normal_incidence = \ thermal_opening_archetype.front_side_solar_transmittance_at_normal_incidence - - @staticmethod - def _create_storeys(building, archetype): - building.average_storey_height = archetype.average_storey_height - building.storeys_above_ground = archetype.storeys_above_ground - storeys_generation = StoreysGeneration(building) - storeys = storeys_generation.storeys - building.storeys = storeys - storeys_generation.assign_thermal_zones_delimited_by_thermal_boundaries() diff --git a/imports/customized_imports/helpers/sanam_customized_usage_helper.py b/imports/customized_imports/helpers/sanam_customized_usage_helper.py index a0d981b8..7bf73f65 100644 --- a/imports/customized_imports/helpers/sanam_customized_usage_helper.py +++ b/imports/customized_imports/helpers/sanam_customized_usage_helper.py @@ -14,7 +14,7 @@ class SanamCustomizedUsageHelper: usage_to_customized = { cte.RESIDENTIAL: 'residential', cte.INDUSTRY: 'manufacturing', - cte.OFFICE_ADMINISTRATION: 'office', + cte.OFFICE_AND_ADMINISTRATION: 'office', cte.HOTEL: 'hotel', cte.HEALTH_CARE: 'health', cte.RETAIL: 'retail', diff --git a/imports/customized_imports/sanam_customized_usage_parameters.py b/imports/customized_imports/sanam_customized_usage_parameters.py index 442ec9d3..ea3cac62 100644 --- a/imports/customized_imports/sanam_customized_usage_parameters.py +++ b/imports/customized_imports/sanam_customized_usage_parameters.py @@ -6,9 +6,11 @@ Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.mons import sys import xmltodict -from imports.geometry.helpers.geometry_helper import GeometryHelper as gh -from imports.usage.data_classes.usage_zone_archetype import UsageZoneArchetype as huza import helpers.constants as cte +from imports.usage.helpers.usage_helper import UsageHelper +from city_model_structure.building_demand.occupancy import Occupancy +from city_model_structure.building_demand.usage_zone import UsageZone +from imports.geometry.helpers.geometry_helper import GeometryHelper class SanamCustomizedUsageParameters: @@ -18,15 +20,10 @@ class SanamCustomizedUsageParameters: def __init__(self, city, base_path): file = 'ashrae_archetypes.xml' path = str(base_path / file) + self._city = city self._usage_archetypes = [] with open(path) as xml: self._archetypes = xmltodict.parse(xml.read(), force_list=('zoneUsageVariant', 'zoneUsageType')) - for zone_usage_type in self._archetypes['buildingUsageLibrary']['zoneUsageType']: - usage = zone_usage_type['id'] - usage_archetype = self._parse_zone_usage_type(usage, zone_usage_type) - self._usage_archetypes.append(usage_archetype) - - self._city = city def enrich_buildings(self): """ @@ -35,48 +32,59 @@ class SanamCustomizedUsageParameters: """ city = self._city for building in city.buildings: - archetype = self._search_archetype(building.function) # todo: building.function or other translation??????? - height = building.average_storey_height - if height is None: - raise Exception('Average storey height not defined, ACH cannot be calculated') - if height <= 0: - raise Exception('Average storey height is zero, ACH cannot be calculated') + libs_usage = GeometryHelper().libs_usage_from_libs_function(building.function) + comnet_usage = UsageHelper().comnet_from_libs_usage(libs_usage) + archetype = self._search_archetype(comnet_usage) if archetype is None: sys.stderr.write(f'Building {building.name} has unknown archetype for building function:' f' {building.function}, that assigns building usage as ' - f'{gh.usage_from_function(building.function)}\n') - continue - mix_usage = False - if not mix_usage: - # just one usage_zone - for usage_zone in building.usage_zones: - self._assign_values(usage_zone, archetype, height) + f'{libs_usage}\n') + return - def _search_archetype(self, building_usage): - for building_archetype in self._usage_archetypes: - if building_archetype.usage == building_usage: - return building_archetype + for internal_zone in building.internal_zones: + if internal_zone.area is None: + raise Exception('Internal zone area not defined, ACH cannot be calculated') + if internal_zone.volume is None: + raise Exception('Internal zone volume not defined, ACH cannot be calculated') + if internal_zone.area <= 0: + raise Exception('Internal zone area is zero, ACH cannot be calculated') + if internal_zone.volume <= 0: + raise Exception('Internal zone volume is zero, ACH cannot be calculated') + volume_per_area = internal_zone.volume / internal_zone.area + + usage_zone = UsageZone() + usage_zone.usage = libs_usage + self._assign_values(usage_zone, archetype, volume_per_area) + + def _search_archetype(self, libs_usage): + comnet_usage = UsageHelper.comnet_from_libs_usage(libs_usage) + for building_archetype in self._archetypes['buildingUsageLibrary']['zoneUsageType']: + if building_archetype['id'] == comnet_usage: + usage_archetype = self._parse_usage_type(self._archetypes) + return usage_archetype return None @staticmethod - def _assign_values(usage_zone, archetype, height): + def _assign_values(usage_zone, archetype, volume_per_area): usage_zone.usage = archetype.usage - # Due to the fact that python is not a typed language, the wrong object type is assigned to - # usage_zone.internal_gains when writing usage_zone.internal_gains = archetype.internal_gains. - # Therefore, this walk around has been done. - if archetype.occupancy_density is not None: - usage_zone.occupancy_density = archetype.occupancy_density - archetype_mechanical_air_change = float(archetype.mechanical_air_change) * float(usage_zone.occupancy_density) \ - * cte.HOUR_TO_MINUTES / cte.METERS_TO_FEET**3 / height - usage_zone.mechanical_air_change = archetype_mechanical_air_change + if archetype.occupancy.occupancy_density is not None: + if usage_zone.occupancy is None: + _occupancy = Occupancy() + usage_zone.occupancy = _occupancy + usage_zone.occupancy.occupancy_density = archetype.occupancy.occupancy_density + archetype_mechanical_air_change = float(archetype.mechanical_air_change) * \ + float(usage_zone.occupancy.occupancy_density) * cte.METERS_TO_FEET ** 2 \ + * cte.HOUR_TO_MINUTES / cte.METERS_TO_FEET ** 3 / volume_per_area + usage_zone.mechanical_air_change = archetype_mechanical_air_change @staticmethod - def _parse_zone_usage_type(usage, zone_usage_type): - mechanical_air_change = zone_usage_type['endUses']['ventilation']['minimumVentilationRate']['#text'] - if 'occupancy' in zone_usage_type: - occupancy_density = zone_usage_type['occupancy']['occupancyDensity']['#text'] - usage_zone_archetype = huza(usage=usage, occupancy_density=occupancy_density, - mechanical_air_change=mechanical_air_change) - else: - usage_zone_archetype = huza(usage=usage, mechanical_air_change=mechanical_air_change) + def _parse_usage_type(data): + usage_zone_archetype = UsageZone() + usage_zone_archetype.usage = data['id'] + usage_zone_archetype.mechanical_air_change = data['endUses']['ventilation']['minimumVentilationRate'][ + '#text'] + if 'occupancy' in data: + _occupancy = Occupancy() + _occupancy.occupancy_density = data['occupancy']['occupancyDensity']['#text'] + usage_zone_archetype.occupancy = _occupancy return usage_zone_archetype diff --git a/imports/customized_imports_factory.py b/imports/customized_imports_factory.py index bca8a87c..0a072f89 100644 --- a/imports/customized_imports_factory.py +++ b/imports/customized_imports_factory.py @@ -17,10 +17,6 @@ class CustomizedImportsFactory: self._importer_class = importer_class self._city = city self._base_path = base_path - for building in city.buildings: - if len(building.thermal_zones) == 0: - raise Exception('It seems that the customized imports factory is being called before the construction factory. ' - 'Please ensure that the construction factory is called first.') def enrich(self): """ diff --git a/imports/geometry/helpers/geometry_helper.py b/imports/geometry/helpers/geometry_helper.py index 104792d5..7307673a 100644 --- a/imports/geometry/helpers/geometry_helper.py +++ b/imports/geometry/helpers/geometry_helper.py @@ -13,20 +13,20 @@ class GeometryHelper: """ # function _pluto_to_function = { - 'A0': cte.SFH, - 'A1': cte.SFH, - 'A2': cte.SFH, - 'A3': cte.SFH, - 'A4': cte.SFH, - 'A5': cte.SFH, - 'A6': cte.SFH, - 'A7': cte.SFH, - 'A8': cte.SFH, - 'A9': cte.SFH, - 'B1': cte.MFH, - 'B2': cte.MFH, - 'B3': cte.MFH, - 'B9': cte.MFH, + 'A0': cte.SINGLE_FAMILY_HOUSE, + 'A1': cte.SINGLE_FAMILY_HOUSE, + 'A2': cte.SINGLE_FAMILY_HOUSE, + 'A3': cte.SINGLE_FAMILY_HOUSE, + 'A4': cte.SINGLE_FAMILY_HOUSE, + 'A5': cte.SINGLE_FAMILY_HOUSE, + 'A6': cte.SINGLE_FAMILY_HOUSE, + 'A7': cte.SINGLE_FAMILY_HOUSE, + 'A8': cte.SINGLE_FAMILY_HOUSE, + 'A9': cte.SINGLE_FAMILY_HOUSE, + 'B1': cte.MULTI_FAMILY_HOUSE, + 'B2': cte.MULTI_FAMILY_HOUSE, + 'B3': cte.MULTI_FAMILY_HOUSE, + 'B9': cte.MULTI_FAMILY_HOUSE, 'C0': cte.RESIDENTIAL, 'C1': cte.RESIDENTIAL, 'C2': cte.RESIDENTIAL, @@ -59,16 +59,16 @@ class GeometryHelper: 'F5': cte.WAREHOUSE, 'F8': cte.WAREHOUSE, 'F9': cte.WAREHOUSE, - 'G0': cte.OFFICE, - 'G1': cte.OFFICE, - 'G2': cte.OFFICE, - 'G3': cte.OFFICE, - 'G4': cte.OFFICE, - 'G5': cte.OFFICE, - 'G6': cte.OFFICE, - 'G7': cte.OFFICE, - 'G8': cte.OFFICE, - 'G9': cte.OFFICE, + 'G0': cte.SMALL_OFFICE, + 'G1': cte.SMALL_OFFICE, + 'G2': cte.SMALL_OFFICE, + 'G3': cte.SMALL_OFFICE, + 'G4': cte.SMALL_OFFICE, + 'G5': cte.SMALL_OFFICE, + 'G6': cte.SMALL_OFFICE, + 'G7': cte.SMALL_OFFICE, + 'G8': cte.SMALL_OFFICE, + 'G9': cte.SMALL_OFFICE, 'H1': cte.HOTEL, 'H2': cte.HOTEL, 'H3': cte.HOTEL, @@ -83,13 +83,13 @@ class GeometryHelper: 'HR': cte.HOTEL, 'HS': cte.HOTEL, 'I1': cte.HOSPITAL, - 'I2': cte.OUTPATIENT, - 'I3': cte.OUTPATIENT, + 'I2': cte.OUT_PATIENT_HEALTH_CARE, + 'I3': cte.OUT_PATIENT_HEALTH_CARE, 'I4': cte.RESIDENTIAL, - 'I5': cte.OUTPATIENT, - 'I6': cte.OUTPATIENT, - 'I7': cte.OUTPATIENT, - 'I9': cte.OUTPATIENT, + 'I5': cte.OUT_PATIENT_HEALTH_CARE, + 'I6': cte.OUT_PATIENT_HEALTH_CARE, + 'I7': cte.OUT_PATIENT_HEALTH_CARE, + 'I9': cte.OUT_PATIENT_HEALTH_CARE, 'J1': cte.LARGE_OFFICE, 'J2': cte.LARGE_OFFICE, 'J3': cte.LARGE_OFFICE, @@ -104,10 +104,10 @@ class GeometryHelper: 'K3': cte.STRIP_MALL, 'K4': cte.RESIDENTIAL, 'K5': cte.RESTAURANT, - 'K6': cte.COMMERCIAL, - 'K7': cte.COMMERCIAL, - 'K8': cte.COMMERCIAL, - 'K9': cte.COMMERCIAL, + 'K6': cte.SUPERMARKET, + 'K7': cte.SUPERMARKET, + 'K8': cte.SUPERMARKET, + 'K9': cte.SUPERMARKET, 'L1': cte.RESIDENTIAL, 'L2': cte.RESIDENTIAL, 'L3': cte.RESIDENTIAL, @@ -123,34 +123,34 @@ class GeometryHelper: 'N3': cte.RESIDENTIAL, 'N4': cte.RESIDENTIAL, 'N9': cte.RESIDENTIAL, - 'O1': cte.OFFICE, - 'O2': cte.OFFICE, - 'O3': cte.OFFICE, - 'O4': cte.OFFICE, - 'O5': cte.OFFICE, - 'O6': cte.OFFICE, - 'O7': cte.OFFICE, - 'O8': cte.OFFICE, - 'O9': cte.OFFICE, + 'O1': cte.SMALL_OFFICE, + 'O2': cte.SMALL_OFFICE, + 'O3': cte.SMALL_OFFICE, + 'O4': cte.SMALL_OFFICE, + 'O5': cte.SMALL_OFFICE, + 'O6': cte.SMALL_OFFICE, + 'O7': cte.SMALL_OFFICE, + 'O8': cte.SMALL_OFFICE, + 'O9': cte.SMALL_OFFICE, 'P1': cte.LARGE_OFFICE, 'P2': cte.HOTEL, - 'P3': cte.OFFICE, - 'P4': cte.OFFICE, - 'P5': cte.OFFICE, - 'P6': cte.OFFICE, + 'P3': cte.SMALL_OFFICE, + 'P4': cte.SMALL_OFFICE, + 'P5': cte.SMALL_OFFICE, + 'P6': cte.SMALL_OFFICE, 'P7': cte.LARGE_OFFICE, 'P8': cte.LARGE_OFFICE, - 'P9': cte.OFFICE, - 'Q0': cte.OFFICE, - 'Q1': cte.OFFICE, - 'Q2': cte.OFFICE, - 'Q3': cte.OFFICE, - 'Q4': cte.OFFICE, - 'Q5': cte.OFFICE, - 'Q6': cte.OFFICE, - 'Q7': cte.OFFICE, - 'Q8': cte.OFFICE, - 'Q9': cte.OFFICE, + 'P9': cte.SMALL_OFFICE, + 'Q0': cte.SMALL_OFFICE, + 'Q1': cte.SMALL_OFFICE, + 'Q2': cte.SMALL_OFFICE, + 'Q3': cte.SMALL_OFFICE, + 'Q4': cte.SMALL_OFFICE, + 'Q5': cte.SMALL_OFFICE, + 'Q6': cte.SMALL_OFFICE, + 'Q7': cte.SMALL_OFFICE, + 'Q8': cte.SMALL_OFFICE, + 'Q9': cte.SMALL_OFFICE, 'R0': cte.RESIDENTIAL, 'R1': cte.RESIDENTIAL, 'R2': cte.RESIDENTIAL, @@ -214,37 +214,47 @@ class GeometryHelper: } _hft_to_function = { 'residential': cte.RESIDENTIAL, - 'single family house': cte.SFH, - 'multifamily house': cte.MFH, + 'single family house': cte.SINGLE_FAMILY_HOUSE, + 'multifamily house': cte.MULTI_FAMILY_HOUSE, 'hotel': cte.HOTEL, 'hospital': cte.HOSPITAL, - 'outpatient': cte.OUTPATIENT, - 'commercial': cte.COMMERCIAL, + 'outpatient': cte.OUT_PATIENT_HEALTH_CARE, + 'commercial': cte.SUPERMARKET, 'strip mall': cte.STRIP_MALL, 'warehouse': cte.WAREHOUSE, 'primary school': cte.PRIMARY_SCHOOL, 'secondary school': cte.SECONDARY_SCHOOL, - 'office': cte.OFFICE, + 'office': cte.MEDIUM_OFFICE, 'large office': cte.LARGE_OFFICE } # usage _function_to_usage = { - cte.RESTAURANT: cte.RESTAURANT, cte.RESIDENTIAL: cte.RESIDENTIAL, - cte.HOSPITAL: cte.HEALTH_CARE, - cte.HOTEL: cte.HOTEL, - cte.LARGE_OFFICE: cte.OFFICE_ADMINISTRATION, - cte.OFFICE: cte.OFFICE_ADMINISTRATION, + cte.SINGLE_FAMILY_HOUSE: cte.SINGLE_FAMILY_HOUSE, + cte.MULTI_FAMILY_HOUSE: cte.MULTI_FAMILY_HOUSE, + cte.ROW_HOSE: cte.RESIDENTIAL, + cte.MID_RISE_APARTMENT: cte.RESIDENTIAL, + cte.HIGH_RISE_APARTMENT: cte.RESIDENTIAL, + cte.SMALL_OFFICE: cte.OFFICE_AND_ADMINISTRATION, + cte.MEDIUM_OFFICE: cte.OFFICE_AND_ADMINISTRATION, + cte.LARGE_OFFICE: cte.OFFICE_AND_ADMINISTRATION, cte.PRIMARY_SCHOOL: cte.EDUCATION, cte.SECONDARY_SCHOOL: cte.EDUCATION, - cte.RETAIL: cte.RETAIL, - cte.STRIP_MALL: cte.HALL, - cte.WAREHOUSE: cte.INDUSTRY + cte.STAND_ALONE_RETAIL: cte.RETAIL_SHOP_WITHOUT_REFRIGERATED_FOOD, + cte.HOSPITAL: cte.HEALTH_CARE, + cte.OUT_PATIENT_HEALTH_CARE: cte.HEALTH_CARE, + cte.STRIP_MALL: cte.RETAIL_SHOP_WITHOUT_REFRIGERATED_FOOD, + cte.SUPERMARKET: cte.RETAIL_SHOP_WITH_REFRIGERATED_FOOD, + cte.WAREHOUSE: cte.RETAIL_SHOP_WITHOUT_REFRIGERATED_FOOD, + cte.QUICK_SERVICE_RESTAURANT: cte.RESTAURANT, + cte.FULL_SERVICE_RESTAURANT: cte.RESTAURANT, + cte.SMALL_HOTEL: cte.HOTEL, + cte.LARGE_HOTEL: cte.HOTEL } @staticmethod - def function_from_hft(building_hft_function): + def libs_function_from_hft(building_hft_function): """ Get internal function from the given HfT function :param building_hft_function: str @@ -253,7 +263,7 @@ class GeometryHelper: return GeometryHelper._hft_to_function[building_hft_function] @staticmethod - def function_from_pluto(building_pluto_function): + def libs_function_from_pluto(building_pluto_function): """ Get internal function from the given pluto function :param building_pluto_function: str @@ -262,7 +272,7 @@ class GeometryHelper: return GeometryHelper._pluto_to_function[building_pluto_function] @staticmethod - def usage_from_function(building_function): + def libs_usage_from_libs_function(building_function): """ Get the internal usage for the given internal building function :param building_function: str diff --git a/imports/schedules/doe_idf.py b/imports/schedules/doe_idf.py index 1c9627de..61390199 100644 --- a/imports/schedules/doe_idf.py +++ b/imports/schedules/doe_idf.py @@ -10,6 +10,8 @@ import parseidf import xmltodict from imports.schedules.helpers.schedules_helper import SchedulesHelper from city_model_structure.attributes.schedule import Schedule +from city_model_structure.building_demand.occupancy import Occupancy +from city_model_structure.building_demand.lighting import Lighting import helpers.constants as cte @@ -51,15 +53,16 @@ class DoeIdf: self._schedule_library = xmltodict.parse(xml.read()) for building in self._city.buildings: - for usage_zone in building.usage_zones: - for schedule_archetype in self._schedule_library['archetypes']['archetypes']: - function = schedule_archetype['@building_type'] - if SchedulesHelper.usage_from_function(function) == usage_zone.usage: - self._idf_schedules_path = (base_path / schedule_archetype['idf']['path']).resolve() - with open(self._idf_schedules_path, 'r') as file: - idf = parseidf.parse(file.read()) - self._load_schedule(idf, usage_zone) - break + for internal_zone in building.internal_zones: + for usage_zone in internal_zone.usage_zones: + for schedule_archetype in self._schedule_library['archetypes']['archetypes']: + function = schedule_archetype['@building_type'] + if SchedulesHelper.usage_from_function(function) == usage_zone.usage: + self._idf_schedules_path = (base_path / schedule_archetype['idf']['path']).resolve() + with open(self._idf_schedules_path, 'r') as file: + idf = parseidf.parse(file.read()) + self._load_schedule(idf, usage_zone) + break def _load_schedule(self, idf, usage_zone): schedules_day = {} @@ -129,4 +132,12 @@ class DoeIdf: continue schedules.append(schedule) - usage_zone.schedules = schedules + for schedule in schedules: + if schedule.type == cte.OCCUPANCY: + if usage_zone.occupancy is None: + usage_zone.occupancy = Occupancy() + usage_zone.occupancy.occupancy_schedules = [schedule] + elif schedule.type == cte.LIGHTING: + if usage_zone.lighting is None: + usage_zone.lighting = Lighting() + usage_zone.lighting.schedules = [schedule] diff --git a/imports/schedules/helpers/schedules_helper.py b/imports/schedules/helpers/schedules_helper.py index 9aca6fab..4d4881a5 100644 --- a/imports/schedules/helpers/schedules_helper.py +++ b/imports/schedules/helpers/schedules_helper.py @@ -15,10 +15,10 @@ class SchedulesHelper: _usage_to_comnet = { cte.RESIDENTIAL: 'C-12 Residential', cte.INDUSTRY: 'C-10 Warehouse', - cte.OFFICE_ADMINISTRATION: 'C-5 Office', + cte.OFFICE_AND_ADMINISTRATION: 'C-5 Office', cte.HOTEL: 'C-3 Hotel', cte.HEALTH_CARE: 'C-2 Health', - cte.RETAIL: 'C-8 Retail', + cte.RETAIL_SHOP_WITHOUT_REFRIGERATED_FOOD: 'C-8 Retail', cte.HALL: 'C-8 Retail', cte.RESTAURANT: 'C-7 Restaurant', cte.EDUCATION: 'C-9 School' @@ -36,18 +36,18 @@ class SchedulesHelper: 'high-rise apartment': cte.RESIDENTIAL, 'hospital': cte.HEALTH_CARE, 'large hotel': cte.HOTEL, - 'large office': cte.OFFICE_ADMINISTRATION, - 'medium office': cte.OFFICE_ADMINISTRATION, + 'large office': cte.OFFICE_AND_ADMINISTRATION, + 'medium office': cte.OFFICE_AND_ADMINISTRATION, 'midrise apartment': cte.RESIDENTIAL, 'outpatient healthcare': cte.HEALTH_CARE, 'primary school': cte.EDUCATION, 'quick service restaurant': cte.RESTAURANT, 'secondary school': cte.EDUCATION, 'small hotel': cte.HOTEL, - 'small office': cte.OFFICE_ADMINISTRATION, - 'stand-alone-retail': cte.RETAIL, + 'small office': cte.OFFICE_AND_ADMINISTRATION, + 'stand-alone-retail': cte.RETAIL_SHOP_WITHOUT_REFRIGERATED_FOOD, 'strip mall': cte.HALL, - 'supermarket': cte.RETAIL, + 'supermarket': cte.RETAIL_SHOP_WITH_REFRIGERATED_FOOD, 'warehouse': cte.INDUSTRY, 'residential': cte.RESIDENTIAL } diff --git a/imports/schedules_factory.py b/imports/schedules_factory.py index 8437593f..3842889a 100644 --- a/imports/schedules_factory.py +++ b/imports/schedules_factory.py @@ -6,7 +6,6 @@ Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@conc """ from pathlib import Path -from imports.schedules.comnet_schedules_parameters import ComnetSchedules from imports.schedules.doe_idf import DoeIdf @@ -19,15 +18,11 @@ class SchedulesFactory: self._city = city self._base_path = base_path for building in city.buildings: - if len(building.usage_zones) == 0: - raise Exception('It seems that the schedule factory is being called before the usage factory. ' - 'Please ensure that the usage factory is called first.') - - def _comnet(self): - """ - Enrich the city by using COMNET schedules as data source - """ - ComnetSchedules(self._city, self._base_path) + for internal_zone in building.internal_zones: + if len(internal_zone.usage_zones) == 0: + raise Exception('It seems that the schedule factory is being called before the usage factory. ' + 'Please ensure that the usage factory is called first as the usage zones must be ' + 'firstly generated.') def _doe_idf(self): """ diff --git a/imports/usage/ca_usage_parameters.py b/imports/usage/ca_usage_parameters.py index c9034b85..b2320fa6 100644 --- a/imports/usage/ca_usage_parameters.py +++ b/imports/usage/ca_usage_parameters.py @@ -5,7 +5,9 @@ Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.mons """ import sys +from imports.geometry.helpers.geometry_helper import GeometryHelper from imports.usage.hft_usage_interface import HftUsageInterface +from imports.usage.helpers.usage_helper import UsageHelper from city_model_structure.building_demand.usage_zone import UsageZone from city_model_structure.building_demand.internal_gains import InternalGains from city_model_structure.building_demand.occupancy import Occupancy @@ -28,9 +30,9 @@ class CaUsageParameters(HftUsageInterface): """ city = self._city for building in city.buildings: + usage = GeometryHelper().libs_usage_from_libs_function(building.function) try: - print(building.function) - archetype = self._search_archetype(building.function) + archetype = self._search_archetype(usage) except KeyError: sys.stderr.write(f'Building {building.name} has unknown archetype for building function:' f' {building.function}\n') @@ -43,7 +45,8 @@ class CaUsageParameters(HftUsageInterface): self._assign_values_usage_zone(usage_zone, archetype) internal_zone.usage_zones = [usage_zone] - def _search_archetype(self, building_usage): + def _search_archetype(self, libs_usage): + building_usage = UsageHelper().hft_from_libs_usage(libs_usage) for building_archetype in self._usage_archetypes: if building_archetype.usage == building_usage: return building_archetype diff --git a/imports/usage/comnet_usage_parameters.py b/imports/usage/comnet_usage_parameters.py index 851473da..5d159af7 100644 --- a/imports/usage/comnet_usage_parameters.py +++ b/imports/usage/comnet_usage_parameters.py @@ -175,13 +175,13 @@ class ComnetUsageParameters: return _usage_zone - def _search_archetypes(self, usage): + def _search_archetypes(self, libs_usage): for item in self._data['lighting']: - comnet_usage = UsageHelper.comnet_from_usage(usage) + comnet_usage = UsageHelper.comnet_from_libs_usage(libs_usage) if comnet_usage == item: usage_archetype = self._parse_usage_type(comnet_usage, self._data, self._xls) return usage_archetype - return None, None + return None def enrich_buildings(self): """ @@ -190,13 +190,13 @@ class ComnetUsageParameters: """ city = self._city for building in city.buildings: - usage = GeometryHelper.usage_from_function(building.function) + usage = GeometryHelper.libs_usage_from_libs_function(building.function) try: archetype_usage = self._search_archetypes(usage) except KeyError: sys.stderr.write(f'Building {building.name} has unknown archetype for building function:' f' {building.function}, that assigns building usage as ' - f'{GeometryHelper.usage_from_function(building.function)}\n') + f'{GeometryHelper.libs_usage_from_libs_function(building.function)}\n') return for internal_zone in building.internal_zones: diff --git a/imports/usage/helpers/usage_helper.py b/imports/usage/helpers/usage_helper.py index f6257503..98f873da 100644 --- a/imports/usage/helpers/usage_helper.py +++ b/imports/usage/helpers/usage_helper.py @@ -13,17 +13,30 @@ class UsageHelper: """ _usage_to_hft = { cte.RESIDENTIAL: 'residential', - cte.INDUSTRY: 'industry', - cte.OFFICE_ADMINISTRATION: 'office and administration', + cte.SINGLE_FAMILY_HOUSE: 'Single family house', + cte.MULTI_FAMILY_HOUSE: 'Multi-family house', + cte.EDUCATION: 'education', + cte.SCHOOL_WITHOUT_SHOWER: 'school without shower', + cte.SCHOOL_WITH_SHOWER: 'school with shower', + cte.RETAIL_SHOP_WITHOUT_REFRIGERATED_FOOD: 'retail', + cte.RETAIL_SHOP_WITH_REFRIGERATED_FOOD: 'retail shop / refrigerated food', cte.HOTEL: 'hotel', - cte.HEALTH_CARE: 'health care', - cte.RETAIL: 'retail', - cte.HALL: 'hall', + cte.HOTEL_MEDIUM_CLASS: 'hotel (Medium-class)', + cte.DORMITORY: 'dormitory', + cte.INDUSTRY: 'industry', cte.RESTAURANT: 'restaurant', - cte.EDUCATION: 'education'} + cte.HEALTH_CARE: 'health care', + cte.RETIREMENT_HOME_OR_ORPHANAGE: 'Home for the aged or orphanage', + cte.OFFICE_AND_ADMINISTRATION: 'office and administration', + cte.EVENT_LOCATION: 'event location', + cte.HALL: 'hall', + cte.SPORTS_LOCATION: 'sport location', + cte.LABOR: 'Labor', + cte.GREEN_HOUSE: 'green house', + cte.NON_HEATED: 'non-heated'} @staticmethod - def hft_from_usage(usage): + def hft_from_libs_usage(usage): """ Get HfT usage from the given internal usage key :param usage: str @@ -32,18 +45,32 @@ class UsageHelper: try: return UsageHelper._usage_to_hft[usage] except KeyError: - sys.stderr.write('Error: keyword not found.\n') + sys.stderr.write('Error: keyword not found to translate from libs_usage to hft usage.\n') _usage_to_comnet = { cte.RESIDENTIAL: 'BA Multifamily', - cte.INDUSTRY: 'BA Manufacturing Facility', - cte.OFFICE_ADMINISTRATION: 'BA Office', + cte.SINGLE_FAMILY_HOUSE: 'BA Multifamily', + cte.MULTI_FAMILY_HOUSE: 'BA Multifamily', + cte.EDUCATION: 'BA School/University', + cte.SCHOOL_WITHOUT_SHOWER: 'BA School/University', + cte.SCHOOL_WITH_SHOWER: 'BA School/University', + cte.RETAIL_SHOP_WITHOUT_REFRIGERATED_FOOD: 'BA Retail', + cte.RETAIL_SHOP_WITH_REFRIGERATED_FOOD: 'BA Retail', cte.HOTEL: 'BA Hotel', + cte.HOTEL_MEDIUM_CLASS: 'BA Hotel', + cte.DORMITORY: 'BA Dormitory', + cte.INDUSTRY: 'BA Manufacturing Facility', + cte.RESTAURANT: 'BA Dining: Family', cte.HEALTH_CARE: 'BA Hospital', - cte.RETAIL: 'BA Retail', - cte.HALL: 'BA Town Hall', - cte.RESTAURANT: 'BA Dining: Bar Lounge/Leisure', - cte.EDUCATION: 'BA School/University'} + cte.RETIREMENT_HOME_OR_ORPHANAGE: 'BA Multifamily', + cte.OFFICE_AND_ADMINISTRATION: 'BA Office', + cte.EVENT_LOCATION: 'BA Convention Center', + cte.HALL: 'BA Convention Center', + cte.SPORTS_LOCATION: 'BA Sports Arena', + cte.LABOR: 'BA Gymnasium', + cte.GREEN_HOUSE: cte.GREEN_HOUSE, + cte.NON_HEATED: cte.NON_HEATED + } _comnet_schedules_key_to_comnet_schedules = { 'C-1 Assembly': 'C-1 Assembly', @@ -62,7 +89,7 @@ class UsageHelper: 'C-14 Gymnasium': 'C-14 Gymnasium'} @staticmethod - def comnet_from_usage(usage): + def comnet_from_libs_usage(usage): """ Get Comnet usage from the given internal usage key :param usage: str @@ -71,7 +98,7 @@ class UsageHelper: try: return UsageHelper._usage_to_comnet[usage] except KeyError: - sys.stderr.write('Error: keyword not found.\n') + sys.stderr.write('Error: keyword not found to translate from libs_usage to comnet usage.\n') @staticmethod def schedules_key(usage): diff --git a/imports/usage/hft_usage_interface.py b/imports/usage/hft_usage_interface.py index 340d8a80..959afbef 100644 --- a/imports/usage/hft_usage_interface.py +++ b/imports/usage/hft_usage_interface.py @@ -5,6 +5,7 @@ SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2022 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ import xmltodict +import copy from city_model_structure.building_demand.usage_zone import UsageZone from city_model_structure.building_demand.internal_gains import InternalGains from city_model_structure.building_demand.occupancy import Occupancy @@ -33,8 +34,6 @@ class HftUsageInterface: usage = usage_zone_variant['id'] usage_archetype_variant = self._parse_zone_usage_variant(usage, usage_archetype, usage_zone_variant) self._usage_archetypes.append(usage_archetype_variant) - for usage in self._usage_archetypes: - print(usage.usage) @staticmethod def _parse_zone_usage_type(usage, zone_usage_type): @@ -135,7 +134,7 @@ class HftUsageInterface: @staticmethod def _parse_zone_usage_variant(usage, usage_zone, usage_zone_variant): # the variants mimic the inheritance concept from OOP - usage_zone_archetype = usage_zone + usage_zone_archetype = copy.deepcopy(usage_zone) usage_zone_archetype.usage = usage if 'occupancy' in usage_zone_variant: diff --git a/imports/usage/hft_usage_parameters.py b/imports/usage/hft_usage_parameters.py index d95a2c3c..48a7b267 100644 --- a/imports/usage/hft_usage_parameters.py +++ b/imports/usage/hft_usage_parameters.py @@ -4,14 +4,12 @@ SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ import sys +import copy -from imports.geometry.helpers.geometry_helper import GeometryHelper as gh +from imports.geometry.helpers.geometry_helper import GeometryHelper from imports.usage.hft_usage_interface import HftUsageInterface +from imports.usage.helpers.usage_helper import UsageHelper from city_model_structure.building_demand.usage_zone import UsageZone -from city_model_structure.building_demand.internal_gains import InternalGains -from city_model_structure.building_demand.occupancy import Occupancy -from city_model_structure.building_demand.appliances import Appliances -from city_model_structure.building_demand.thermal_control import ThermalControl class HftUsageParameters(HftUsageInterface): @@ -29,23 +27,25 @@ class HftUsageParameters(HftUsageInterface): """ city = self._city for building in city.buildings: - usage = gh.usage_from_function(building.function) + usage = GeometryHelper().libs_usage_from_libs_function(building.function) try: archetype = self._search_archetype(usage) except KeyError: sys.stderr.write(f'Building {building.name} has unknown archetype for building function:' f' {building.function}, that assigns building usage as ' - f'{gh.usage_from_function(building.function)}\n') + f'{GeometryHelper().libs_usage_from_libs_function(building.function)}\n') return for internal_zone in building.internal_zones: usage_zone = UsageZone() - usage_zone.usage = building.function + libs_usage = GeometryHelper().libs_usage_from_libs_function(building.function) + usage_zone.usage = UsageHelper().hft_from_libs_usage(libs_usage) self._assign_values(usage_zone, archetype) usage_zone.percentage = 1 internal_zone.usage_zones = [usage_zone] - def _search_archetype(self, building_usage): + def _search_archetype(self, libs_usage): + building_usage = UsageHelper().hft_from_libs_usage(libs_usage) for building_archetype in self._usage_archetypes: if building_archetype.usage == building_usage: return building_archetype @@ -53,7 +53,7 @@ class HftUsageParameters(HftUsageInterface): @staticmethod def _assign_values(usage_zone, archetype): - # Due to the fact that python is not a typed language, the wrong object type is assigned to + """ # Due to the fact that python is not a typed language, the wrong object type is assigned to # usage_zone.internal_gains when writing usage_zone.internal_gains = archetype.internal_gains. # Therefore, this walk around has been done. # Due to the fact that python is not a typed language, the wrong object type is assigned to @@ -83,3 +83,12 @@ class HftUsageParameters(HftUsageInterface): _internal_gain.schedules = archetype_internal_gain.schedules _internal_gains.append(_internal_gain) usage_zone.not_detailed_source_mean_annual_internal_gains = _internal_gains + """ + usage_zone.mechanical_air_change = archetype.mechanical_air_change + usage_zone.occupancy = copy.deepcopy(archetype.occupancy) + usage_zone.appliances = copy.deepcopy(archetype.appliances) + usage_zone.thermal_control = copy.deepcopy(archetype.thermal_control) + usage_zone.not_detailed_source_mean_annual_internal_gains = \ + copy.deepcopy(archetype.not_detailed_source_mean_annual_internal_gains) + usage_zone.days_year = archetype.days_year + usage_zone.hours_day = archetype.hours_day diff --git a/recognized_functions_and_usages.md b/recognized_functions_and_usages.md new file mode 100644 index 00000000..fad74352 --- /dev/null +++ b/recognized_functions_and_usages.md @@ -0,0 +1,64 @@ +# Functions and usages internally recognized within the libs + +The libs uses a list of building functions a building usages that are the only ones recognized. All new categories should be added to the dicctionaries that translate from the input formats to the libs functions. From the libs functions to the libs usages and from the libs usages and libs functions to the output formats. + +Input formats accepted: +* Function: + * pluto + * hft + +Output formats accepted: +* Function: + * nrel + * nrcan +* Usage: + * ca + * hft + * comnet + +Libs_functions: +* single family house +* multi family house +* row hose +* mid rise apartment +* high rise apartment +* residential +* small office +* medium office +* large office +* primary school +* secondary school +* stand alone retail +* hospital +* out-patient health care +* strip mall +* supermarket +* ware house +* quick service restaurant +* full service restaurant +* small hotel +* large hotel + +Libs_usage: +* residential +* single family house +* multi family house +* education +* school without shower +* school with shower +* retail shop without refrigerated food +* retail shop with refrigerated food +* hotel +* hotel medium class +* dormitory +* industry +* restaurant +* health care +* retirement home or orphanage +* office and administration +* event location +* hall +* sports location +* labor +* green-house +* non-heated diff --git a/unittests/test_construction_factory.py b/unittests/test_construction_factory.py index df7e1edd..9348e8d3 100644 --- a/unittests/test_construction_factory.py +++ b/unittests/test_construction_factory.py @@ -64,7 +64,6 @@ class TestConstructionFactory(TestCase): self.assertEqual(len(building.heating), 0, 'building heating is not none') self.assertEqual(len(building.cooling), 0, 'building cooling is not none') self.assertIsNotNone(building.eave_height, 'building eave height is none') - self.assertIsNotNone(building.storeys, 'building storeys are not defined') self.assertIsNotNone(building.roof_type, 'building roof type is none') self.assertIsNotNone(building.floor_area, 'building floor_area is none') self.assertIsNone(building.households, 'building households is not none') @@ -93,7 +92,7 @@ class TestConstructionFactory(TestCase): for thermal_boundary in thermal_zone.thermal_boundaries: self.assertIsNotNone(thermal_boundary.id, 'thermal_boundary id is none') self.assertIsNotNone(thermal_boundary.parent_surface, 'thermal_boundary surface is none') - self.assertIsNotNone(thermal_boundary.thermal_zones, 'thermal_boundary delimits is none') + self.assertIsNotNone(thermal_boundary.thermal_zones, 'thermal_boundary delimits no thermal zone') self.assertIsNotNone(thermal_boundary.opaque_area, 'thermal_boundary area is none') self.assertIsNotNone(thermal_boundary.azimuth, 'thermal_boundary azimuth is none') self.assertIsNotNone(thermal_boundary.inclination, 'thermal_boundary inclination is none') @@ -166,7 +165,7 @@ class TestConstructionFactory(TestCase): file = 'one_building_in_kelowna.gml' city = self._get_citygml(file) for building in city.buildings: - building.function = GeometryHelper.function_from_hft(building.function) + building.function = GeometryHelper.libs_function_from_hft(building.function) ConstructionFactory('nrcan', city).enrich() self._check_buildings(city) @@ -196,7 +195,7 @@ class TestConstructionFactory(TestCase): file = 'pluto_building.gml' city = self._get_citygml(file) for building in city.buildings: - building.function = GeometryHelper.function_from_pluto(building.function) + building.function = GeometryHelper.libs_function_from_pluto(building.function) ConstructionFactory('nrel', city).enrich() self._check_buildings(city) @@ -226,9 +225,9 @@ class TestConstructionFactory(TestCase): @staticmethod def _internal_function(function_format, original_function): if function_format == 'hft': - new_function = GeometryHelper.function_from_hft(original_function) + new_function = GeometryHelper.libs_function_from_hft(original_function) elif function_format == 'pluto': - new_function = GeometryHelper.function_from_pluto(original_function) + new_function = GeometryHelper.libs_function_from_pluto(original_function) else: raise Exception('Function key not recognized. Implemented only "hft" and "pluto"') return new_function diff --git a/unittests/test_customized_imports_factory.py b/unittests/test_customized_imports_factory.py index 80373480..245a8821 100644 --- a/unittests/test_customized_imports_factory.py +++ b/unittests/test_customized_imports_factory.py @@ -6,8 +6,8 @@ Copyright © 2021 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.mons from pathlib import Path from unittest import TestCase +import helpers.constants as cte from imports.geometry_factory import GeometryFactory -from imports.construction_factory import ConstructionFactory from imports.usage_factory import UsageFactory from imports.customized_imports_factory import CustomizedImportsFactory from imports.customized_imports.sanam_customized_usage_parameters import SanamCustomizedUsageParameters as scp @@ -28,7 +28,6 @@ class TestCustomizedImportsFactory(TestCase): file_path = (self._example_path / file).resolve() _city = GeometryFactory('citygml', file_path).city self.assertIsNotNone(_city, 'city is none') - ConstructionFactory('nrel', _city).enrich() UsageFactory('hft', _city).enrich() return _city @@ -44,6 +43,9 @@ class TestCustomizedImportsFactory(TestCase): CustomizedImportsFactory(scp, city).enrich() for building in city.buildings: - self.assertIsNot(len(building.usage_zones), 0, 'no building usage_zones defined') - for usage_zone in building.usage_zones: - self.assertIsNotNone(usage_zone.mechanical_air_change, 'usage is none') + self.assertIsNot(len(building.internal_zones), 0, 'no building internal_zones defined') + for internal_zone in building.internal_zones: + for usage_zone in internal_zone.usage_zones: + if usage_zone.usage != cte.RESIDENTIAL: + self.assertIsNotNone(usage_zone.mechanical_air_change, 'mechanical air change rate is none') + self.assertIsNotNone(usage_zone.occupancy.occupancy_density, 'occupancy density us none') diff --git a/unittests/test_doe_idf.py b/unittests/test_doe_idf.py index 76ce8440..c75e6372 100644 --- a/unittests/test_doe_idf.py +++ b/unittests/test_doe_idf.py @@ -34,15 +34,22 @@ class TestBuildings(TestCase): ExportsFactory('idf', city, output_path).export() self.assertEqual(10, len(city.buildings)) for building in city.buildings: - self.assertTrue(len(building.usage_zones) > 0) - for usage_zone in building.usage_zones: - self.assertIsNot(len(usage_zone.schedules), 0, 'no usage_zones schedules defined') - for schedule in usage_zone.schedules: - self.assertIsNotNone(schedule.type) - self.assertIsNotNone(schedule.values) - self.assertIsNotNone(schedule.data_type) - self.assertIsNotNone(schedule.time_step) - self.assertIsNotNone(schedule.time_range) - self.assertIsNotNone(schedule.day_types) - - + for internal_zone in building.internal_zones: + self.assertTrue(len(internal_zone.usage_zones) > 0) + for usage_zone in internal_zone.usage_zones: + self.assertIsNot(len(usage_zone.occupancy.occupancy_schedules), 0, 'no occupancy schedules defined') + for schedule in usage_zone.occupancy.occupancy_schedules: + self.assertIsNotNone(schedule.type) + self.assertIsNotNone(schedule.values) + self.assertIsNotNone(schedule.data_type) + self.assertIsNotNone(schedule.time_step) + self.assertIsNotNone(schedule.time_range) + self.assertIsNotNone(schedule.day_types) + self.assertIsNot(len(usage_zone.lighting.schedules), 0, 'no lighting schedules defined') + for schedule in usage_zone.lighting.schedules: + self.assertIsNotNone(schedule.type) + self.assertIsNotNone(schedule.values) + self.assertIsNotNone(schedule.data_type) + self.assertIsNotNone(schedule.time_step) + self.assertIsNotNone(schedule.time_range) + self.assertIsNotNone(schedule.day_types) diff --git a/unittests/test_enrichement.py b/unittests/test_enrichement.py index 7c5861d5..95861429 100644 --- a/unittests/test_enrichement.py +++ b/unittests/test_enrichement.py @@ -54,10 +54,10 @@ class TestGeometryFactory(TestCase): def _prepare_case_usage_first(city, input_key, construction_key, usage_key): if input_key == 'pluto': for building in city.buildings: - building.function = GeometryHelper.function_from_pluto(building.function) + building.function = GeometryHelper.libs_function_from_pluto(building.function) elif input_key == 'hft': for building in city.buildings: - building.function = GeometryHelper.function_from_hft(building.function) + building.function = GeometryHelper.libs_function_from_hft(building.function) UsageFactory(usage_key, city).enrich() ConstructionFactory(construction_key, city).enrich() @@ -65,10 +65,10 @@ class TestGeometryFactory(TestCase): def _prepare_case_construction_first(city, input_key, construction_key, usage_key): if input_key == 'pluto': for building in city.buildings: - building.function = GeometryHelper.function_from_pluto(building.function) + building.function = GeometryHelper.libs_function_from_pluto(building.function) elif input_key == 'hft': for building in city.buildings: - building.function = GeometryHelper.function_from_hft(building.function) + building.function = GeometryHelper.libs_function_from_hft(building.function) ConstructionFactory(construction_key, city).enrich() UsageFactory(usage_key, city).enrich() @@ -112,34 +112,52 @@ class TestGeometryFactory(TestCase): self._check_thermal_zones(thermal_zone) for construction_key in _construction_keys: - for usage_key in _usage_keys: - if usage_key != 'ca': - city = self._get_citygml(file_2) - self.assertTrue(len(city.buildings) == 1) - self._prepare_case_construction_first(city, 'pluto', construction_key, usage_key) - self._check_buildings(city) - for building in city.buildings: - for internal_zone in building.internal_zones: - self.assertIsNot(len(internal_zone.usage_zones), 0, 'no building usage_zones defined') - for usage_zone in internal_zone.usage_zones: - self._check_usage_zone(usage_zone) - for thermal_zone in internal_zone.thermal_zones: - self._check_thermal_zones(thermal_zone) + if construction_key != 'nrcan': + for usage_key in _usage_keys: + if usage_key != 'ca': + city = self._get_citygml(file_2) + self.assertTrue(len(city.buildings) == 1) + self._prepare_case_construction_first(city, 'pluto', construction_key, usage_key) + self._check_buildings(city) + for building in city.buildings: + for internal_zone in building.internal_zones: + self.assertIsNot(len(internal_zone.usage_zones), 0, 'no building usage_zones defined') + for usage_zone in internal_zone.usage_zones: + self._check_usage_zone(usage_zone) + for thermal_zone in internal_zone.thermal_zones: + self._check_thermal_zones(thermal_zone) for construction_key in _construction_keys: - for usage_key in _usage_keys: - if usage_key != 'ca': - city = self._get_citygml(file_2) - self.assertTrue(len(city.buildings) == 1) - self._prepare_case_usage_first(city, 'pluto', construction_key, usage_key) - self._check_buildings(city) - for building in city.buildings: - for internal_zone in building.internal_zones: - self.assertIsNot(len(internal_zone.usage_zones), 0, 'no building usage_zones defined') - for usage_zone in internal_zone.usage_zones: - self._check_usage_zone(usage_zone) - for thermal_zone in internal_zone.thermal_zones: - self._check_thermal_zones(thermal_zone) + if construction_key != 'nrcan': + for usage_key in _usage_keys: + if usage_key != 'ca': + city = self._get_citygml(file_2) + self.assertTrue(len(city.buildings) == 1) + self._prepare_case_usage_first(city, 'pluto', construction_key, usage_key) + self._check_buildings(city) + for building in city.buildings: + for internal_zone in building.internal_zones: + self.assertIsNot(len(internal_zone.usage_zones), 0, 'no building usage_zones defined') + for usage_zone in internal_zone.usage_zones: + self._check_usage_zone(usage_zone) + for thermal_zone in internal_zone.thermal_zones: + self._check_thermal_zones(thermal_zone) city = self._get_citygml(file_3) self.assertTrue(len(city.buildings) == 10) + + for construction_key in _construction_keys: + if construction_key != 'nrcan': + for usage_key in _usage_keys: + if usage_key != 'ca': + city = self._get_citygml(file_2) + self.assertTrue(len(city.buildings) == 1) + self._prepare_case_construction_first(city, 'pluto', construction_key, usage_key) + self._check_buildings(city) + for building in city.buildings: + for internal_zone in building.internal_zones: + self.assertIsNot(len(internal_zone.usage_zones), 0, 'no building usage_zones defined') + for usage_zone in internal_zone.usage_zones: + self._check_usage_zone(usage_zone) + for thermal_zone in internal_zone.thermal_zones: + self._check_thermal_zones(thermal_zone) diff --git a/unittests/test_exports.py b/unittests/test_exports.py index b4c38be1..20900b6f 100644 --- a/unittests/test_exports.py +++ b/unittests/test_exports.py @@ -9,8 +9,8 @@ from pathlib import Path from unittest import TestCase import pandas as pd from imports.geometry_factory import GeometryFactory +from imports.geometry.helpers.geometry_helper import GeometryHelper from imports.construction_factory import ConstructionFactory -from imports.schedules_factory import SchedulesFactory from imports.usage_factory import UsageFactory from exports.exports_factory import ExportsFactory import helpers.constants as cte @@ -45,9 +45,10 @@ class TestExports(TestCase): else: file_path = (self._example_path / 'one_building_in_kelowna.gml').resolve() self._complete_city = self._get_citygml(file_path) + for building in self._complete_city.buildings: + building.function = GeometryHelper().libs_function_from_hft(building.function) ConstructionFactory('nrel', self._complete_city).enrich() UsageFactory('ca', self._complete_city).enrich() - SchedulesFactory('comnet', self._complete_city).enrich() cli = 'C:\\Users\\Pilar\\PycharmProjects\\monthlyenergybalance\\tests_data\\weather\\inseldb_Summerland.cli' self._complete_city.climate_file = Path(cli) self._complete_city.climate_reference_city = 'Summerland' diff --git a/unittests/test_geometry_factory.py b/unittests/test_geometry_factory.py index db5ae4bb..d3fc1473 100644 --- a/unittests/test_geometry_factory.py +++ b/unittests/test_geometry_factory.py @@ -55,8 +55,10 @@ class TestGeometryFactory(TestCase): self.assertIsNotNone(building.grounds, 'building grounds is none') self.assertIsNotNone(building.walls, 'building walls is none') self.assertIsNotNone(building.roofs, 'building roofs is none') - self.assertIsNone(building.usage_zones, 'usage zones are defined') - self.assertIsNone(building.thermal_zones, 'thermal zones are defined') + self.assertIsNotNone(building.internal_zones, 'building internal zones is none') + for internal_zone in building.internal_zones: + self.assertIsNone(internal_zone.usage_zones, 'usage zones are defined') + self.assertIsNone(internal_zone.thermal_zones, 'thermal zones are defined') self.assertIsNone(building.basement_heated, 'building basement_heated is not none') self.assertIsNone(building.attic_heated, 'building attic_heated is not none') self.assertIsNone(building.terrains, 'building terrains is not none') @@ -67,7 +69,6 @@ class TestGeometryFactory(TestCase): self.assertEqual(len(building.heating), 0, 'building heating is not none') self.assertEqual(len(building.cooling), 0, 'building cooling is not none') self.assertIsNotNone(building.eave_height, 'building eave height is none') - self.assertIsNone(building.storeys, 'building storeys are defined') self.assertIsNotNone(building.roof_type, 'building roof type is none') self.assertIsNotNone(building.floor_area, 'building floor_area is none') self.assertIsNone(building.households, 'building households is not none') diff --git a/unittests/test_schedules_factory.py b/unittests/test_schedules_factory.py index 0506abae..f59f3cf6 100644 --- a/unittests/test_schedules_factory.py +++ b/unittests/test_schedules_factory.py @@ -31,30 +31,10 @@ class TestSchedulesFactory(TestCase): ConstructionFactory('nrel', _city).enrich() self.assertIsNotNone(_city, 'city is none') for building in _city.buildings: - building.function = GeometryHelper.hft_to_function[building.function] + building.function = GeometryHelper.libs_function_from_hft(building.function) UsageFactory('hft', _city).enrich() return _city - def test_comnet_archetypes(self): - """ - Enrich the city with commet schedule archetypes and verify it - """ - file = (self._example_path / 'one_building_in_kelowna.gml').resolve() - city = self._get_citygml(file) - occupancy_handler = 'comnet' - SchedulesFactory(occupancy_handler, city).enrich() - for building in city.buildings: - self.assertIsNot(len(building.usage_zones), 0, 'no building usage_zones defined') - for usage_zone in building.usage_zones: - self.assertIsNot(len(usage_zone.schedules), 0, 'no usage_zones schedules defined') - for schedule in usage_zone.schedules: - self.assertIsNotNone(schedule.type) - self.assertIsNotNone(schedule.values) - self.assertIsNotNone(schedule.data_type) - self.assertIsNotNone(schedule.time_step) - self.assertIsNotNone(schedule.time_range) - self.assertIsNotNone(schedule.day_types) - def test_doe_idf_archetypes(self): """ Enrich the city with doe_idf schedule archetypes and verify it @@ -64,7 +44,22 @@ class TestSchedulesFactory(TestCase): occupancy_handler = 'doe_idf' SchedulesFactory(occupancy_handler, city).enrich() for building in city.buildings: - for usage_zone in building.usage_zones: - for schedule in usage_zone.schedules: - print(schedule) - print(usage_zone.schedules[schedule]) + for internal_zone in building.internal_zones: + self.assertTrue(len(internal_zone.usage_zones) > 0) + for usage_zone in internal_zone.usage_zones: + self.assertIsNot(len(usage_zone.occupancy.occupancy_schedules), 0, 'no occupancy schedules defined') + for schedule in usage_zone.occupancy.occupancy_schedules: + self.assertIsNotNone(schedule.type) + self.assertIsNotNone(schedule.values) + self.assertIsNotNone(schedule.data_type) + self.assertIsNotNone(schedule.time_step) + self.assertIsNotNone(schedule.time_range) + self.assertIsNotNone(schedule.day_types) + self.assertIsNot(len(usage_zone.lighting.schedules), 0, 'no lighting schedules defined') + for schedule in usage_zone.lighting.schedules: + self.assertIsNotNone(schedule.type) + self.assertIsNotNone(schedule.values) + self.assertIsNotNone(schedule.data_type) + self.assertIsNotNone(schedule.time_step) + self.assertIsNotNone(schedule.time_range) + self.assertIsNotNone(schedule.day_types) diff --git a/unittests/test_usage_factory.py b/unittests/test_usage_factory.py index 948cf4e4..269457be 100644 --- a/unittests/test_usage_factory.py +++ b/unittests/test_usage_factory.py @@ -53,7 +53,7 @@ class TestUsageFactory(TestCase): self.assertIsNotNone(building.roofs, 'building roofs is none') for internal_zone in building.internal_zones: self.assertTrue(len(internal_zone.usage_zones) > 0, 'usage zones are not defined') - self.assertIsNone(building.thermal_zones, 'thermal zones are defined') + self.assertIsNone(internal_zone.thermal_zones, 'thermal zones are defined') self.assertIsNone(building.basement_heated, 'building basement_heated is not none') self.assertIsNone(building.attic_heated, 'building attic_heated is not none') self.assertIsNone(building.terrains, 'building terrains is not none') @@ -64,7 +64,6 @@ class TestUsageFactory(TestCase): self.assertEqual(len(building.heating), 0, 'building heating is not none') self.assertEqual(len(building.cooling), 0, 'building cooling is not none') self.assertIsNotNone(building.eave_height, 'building eave height is none') - self.assertIsNone(building.storeys, 'building storeys are defined') self.assertIsNotNone(building.roof_type, 'building roof type is none') self.assertIsNotNone(building.floor_area, 'building floor_area is none') self.assertIsNone(building.households, 'building households is not none') @@ -76,7 +75,6 @@ class TestUsageFactory(TestCase): self.assertIsNotNone(usage_zone.get_internal_gains, 'internal gains is none') self.assertIsNotNone(usage_zone.hours_day, 'hours per day is none') self.assertIsNotNone(usage_zone.days_year, 'days per year is none') - self.assertIsNotNone(usage_zone.mechanical_air_change, 'mechanical air change is none') self.assertIsNotNone(usage_zone.thermal_control, 'thermal control is none') self.assertIsNotNone(usage_zone.thermal_control.mean_heating_set_point, 'control heating set point is none') self.assertIsNotNone(usage_zone.thermal_control.heating_set_back, 'control heating set back is none') @@ -89,7 +87,7 @@ class TestUsageFactory(TestCase): file = 'pluto_building.gml' city = self._get_citygml(file) for building in city.buildings: - building.function = GeometryHelper.function_from_pluto(building.function) + building.function = GeometryHelper.libs_function_from_pluto(building.function) UsageFactory('comnet', city).enrich() self._check_buildings(city) @@ -98,6 +96,7 @@ class TestUsageFactory(TestCase): self.assertIsNot(len(internal_zone.usage_zones), 0, 'no building usage_zones defined') for usage_zone in internal_zone.usage_zones: self._check_usage_zone(usage_zone) + self.assertIsNotNone(usage_zone.mechanical_air_change, 'mechanical air change is none') self.assertIsNotNone(usage_zone.thermal_control.heating_set_point_schedules, 'control heating set point schedule is none') self.assertIsNotNone(usage_zone.thermal_control.cooling_set_point_schedules, @@ -142,6 +141,7 @@ class TestUsageFactory(TestCase): self.assertIsNot(len(internal_zone.usage_zones), 0, 'no building usage_zones defined') for usage_zone in internal_zone.usage_zones: self._check_usage_zone(usage_zone) + self.assertIsNotNone(usage_zone.mechanical_air_change, 'mechanical air change is none') self.assertIsNotNone(usage_zone.not_detailed_source_mean_annual_internal_gains, 'not detailed internal gains is none') @@ -152,7 +152,7 @@ class TestUsageFactory(TestCase): file = 'pluto_building.gml' city = self._get_citygml(file) for building in city.buildings: - building.function = GeometryHelper.function_from_pluto(building.function) + building.function = GeometryHelper.libs_function_from_pluto(building.function) UsageFactory('hft', city).enrich() self._check_buildings(city) @@ -161,6 +161,7 @@ class TestUsageFactory(TestCase): self.assertIsNot(len(internal_zone.usage_zones), 0, 'no building usage_zones defined') for usage_zone in internal_zone.usage_zones: self._check_usage_zone(usage_zone) + self.assertIsNone(usage_zone.mechanical_air_change, 'mechanical air change is not none') self.assertIsNotNone(usage_zone.thermal_control.heating_set_point_schedules, 'control heating set point schedule is none') self.assertIsNotNone(usage_zone.thermal_control.cooling_set_point_schedules, @@ -168,24 +169,13 @@ class TestUsageFactory(TestCase): self.assertIsNotNone(usage_zone.occupancy, 'occupancy is none') occupancy = usage_zone.occupancy self.assertIsNotNone(occupancy.occupancy_density, 'occupancy density is none') - self.assertIsNotNone(occupancy.latent_internal_gain, 'occupancy latent internal gain is none') - self.assertIsNotNone(occupancy.sensible_convective_internal_gain, - 'occupancy sensible convective internal gain is none') - self.assertIsNotNone(occupancy.sensible_radiative_internal_gain, - 'occupancy sensible radiant internal gain is none') - self.assertIsNotNone(occupancy.occupancy_schedules, 'occupancy schedule is none') + self.assertIsNone(occupancy.latent_internal_gain, 'occupancy latent internal gain is none') + self.assertIsNone(occupancy.sensible_convective_internal_gain, + 'occupancy sensible convective internal gain is not none') + self.assertIsNone(occupancy.sensible_radiative_internal_gain, + 'occupancy sensible radiant internal gain is not none') + self.assertIsNone(occupancy.occupancy_schedules, 'occupancy schedule is not none') self.assertIsNone(occupancy.occupants, 'occupancy density is not none') - self.assertIsNotNone(usage_zone.lighting, 'lighting is none') - lighting = usage_zone.lighting - self.assertIsNotNone(lighting.lighting_density, 'lighting density is none') - self.assertIsNotNone(lighting.latent_fraction, 'lighting latent fraction is none') - self.assertIsNotNone(lighting.convective_fraction, 'lighting convective fraction is none') - self.assertIsNotNone(lighting.radiative_fraction, 'lighting radiant fraction is none') - self.assertIsNotNone(lighting.schedules, 'lighting schedule is none') - self.assertIsNotNone(usage_zone.appliances, 'appliances is none') - appliances = usage_zone.appliances - self.assertIsNotNone(appliances.appliances_density, 'appliances density is none') - self.assertIsNotNone(appliances.latent_fraction, 'appliances latent fraction is none') - self.assertIsNotNone(appliances.convective_fraction, 'appliances convective fraction is none') - self.assertIsNotNone(appliances.radiative_fraction, 'appliances radiant fraction is none') - self.assertIsNotNone(appliances.schedules, 'appliances schedule is none') + self.assertIsNone(usage_zone.lighting, 'lighting is not none') + self.assertIsNone(usage_zone.appliances, 'appliances is not none') + From 2bc217773ec481ac64d91a6cf74686685b391186 Mon Sep 17 00:00:00 2001 From: guille Date: Thu, 24 Mar 2022 19:06:47 -0400 Subject: [PATCH 12/19] Correct merge error add deleted files --- .../ecore_greenery/greenerycatalog.ecore | 268 +++++++++++++++ .../ecore_greenery/greenerycatalog.py | 318 ++++++++++++++++++ .../greenerycatalog_no_quantities.ecore | 268 +++++++++++++++ city_model_structure/iot/sensor_measure.py | 40 +++ city_model_structure/iot/sensor_type.py | 20 ++ city_model_structure/iot/station.py | 41 +++ city_model_structure/lca_material.py | 242 +++++++++++++ city_model_structure/material.py | 118 ------- data/greenery/ecore_greenery_catalog.xml | 59 ++++ 9 files changed, 1256 insertions(+), 118 deletions(-) create mode 100644 catalogs/greenery/ecore_greenery/greenerycatalog.ecore create mode 100644 catalogs/greenery/ecore_greenery/greenerycatalog.py create mode 100644 catalogs/greenery/ecore_greenery/greenerycatalog_no_quantities.ecore create mode 100644 city_model_structure/iot/sensor_measure.py create mode 100644 city_model_structure/iot/sensor_type.py create mode 100644 city_model_structure/iot/station.py create mode 100644 city_model_structure/lca_material.py delete mode 100644 city_model_structure/material.py create mode 100644 data/greenery/ecore_greenery_catalog.xml diff --git a/catalogs/greenery/ecore_greenery/greenerycatalog.ecore b/catalogs/greenery/ecore_greenery/greenerycatalog.ecore new file mode 100644 index 00000000..5c3bb85d --- /dev/null +++ b/catalogs/greenery/ecore_greenery/greenerycatalog.ecore @@ -0,0 +1,268 @@ + + + + +

+ + + + +
+ + + + +
+ + +
+
+ + + + +
+ + +
+
+ + + + +
+ + +
+
+ + + + +
+ + +
+
+ + + + +
+ + +
+
+ + + + +
+ + +
+
+ + + + +
+ + +
+
+ + + + +
+ + +
+
+ + + + +
+ + +
+
+ + + + + + + +
+ + +
+
+ + + + +
+ + +
+
+ + + + +
+ + +
+
+ + + + +
+ + +
+
+ + + + +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ + + + +
+
+ + + + + + + + + +
+ + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + diff --git a/catalogs/greenery/ecore_greenery/greenerycatalog.py b/catalogs/greenery/ecore_greenery/greenerycatalog.py new file mode 100644 index 00000000..30ac116c --- /dev/null +++ b/catalogs/greenery/ecore_greenery/greenerycatalog.py @@ -0,0 +1,318 @@ +"""Definition of meta model 'greenerycatalog'.""" +from functools import partial +import pyecore.ecore as Ecore +from pyecore.ecore import * + + +name = 'greenerycatalog' +nsURI = 'http://ca.concordia/greenerycatalog' +nsPrefix = 'greenery' + +eClass = EPackage(name=name, nsURI=nsURI, nsPrefix=nsPrefix) + +eClassifiers = {} +getEClassifier = partial(Ecore.getEClassifier, searchspace=eClassifiers) +Management = EEnum('Management', literals=['Intensive', 'Extensive', 'SemiIntensive', 'NA']) + +Roughness = EEnum('Roughness', literals=['VeryRough', 'Rough', + 'MediumRough', 'MediumSmooth', 'Smooth', 'VerySmooth']) + + +class Soil(EObject, metaclass=MetaEClass): + + name = EAttribute(eType=EString, unique=True, derived=False, changeable=True) + roughness = EAttribute(eType=Roughness, unique=True, derived=False, + changeable=True, default_value=Roughness.MediumRough) + conductivityOfDrySoil = EAttribute( + eType=EString, unique=True, derived=False, changeable=True, default_value='1.0 W/(m*K)') + densityOfDrySoil = EAttribute(eType=EString, unique=True, derived=False, + changeable=True, default_value='1100 kg/m³') + specificHeatOfDrySoil = EAttribute( + eType=EString, unique=True, derived=False, changeable=True, default_value='1200 J/(kg*K)') + thermalAbsorptance = EAttribute(eType=EString, unique=True, + derived=False, changeable=True, default_value='0.9') + solarAbsorptance = EAttribute(eType=EString, unique=True, + derived=False, changeable=True, default_value='0.7') + visibleAbsorptance = EAttribute(eType=EString, unique=True, + derived=False, changeable=True, default_value='0.75') + saturationVolumetricMoistureContent = EAttribute( + eType=EString, unique=True, derived=False, changeable=True, default_value='0.0') + residualVolumetricMoistureContent = EAttribute( + eType=EString, unique=True, derived=False, changeable=True, default_value='0.05') + initialVolumetricMoistureContent = EAttribute( + eType=EString, unique=True, derived=False, changeable=True, default_value='0.1') + + def __init__(self, *, name=None, roughness=None, conductivityOfDrySoil=None, densityOfDrySoil=None, specificHeatOfDrySoil=None, thermalAbsorptance=None, solarAbsorptance=None, visibleAbsorptance=None, saturationVolumetricMoistureContent=None, residualVolumetricMoistureContent=None, initialVolumetricMoistureContent=None): + # if kwargs: + # raise AttributeError('unexpected arguments: {}'.format(kwargs)) + + super().__init__() + + if name is not None: + self.name = name + + if roughness is not None: + self.roughness = roughness + + if conductivityOfDrySoil is not None: + self.conductivityOfDrySoil = conductivityOfDrySoil + + if densityOfDrySoil is not None: + self.densityOfDrySoil = densityOfDrySoil + + if specificHeatOfDrySoil is not None: + self.specificHeatOfDrySoil = specificHeatOfDrySoil + + if thermalAbsorptance is not None: + self.thermalAbsorptance = thermalAbsorptance + + if solarAbsorptance is not None: + self.solarAbsorptance = solarAbsorptance + + if visibleAbsorptance is not None: + self.visibleAbsorptance = visibleAbsorptance + + if saturationVolumetricMoistureContent is not None: + self.saturationVolumetricMoistureContent = saturationVolumetricMoistureContent + + if residualVolumetricMoistureContent is not None: + self.residualVolumetricMoistureContent = residualVolumetricMoistureContent + + if initialVolumetricMoistureContent is not None: + self.initialVolumetricMoistureContent = initialVolumetricMoistureContent + + +class Plant(EObject, metaclass=MetaEClass): + + name = EAttribute(eType=EString, unique=True, derived=False, changeable=True) + height = EAttribute(eType=EString, unique=True, derived=False, + changeable=True, default_value='0.1 m') + leafAreaIndex = EAttribute(eType=EString, unique=True, derived=False, + changeable=True, default_value='2.5') + leafReflectivity = EAttribute(eType=EString, unique=True, + derived=False, changeable=True, default_value='0.1') + leafEmissivity = EAttribute(eType=EString, unique=True, derived=False, + changeable=True, default_value='0.9') + minimalStomatalResistance = EAttribute( + eType=EString, unique=True, derived=False, changeable=True, default_value='100.0 s/m') + co2Sequestration = EAttribute(eType=EString, unique=True, derived=False, + changeable=True, default_value='kgCO₂eq') + growsOn = EReference(ordered=True, unique=True, containment=False, derived=False, upper=-1) + + def __init__(self, *, name=None, height=None, leafAreaIndex=None, leafReflectivity=None, leafEmissivity=None, minimalStomatalResistance=None, growsOn=None, co2Sequestration=None): + # if kwargs: + # raise AttributeError('unexpected arguments: {}'.format(kwargs)) + + super().__init__() + + if name is not None: + self.name = name + + if height is not None: + self.height = height + + if leafAreaIndex is not None: + self.leafAreaIndex = leafAreaIndex + + if leafReflectivity is not None: + self.leafReflectivity = leafReflectivity + + if leafEmissivity is not None: + self.leafEmissivity = leafEmissivity + + if minimalStomatalResistance is not None: + self.minimalStomatalResistance = minimalStomatalResistance + + if co2Sequestration is not None: + self.co2Sequestration = co2Sequestration + + if growsOn: + self.growsOn.extend(growsOn) + + +class SupportEnvelope(EObject, metaclass=MetaEClass): + + roughness = EAttribute(eType=Roughness, unique=True, derived=False, + changeable=True, default_value=Roughness.MediumRough) + solarAbsorptance = EAttribute(eType=EDouble, unique=True, + derived=False, changeable=True, default_value=0.0) + conductivity = EAttribute(eType=EDouble, unique=True, derived=False, + changeable=True, default_value=0.0) + visibleAbsorptance = EAttribute(eType=EDouble, unique=True, + derived=False, changeable=True, default_value=0.0) + specificHeat = EAttribute(eType=EDouble, unique=True, derived=False, + changeable=True, default_value=0.0) + density = EAttribute(eType=EDouble, unique=True, derived=False, + changeable=True, default_value=0.0) + thermalAbsorptance = EAttribute(eType=EDouble, unique=True, + derived=False, changeable=True, default_value=0.0) + + def __init__(self, *, roughness=None, solarAbsorptance=None, conductivity=None, visibleAbsorptance=None, specificHeat=None, density=None, thermalAbsorptance=None): + # if kwargs: + # raise AttributeError('unexpected arguments: {}'.format(kwargs)) + + super().__init__() + + if roughness is not None: + self.roughness = roughness + + if solarAbsorptance is not None: + self.solarAbsorptance = solarAbsorptance + + if conductivity is not None: + self.conductivity = conductivity + + if visibleAbsorptance is not None: + self.visibleAbsorptance = visibleAbsorptance + + if specificHeat is not None: + self.specificHeat = specificHeat + + if density is not None: + self.density = density + + if thermalAbsorptance is not None: + self.thermalAbsorptance = thermalAbsorptance + + +class GreeneryCatalog(EObject, metaclass=MetaEClass): + + name = EAttribute(eType=EString, unique=True, derived=False, changeable=True) + description = EAttribute(eType=EString, unique=True, derived=False, changeable=True) + source = EAttribute(eType=EString, unique=True, derived=False, changeable=True) + plantCategories = EReference(ordered=True, unique=True, + containment=True, derived=False, upper=-1) + vegetationCategories = EReference(ordered=True, unique=True, + containment=True, derived=False, upper=-1) + soils = EReference(ordered=True, unique=True, containment=True, derived=False, upper=-1) + + def __init__(self, *, name=None, description=None, source=None, plantCategories=None, vegetationCategories=None, soils=None): + # if kwargs: + # raise AttributeError('unexpected arguments: {}'.format(kwargs)) + + super().__init__() + + if name is not None: + self.name = name + + if description is not None: + self.description = description + + if source is not None: + self.source = source + + if plantCategories: + self.plantCategories.extend(plantCategories) + + if vegetationCategories: + self.vegetationCategories.extend(vegetationCategories) + + if soils: + self.soils.extend(soils) + + +class PlantCategory(EObject, metaclass=MetaEClass): + """Excluding (that is non-overlapping) categories like Trees, Hedeges, Grasses that help users finding a specific biol. plant species.""" + name = EAttribute(eType=EString, unique=True, derived=False, changeable=True) + plants = EReference(ordered=True, unique=True, containment=True, derived=False, upper=-1) + + def __init__(self, *, name=None, plants=None): + # if kwargs: + # raise AttributeError('unexpected arguments: {}'.format(kwargs)) + + super().__init__() + + if name is not None: + self.name = name + + if plants: + self.plants.extend(plants) + + +class IrrigationSchedule(EObject, metaclass=MetaEClass): + + name = EAttribute(eType=EString, unique=True, derived=False, changeable=True) + + def __init__(self, *, name=None): + # if kwargs: + # raise AttributeError('unexpected arguments: {}'.format(kwargs)) + + super().__init__() + + if name is not None: + self.name = name + + +class Vegetation(EObject, metaclass=MetaEClass): + """Plant life or total plant cover (as of an area)""" + name = EAttribute(eType=EString, unique=True, derived=False, changeable=True) + thicknessOfSoil = EAttribute(eType=EString, unique=True, derived=False, + changeable=True, default_value='20 cm') + management = EAttribute(eType=Management, unique=True, derived=False, + changeable=True, default_value=Management.NA) + airGap = EAttribute(eType=EString, unique=True, derived=False, + changeable=True, default_value='0.0 cm') + soil = EReference(ordered=True, unique=True, containment=False, derived=False) + plants = EReference(ordered=True, unique=True, containment=True, derived=False, upper=-1) + + def __init__(self, *, name=None, thicknessOfSoil=None, soil=None, plants=None, management=None, airGap=None): + # if kwargs: + # raise AttributeError('unexpected arguments: {}'.format(kwargs)) + + super().__init__() + + if name is not None: + self.name = name + + if thicknessOfSoil is not None: + self.thicknessOfSoil = thicknessOfSoil + + if management is not None: + self.management = management + + if airGap is not None: + self.airGap = airGap + + if soil is not None: + self.soil = soil + + if plants: + self.plants.extend(plants) + + +class VegetationCategory(EObject, metaclass=MetaEClass): + """Excluding (that is non-overlapping) categories to help users finding a specific vegetation template.""" + name = EAttribute(eType=EString, unique=True, derived=False, changeable=True) + vegetationTemplates = EReference(ordered=True, unique=True, + containment=True, derived=False, upper=-1) + + def __init__(self, *, vegetationTemplates=None, name=None): + # if kwargs: + # raise AttributeError('unexpected arguments: {}'.format(kwargs)) + + super().__init__() + + if name is not None: + self.name = name + + if vegetationTemplates: + self.vegetationTemplates.extend(vegetationTemplates) + + +class PlantPercentage(EObject, metaclass=MetaEClass): + + percentage = EAttribute(eType=EString, unique=True, derived=False, + changeable=True, default_value='100') + plant = EReference(ordered=True, unique=True, containment=False, derived=False) + + def __init__(self, *, percentage=None, plant=None): + # if kwargs: + # raise AttributeError('unexpected arguments: {}'.format(kwargs)) + + super().__init__() + + if percentage is not None: + self.percentage = percentage + + if plant is not None: + self.plant = plant diff --git a/catalogs/greenery/ecore_greenery/greenerycatalog_no_quantities.ecore b/catalogs/greenery/ecore_greenery/greenerycatalog_no_quantities.ecore new file mode 100644 index 00000000..db58a9c0 --- /dev/null +++ b/catalogs/greenery/ecore_greenery/greenerycatalog_no_quantities.ecore @@ -0,0 +1,268 @@ + + + + +
+ + + + +
+ + + + +
+ + +
+
+ + + + +
+ + +
+
+ + + + +
+ + +
+
+ + + + +
+ + +
+
+ + + + +
+ + +
+
+ + + + +
+ + +
+
+ + + + +
+ + +
+
+ + + + +
+ + +
+
+ + + + +
+ + +
+
+ + + + + + + +
+ + +
+
+ + + + +
+ + +
+
+ + + + +
+ + +
+
+ + + + +
+ + +
+
+ + + + +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ + + + +
+
+ + + + + + + + + +
+ + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + diff --git a/city_model_structure/iot/sensor_measure.py b/city_model_structure/iot/sensor_measure.py new file mode 100644 index 00000000..608d97f4 --- /dev/null +++ b/city_model_structure/iot/sensor_measure.py @@ -0,0 +1,40 @@ +""" +Sensor measure module +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2022 Project Author Guille Gutierrez guillermo.gutierrezmorote@concordia.ca +""" + +class SensorMeasure: + def __init__(self, latitude, longitude, utc_timestamp, value): + self._latitude = latitude + self._longitude = longitude + self._utc_timestamp = utc_timestamp + self._value = value + + @property + def latitude(self): + """ + Get measure latitude + """ + return self._latitude + + @property + def longitude(self): + """ + Get measure longitude + """ + return self._longitude + + @property + def utc_timestamp(self): + """ + Get measure timestamp in utc + """ + return self._utc_timestamp + + @property + def value(self): + """ + Get sensor measure value + """ + return self._value diff --git a/city_model_structure/iot/sensor_type.py b/city_model_structure/iot/sensor_type.py new file mode 100644 index 00000000..414b6f60 --- /dev/null +++ b/city_model_structure/iot/sensor_type.py @@ -0,0 +1,20 @@ +""" +Sensor type module +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2022 Project Author Guille Gutierrez guillermo.gutierrezmorote@concordia.ca +""" + +from enum import Enum + +class SensorType(Enum): + HUMIDITY = 0 + TEMPERATURE = 1 + CO2 = 2 + NOISE = 3 + PRESSURE = 4 + DIRECT_RADIATION = 5 + DIFFUSE_RADIATION = 6 + GLOBAL_RADIATION = 7 + AIR_QUALITY = 8 + GAS_FLOW = 9 + ENERGY = 10 diff --git a/city_model_structure/iot/station.py b/city_model_structure/iot/station.py new file mode 100644 index 00000000..8a15e62d --- /dev/null +++ b/city_model_structure/iot/station.py @@ -0,0 +1,41 @@ +""" +Station +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2022 Project Author Guille Gutierrez guillermo.gutierrezmorote@concordia.ca +""" +import uuid + +from city_model_structure.iot.sensor import Sensor + + +class Station: + def __init__(self, station_id=None, _mobile=False): + self._id = station_id + self._mobile = _mobile + self._sensors = [] + + @property + def id(self): + """ + Get the station id a random uuid will be assigned if no ID was provided to the constructor + :return: Id + """ + if self._id is None: + self._id = uuid.uuid4() + return self._id + + @property + def _mobile(self): + """ + Get if the station is mobile or not + :return: bool + """ + return self._mobile + + @property + def sensors(self) -> [Sensor]: + """ + Get the sensors belonging to the station + :return: [Sensor] + """ + return self._sensors diff --git a/city_model_structure/lca_material.py b/city_model_structure/lca_material.py new file mode 100644 index 00000000..f9c620a7 --- /dev/null +++ b/city_model_structure/lca_material.py @@ -0,0 +1,242 @@ +""" +Material module +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2020 Project Author Atiya atiya.atiya@mail.concordia.ca +Contributor Mohammad Reza mohammad.seyedabadi@mail.concordia.ca +""" + +from typing import Union + +class LcaMaterial: + def __init__(self): + self._id = None + self._type = None + self._name = None + self._density = None + self._density_unit = None + self._embodied_carbon = None + self._embodied_carbon_unit = None + self._recycling_ratio = None + self._company_recycling_ratio = None + self._onsite_recycling_ratio = None + self._landfilling_ratio = None + self._cost = None + self._cost_unit = None + + @property + def id(self): + """ + Get material id + :return: int + """ + return self._id + + @id.setter + def id(self, value): + """ + Set material id + :param value: int + """ + self._id = int(value) + + @property + def type(self): + """ + Get material type + :return: str + """ + return self._type + + @type.setter + def type(self, value): + """ + Set material type + :param value: string + """ + self._type = str(value) + + @property + def name(self): + """ + Get material name + :return: str + """ + return self._name + + @name.setter + def name(self, value): + """ + Set material name + :param value: string + """ + self._name = str(value) + + @property + def density(self) -> Union[None, float]: + """ + Get material density in kg/m3 + :return: None or float + """ + return self._density + + @density.setter + def density(self, value): + """ + Set material density + :param value: float + """ + if value is not None: + self._density = float(value) + + @property + def density_unit(self) -> Union[None, str]: + """ + Get material density unit + :return: None or string + """ + return self._density_unit + + @density_unit.setter + def density_unit(self, value): + """ + Set material density unit + :param value: string + """ + if value is not None: + self._density_unit = str(value) + + @property + def embodied_carbon(self) -> Union[None, float]: + """ + Get material embodied carbon + :return: None or float + """ + return self._embodied_carbon + + @embodied_carbon.setter + def embodied_carbon(self, value): + """ + Set material embodied carbon + :param value: float + """ + if value is not None: + self._embodied_carbon = float(value) + + @property + def embodied_carbon_unit(self) -> Union[None, str]: + """ + Get material embodied carbon unit + :return: None or string + """ + return self._embodied_carbon + + @embodied_carbon_unit.setter + def embodied_carbon_unit(self, value): + """ + Set material embodied carbon unit + :param value: string + """ + if value is not None: + self._embodied_carbon_unit = str(value) + + @property + def recycling_ratio(self) -> Union[None, float]: + """ + Get material recycling ratio + :return: None or float + """ + return self._recycling_ratio + + @recycling_ratio.setter + def recycling_ratio(self, value): + """ + Set material recycling ratio + :param value: float + """ + if value is not None: + self._recycling_ratio = float(value) + + @property + def onsite_recycling_ratio(self) -> Union[None, float]: + """ + Get material onsite recycling ratio + :return: None or float + """ + return self._onsite_recycling_ratio + + @onsite_recycling_ratio.setter + def onsite_recycling_ratio(self, value): + """ + Set material onsite recycling ratio + :param value: float + """ + if value is not None: + self._onsite_recycling_ratio = float(value) + + @property + def company_recycling_ratio(self) -> Union[None, float]: + """ + Get material company recycling ratio + :return: None or float + """ + return self._company_recycling_ratio + + @company_recycling_ratio.setter + def company_recycling_ratio(self, value): + """ + Set material company recycling ratio + :param value: float + """ + if value is not None: + self._company_recycling_ratio = float(value) + + @property + def landfilling_ratio(self) -> Union[None, float]: + """ + Get material landfilling ratio + :return: None or float + """ + return self._landfilling_ratio + + @landfilling_ratio.setter + def landfilling_ratio(self, value): + """ + Set material landfilling ratio + :param value: float + """ + if value is not None: + self._landfilling_ratio = float(value) + + @property + def cost(self) -> Union[None, float]: + """ + Get material cost + :return: None or float + """ + return self._cost + + @cost.setter + def cost(self, value): + """ + Set material cost + :param value: float + """ + if value is not None: + self._cost = float(value) + + @property + def cost_unit(self) -> Union[None, str]: + """ + Get material cost unit + :return: None or string + """ + return self._cost_unit + + @cost_unit.setter + def cost_unit(self, value): + """ + Set material cost unit + :param value: string + """ + if value is not None: + self._cost_unit = float(value) diff --git a/city_model_structure/material.py b/city_model_structure/material.py deleted file mode 100644 index d0ef2c53..00000000 --- a/city_model_structure/material.py +++ /dev/null @@ -1,118 +0,0 @@ -""" -LifeCycleAssessment retrieve the specific Life Cycle Assessment module for the given region -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2020 Project Author Atiya -""" - -class Material: - """ - LCA Material class - """ - - def __init__(self, material_name, material_id, type, density, density_unit, embodied_carbon, embodied_carbon_unit, recycling_ratio, - onsite_recycling_ratio, company_recycling_ratio, landfilling_ratio, cost, cost_unit): - self._material_name = material_name - self._material_id = material_id - self._type = type - self._density = density - self._density_unit = density_unit - self._embodied_carbon = embodied_carbon - self._embodied_carbon_unit = embodied_carbon_unit - self._recycling_ratio = recycling_ratio - self._onsite_recycling_ratio = onsite_recycling_ratio - self._company_recycling_ratio = company_recycling_ratio - self._landfilling_ratio = landfilling_ratio - self._cost = cost - self._cost_unit = cost_unit - - @property - def material_name(self): - """ - Get material name - """ - return self._material_name - - @property - def id(self): - """ - Get material id - """ - return self._material_id - - @property - def type(self): - """ - Get material type - """ - return self._type - - @property - def density(self): - """ - Get material density - """ - return self._density - - @property - def density_unit(self): - """ - Get material density unit - """ - return self._density_unit - - @property - def embodied_carbon(self): - """ - Get material embodied carbon - """ - return self._embodied_carbon - - @property - def embodied_carbon_unit(self): - """ - Get material embodied carbon unit - """ - return self._embodied_carbon_unit - - @property - def recycling_ratio(self): - """ - Get material recycling ratio - """ - return self._recycling_ratio - - @property - def onsite_recycling_ratio(self): - """ - Get material onsite recycling ratio - """ - return self._onsite_recycling_ratio - - @property - def company_recycling_ratio(self): - """ - Get material company recycling ratio - """ - return self._company_recycling_ratio - - @property - def landfilling_ratio(self): - """ - Get material landfilling ratio - """ - return self._landfilling_ratio - - @property - def cost(self): - """ - Get material cost - """ - return self._cost - - @property - def cost_unit(self): - """ - Get material cost unit - """ - return self._cost_unit - diff --git a/data/greenery/ecore_greenery_catalog.xml b/data/greenery/ecore_greenery_catalog.xml new file mode 100644 index 00000000..41b7801c --- /dev/null +++ b/data/greenery/ecore_greenery_catalog.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From bf7ba1712103f19d6d4e5198ed79d914e0817c54 Mon Sep 17 00:00:00 2001 From: guille Date: Tue, 29 Mar 2022 06:53:55 -0400 Subject: [PATCH 13/19] Correct sensor class definition --- city_model_structure/iot/sensor.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/city_model_structure/iot/sensor.py b/city_model_structure/iot/sensor.py index 4eceafd1..5855d85b 100644 --- a/city_model_structure/iot/sensor.py +++ b/city_model_structure/iot/sensor.py @@ -5,6 +5,8 @@ Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@conc """ from helpers.location import Location +from city_model_structure.iot.sensor_measure import SensorMeasure +from city_model_structure.iot.sensor_type import SensorType class Sensor: @@ -37,7 +39,7 @@ class Sensor: self._name = str(value) @property - def type(self): + def type(self) -> SensorType: """ Get sensor type :return: str @@ -69,7 +71,7 @@ class Sensor: self._location = value @property - def measures(self): + def measures(self) -> [SensorMeasure]: """ Raises not implemented error """ From b2453b0d27f7093b02129d0c609495423a3bbbd7 Mon Sep 17 00:00:00 2001 From: guille Date: Tue, 29 Mar 2022 07:27:30 -0400 Subject: [PATCH 14/19] Add missing property decorator for getter --- city_model_structure/building_demand/usage_zone.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/city_model_structure/building_demand/usage_zone.py b/city_model_structure/building_demand/usage_zone.py index f8d77890..c2f8e9da 100644 --- a/city_model_structure/building_demand/usage_zone.py +++ b/city_model_structure/building_demand/usage_zone.py @@ -209,7 +209,8 @@ class UsageZone: """ self._appliances = value - def get_internal_gains(self) -> [InternalGains]: + @property + def internal_gains(self) -> [InternalGains]: """ Calculates and returns the list of all internal gains defined :return: InternalGains From 5281b63de214f13d537b411c086a2b792d319031 Mon Sep 17 00:00:00 2001 From: guille Date: Tue, 29 Mar 2022 07:33:04 -0400 Subject: [PATCH 15/19] Remove unneeded files and wrong unittest, partial usage factory test correction --- .../iot/concordia_energy_sensor.py | 45 -------------- .../iot/concordia_gas_flow_sensor.py | 45 -------------- .../iot/concordia_temperature_sensor.py | 45 -------------- unittests/test_sensors_factory.py | 58 ------------------- unittests/test_usage_factory.py | 2 +- 5 files changed, 1 insertion(+), 194 deletions(-) delete mode 100644 city_model_structure/iot/concordia_energy_sensor.py delete mode 100644 city_model_structure/iot/concordia_gas_flow_sensor.py delete mode 100644 city_model_structure/iot/concordia_temperature_sensor.py delete mode 100644 unittests/test_sensors_factory.py diff --git a/city_model_structure/iot/concordia_energy_sensor.py b/city_model_structure/iot/concordia_energy_sensor.py deleted file mode 100644 index afc7b930..00000000 --- a/city_model_structure/iot/concordia_energy_sensor.py +++ /dev/null @@ -1,45 +0,0 @@ -""" -Energy Sensor module -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@concordia.ca -""" - -import pandas as pd -from city_model_structure.iot.sensor import Sensor - - -class ConcordiaEnergySensor(Sensor): - """ - Concordia energy sensor. - """ - - def __init__(self, name): - super().__init__() - self._name = name - self._interval = 5 - self._interval_units = 'minutes' - self._type = 'ConcordiaEnergySensor' - self._units = 'kW' - self._measures = pd.DataFrame(columns=["Date time", "Energy consumption"]) - - @property - def measures(self) -> pd.DataFrame: - """ - Get sensor measures [yyyy-mm-dd, hh:mm:ss kW] - :return: DataFrame["Date time", "Energy consumption"] - """ - return self._measures - - @measures.deleter - def measures(self): - """ - Delete sensor measures - """ - self._measures.drop = None - - def add_period(self, measures): - """ - Add or update a period measures to the dataframe - """ - measures = self._measures.append(measures, ignore_index=True) - self._measures = measures.drop_duplicates('Date time', keep='last') diff --git a/city_model_structure/iot/concordia_gas_flow_sensor.py b/city_model_structure/iot/concordia_gas_flow_sensor.py deleted file mode 100644 index 86da31bd..00000000 --- a/city_model_structure/iot/concordia_gas_flow_sensor.py +++ /dev/null @@ -1,45 +0,0 @@ -""" -Gas Flow Sensor module -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca -""" - -import pandas as pd -from city_model_structure.iot.sensor import Sensor - - -class ConcordiaGasFlowSensor(Sensor): - """ - Concordia gas flow sensor. - """ - - def __init__(self, name): - super().__init__() - self._name = name - self._interval = 5 - self._interval_units = 'minutes' - self._type = 'ConcordiaGasFlowSensor' - self._units = 'm3' - self._measures = pd.DataFrame(columns=["Date time", "Gas Flow Cumulative Monthly"]) - - @property - def measures(self) -> pd.DataFrame: - """ - Get sensor measures [yyyy-mm-dd, hh:mm:ss m3] - :return: DataFrame["Date time", "Gas Flow Cumulative Monthly"] - """ - return self._measures - - @measures.deleter - def measures(self): - """ - Delete sensor measures - """ - self._measures.drop = None - - def add_period(self, measures): - """ - Add or update a period measures to the dataframe - """ - measures = self._measures.append(measures, ignore_index=True) - self._measures = measures.drop_duplicates('Date time', keep='last') diff --git a/city_model_structure/iot/concordia_temperature_sensor.py b/city_model_structure/iot/concordia_temperature_sensor.py deleted file mode 100644 index 9ae6079c..00000000 --- a/city_model_structure/iot/concordia_temperature_sensor.py +++ /dev/null @@ -1,45 +0,0 @@ -""" -Temperature Sensor module -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca -""" - -import pandas as pd -from city_model_structure.iot.sensor import Sensor - - -class ConcordiaTemperatureSensor(Sensor): - """ - Concordia temperature sensor. - """ - - def __init__(self, name): - super().__init__() - self._name = name - self._interval = 5 - self._interval_units = 'minutes' - self._type = 'ConcordiaTemperatureSensor' - self._units = 'Celsius' - self._measures = pd.DataFrame(columns=["Date time", "Temperature"]) - - @property - def measures(self) -> pd.DataFrame: - """ - Get sensor measures [yyyy-mm-dd, hh:mm:ss Celsius] - :return: DataFrame["Date time", "Temperature"] - """ - return self._measures - - @measures.deleter - def measures(self): - """ - Delete sensor measures - """ - self._measures.drop = None - - def add_period(self, measures): - """ - Add or update a period measures to the dataframe - """ - measures = self._measures.append(measures, ignore_index=True) - self._measures = measures.drop_duplicates('Date time', keep='last') diff --git a/unittests/test_sensors_factory.py b/unittests/test_sensors_factory.py deleted file mode 100644 index 55b13e01..00000000 --- a/unittests/test_sensors_factory.py +++ /dev/null @@ -1,58 +0,0 @@ -""" -TestSensorsFactory test and validate the city model structure schedules -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2021 Project Author Guille Gutierrez guillermo.gutierrezmorote@concordia.ca -""" -from pathlib import Path -from unittest import TestCase -from city_model_structure.building import Building -from city_model_structure.buildings_cluster import BuildingsCluster -from city_model_structure.city import City -from imports.sensors_factory import SensorsFactory - - -class TestSensorsFactory(TestCase): - """ - TestSchedulesFactory TestCase - """ - - def setUp(self) -> None: - """ - Configure test environment - :return: - """ - self._city = TestSensorsFactory._mockup_city() - self._end_point = (Path(__file__).parent / - 'tests_data/EV-GM energy demand weekly report_01-26-20_04-30.csv').resolve() - - @staticmethod - def _mockup_city(): - lower_corner = [0, 0, 0] - upper_corner = [10, 10, 10] - srs_name = 'Mockup_city' - buildings = [] - lod = 2 - surfaces = [] - year_of_construction = 2021 - function = "office" - city = City(lower_corner, upper_corner, srs_name) - buildings.append(Building("EV", lod, surfaces, year_of_construction, function, lower_corner)) - buildings.append(Building("GM", lod, surfaces, year_of_construction, function, lower_corner)) - buildings.append(Building("MB", lod, surfaces, year_of_construction, function, lower_corner)) - for building in buildings: - city.add_city_object(building) - buildings_cluster = BuildingsCluster("GM_MB_EV", buildings) - city.add_city_objects_cluster(buildings_cluster) - return city - - def test_city_with_sensors(self): - """ - Load concordia sensors and verify it - """ - SensorsFactory('cec', self._city, self._end_point).enrich() - SensorsFactory('cgf', self._city, self._end_point).enrich() - SensorsFactory('ct', self._city, self._end_point).enrich() - for city_object in self._city.city_objects: - print(city_object.name, len(city_object.sensors)) - for sensor in city_object.sensors: - print(sensor.name) diff --git a/unittests/test_usage_factory.py b/unittests/test_usage_factory.py index 269457be..2bcb6ceb 100644 --- a/unittests/test_usage_factory.py +++ b/unittests/test_usage_factory.py @@ -72,7 +72,7 @@ class TestUsageFactory(TestCase): def _check_usage_zone(self, usage_zone): self.assertIsNotNone(usage_zone.usage, 'usage is none') self.assertIsNotNone(usage_zone.percentage, 'usage percentage is none') - self.assertIsNotNone(usage_zone.get_internal_gains, 'internal gains is none') + self.assertIsNotNone(usage_zone.internal_gains, 'internal gains is none') self.assertIsNotNone(usage_zone.hours_day, 'hours per day is none') self.assertIsNotNone(usage_zone.days_year, 'days per year is none') self.assertIsNotNone(usage_zone.thermal_control, 'thermal control is none') From 0c92e6e4ad713756a4b0f3a2a4518a6ecf26fbc5 Mon Sep 17 00:00:00 2001 From: guille Date: Wed, 30 Mar 2022 13:26:31 -0400 Subject: [PATCH 16/19] Fixing libs wrong commit --- exports/energy_systems/__init__.py | 0 imports/construction_factory.py | 1 + imports/geometry/rhino.py | 6 ++++-- imports/sensors_factory.py | 9 +++------ 4 files changed, 8 insertions(+), 8 deletions(-) delete mode 100644 exports/energy_systems/__init__.py diff --git a/exports/energy_systems/__init__.py b/exports/energy_systems/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/imports/construction_factory.py b/imports/construction_factory.py index c315c455..e83ea7c8 100644 --- a/imports/construction_factory.py +++ b/imports/construction_factory.py @@ -22,6 +22,7 @@ class ConstructionFactory: def _nrel(self): """ Enrich the city by using NREL information + :cat: """ UsPhysicsParameters(self._city, self._base_path).enrich_buildings() diff --git a/imports/geometry/rhino.py b/imports/geometry/rhino.py index b9007ecb..53f21088 100644 --- a/imports/geometry/rhino.py +++ b/imports/geometry/rhino.py @@ -4,8 +4,10 @@ SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@concordia.ca """ from numpy import inf -#from rhino3dm import * -#from rhino3dm._rhino3dm import MeshType +from rhino3dm import * +from rhino3dm._rhino3dm import MeshType + + from city_model_structure.attributes.point import Point import numpy as np diff --git a/imports/sensors_factory.py b/imports/sensors_factory.py index 3060d1a2..98fbb809 100644 --- a/imports/sensors_factory.py +++ b/imports/sensors_factory.py @@ -5,9 +5,6 @@ Copyright © 2021 Project Author Guille Gutierrez guillermo.gutierrezmorote@conc Contributors Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ from pathlib import Path -from imports.sensors.concordia_energy_consumption import ConcordiaEnergyConsumption -from imports.sensors.concordia_gas_flow import ConcordiaGasFlow -from imports.sensors.concordia_temperature import ConcordiaTemperature class SensorsFactory: @@ -26,19 +23,19 @@ class SensorsFactory: """ Enrich the city by using concordia energy consumption sensors as data source """ - ConcordiaEnergyConsumption(self._city, self._end_point, self._base_path) + raise NotImplementedError('need to be reimplemented') def _cgf(self): """ Enrich the city by using concordia gas flow sensors as data source """ - ConcordiaGasFlow(self._city, self._end_point, self._base_path) + raise NotImplementedError('need to be reimplemented') def _ct(self): """ Enrich the city by using concordia temperature sensors as data source """ - ConcordiaTemperature(self._city, self._end_point, self._base_path) + raise NotImplementedError('need to be reimplemented') def enrich(self): """ From 688541b20c0619107b4456314c428614da914f67 Mon Sep 17 00:00:00 2001 From: guille Date: Wed, 30 Mar 2022 13:31:59 -0400 Subject: [PATCH 17/19] removing wrong comment --- imports/construction_factory.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/imports/construction_factory.py b/imports/construction_factory.py index e83ea7c8..e65d5552 100644 --- a/imports/construction_factory.py +++ b/imports/construction_factory.py @@ -22,14 +22,13 @@ class ConstructionFactory: def _nrel(self): """ Enrich the city by using NREL information - :cat: """ UsPhysicsParameters(self._city, self._base_path).enrich_buildings() def _nrcan(self): """ Enrich the city by using NRCAN information - :alert: NRCAN handler only contains simplified construction information + :alert: NRCAN handler only contains simplified construction information (residential) """ CaPhysicsParameters(self._city, self._base_path).enrich_buildings() From 60f5d9aca150515161da5a98408574ec279687e9 Mon Sep 17 00:00:00 2001 From: guille Date: Thu, 31 Mar 2022 06:44:33 -0400 Subject: [PATCH 18/19] Changing comments and signature to improve the documentation --- catalogs/catalog.py | 10 +++++----- catalogs/greenery_catalog_factory.py | 19 ++++++------------- 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/catalogs/catalog.py b/catalogs/catalog.py index 1aff80af..67de9fda 100644 --- a/catalogs/catalog.py +++ b/catalogs/catalog.py @@ -6,27 +6,27 @@ Copyright © 2022 Project Author Guille Gutierrez guillermo.gutierrezmorote@conc class Catalog: """ - Catalog name + Catalogs base class not implemented instance of the Catalog base class, catalogs will inherit from this class. """ @property - def names(self): + def names(self, category=None): """ Base property to return the catalog entries names - :return: not implemented error + :return: Catalog names filter by category if provided """ raise NotImplementedError def entries(self, category=None): """ Base property to return the catalog entries - :return: not implemented error + :return: Catalog content filter by category if provided """ raise NotImplementedError def get_entry(self, name): """ Base property to return the catalog entry matching the given name - :return: not implemented error + :return: Catalog entry with the matching name """ raise NotImplementedError diff --git a/catalogs/greenery_catalog_factory.py b/catalogs/greenery_catalog_factory.py index 3b70687a..905f1602 100644 --- a/catalogs/greenery_catalog_factory.py +++ b/catalogs/greenery_catalog_factory.py @@ -5,8 +5,9 @@ Copyright © 2022 Project Author Guille Gutierrez guillermo.gutierrezmorote@conc """ from pathlib import Path +from typing import TypeVar from catalogs.greenery.greenery_catalog import GreeneryCatalog - +Catalog = TypeVar('Catalog') class GreeneryCatalogFactory: """ @@ -19,25 +20,17 @@ class GreeneryCatalogFactory: self._path = base_path @property - def _nrel(self) -> GreeneryCatalog: + def _nrel(self): """ - Return a greenery catalog using ecore as datasource + Return a greenery catalog based in NREL using ecore as datasource :return: GreeneryCatalog """ return GreeneryCatalog((self._path / 'ecore_greenery_catalog.xml').resolve()) @property - def catalog(self) -> GreeneryCatalog: + def catalog(self) -> Catalog: """ Enrich the city given to the class using the class given handler - :return: City + :return: Catalog """ return getattr(self, self._file_type, lambda: None) - - @property - def catalog_debug(self) -> GreeneryCatalog: - """ - Enrich the city given to the class using the class given handler - :return: City - """ - return GreeneryCatalog((self._path / 'ecore_greenery_catalog.xml').resolve()) From ee8549bf98bad6f0ef470a14ce822b02d42c45fe Mon Sep 17 00:00:00 2001 From: guille Date: Mon, 4 Apr 2022 08:37:21 -0400 Subject: [PATCH 19/19] Correct import issue --- city_model_structure/city.py | 1 + 1 file changed, 1 insertion(+) diff --git a/city_model_structure/city.py b/city_model_structure/city.py index 780d8e35..b9f029b3 100644 --- a/city_model_structure/city.py +++ b/city_model_structure/city.py @@ -18,6 +18,7 @@ from city_model_structure.city_object import CityObject from city_model_structure.city_objects_cluster import CityObjectsCluster from city_model_structure.buildings_cluster import BuildingsCluster from city_model_structure.fuel import Fuel +from city_model_structure.iot.station import Station from city_model_structure.machine import Machine from city_model_structure.parts_consisting_building import PartsConsistingBuilding from city_model_structure.subway_entrance import SubwayEntrance