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')