diff --git a/city_model_structure/attributes/surface.py b/city_model_structure/attributes/surface.py index 0ff20d18..a5b0578d 100644 --- a/city_model_structure/attributes/surface.py +++ b/city_model_structure/attributes/surface.py @@ -36,6 +36,9 @@ class Surface: self._min_x = None self._min_y = None self._min_z = None + self._max_x = None + self._max_y = None + self._max_z = None self._shared_surfaces = [] self._global_irradiance = dict() self._perimeter_polygon = None @@ -171,6 +174,21 @@ class Surface: self._perimeter_points_list = np.reshape(s, len(s) * 3) return self._perimeter_points_list + def _max_coord(self, axis): + if axis == 'x': + axis = 0 + elif axis == 'y': + axis = 1 + else: + axis = 2 + max_coordinate = '' + for point in self.points: + if max_coordinate == '': + max_coordinate = point[axis] + elif max_coordinate < point[axis]: + max_coordinate = point[axis] + return max_coordinate + def _min_coord(self, axis): if axis == 'x': axis = 0 @@ -186,6 +204,36 @@ class Surface: min_coordinate = point[axis] return min_coordinate + @property + def max_x(self): + """ + Surface maximal x value + :return: float + """ + if self._max_x is None: + self._max_x = self._max_coord('x') + return self._max_x + + @property + def max_y(self): + """ + Surface maximal y value + :return: float + """ + if self._max_y is None: + self._max_y = self._max_coord('y') + return self._max_y + + @property + def max_z(self): + """ + Surface maximal z value + :return: float + """ + if self._max_z is None: + self._max_z = self._max_coord('z') + return self._max_z + @property def min_x(self): """ diff --git a/city_model_structure/attributes/thermal_zone.py b/city_model_structure/attributes/thermal_zone.py index 0c4cc107..2e23088c 100644 --- a/city_model_structure/attributes/thermal_zone.py +++ b/city_model_structure/attributes/thermal_zone.py @@ -4,8 +4,8 @@ SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@concordia.ca Contributors Pilar Monsalvete Álvarez de Uribarri pilar.monsalvete@concordia.ca """ +import uuid from typing import List, TypeVar - from city_model_structure.attributes.surface import Surface from city_model_structure.attributes.usage_zone import UsageZone @@ -17,12 +17,10 @@ class ThermalZone: """ ThermalZone class """ - def __init__(self, surfaces, is_heated, is_cooled): + def __init__(self, surfaces): self._surfaces = surfaces self._floor_area = None self._bounded = None - self._is_heated = is_heated - self._is_cooled = is_cooled self._is_mechanically_ventilated = None self._additional_thermal_bridge_u_value = None self._effective_thermal_capacity = None @@ -32,6 +30,15 @@ class ThermalZone: self._usage_zones = None self._volume = None self._volume_geometry = None + self._id = None + self._is_heated = False + self._is_cooled = False + + @property + def id(self): + if self._id is None: + self._id = uuid.uuid4() + return self._id @property def is_heated(self): @@ -41,6 +48,14 @@ class ThermalZone: """ return self._is_heated + @is_heated.setter + def is_heated(self, value): + """ + Set thermal zone heated flag + :return: Boolean + """ + self._is_heated = value + @property def is_cooled(self): """ @@ -49,6 +64,14 @@ class ThermalZone: """ return self._is_cooled + @is_cooled.setter + def is_cooled(self, value): + """ + Set thermal zone cooled flag + :return: Boolean + """ + self._is_cooled = value + @property def is_mechanically_ventilated(self): """ diff --git a/city_model_structure/attributes/usage_zone.py b/city_model_structure/attributes/usage_zone.py index 648dd935..1f73c500 100644 --- a/city_model_structure/attributes/usage_zone.py +++ b/city_model_structure/attributes/usage_zone.py @@ -4,6 +4,7 @@ SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2020 Project Author Pilar Monsalvete pilar_monsalvete@yahoo.es Contributors Guille Gutierrez guillermo.gutierrezmorote@concordia.ca """ +import uuid from typing import List, TypeVar InternalGains = TypeVar('InternalGains') @@ -17,6 +18,7 @@ class UsageZone: UsageZone class """ def __init__(self): + self._id = None self._usage = None self._internal_gains = None self._heating_setpoint = None @@ -36,6 +38,12 @@ class UsageZone: self._ventilation_schedule = None self._volume_geometry = None + @property + def id(self): + if self._id is None: + self._id = uuid.uuid4() + return self._id + @property def lights(self) -> List[Lighting]: return self._lights diff --git a/city_model_structure/building.py b/city_model_structure/building.py index fe8b3884..151b882d 100644 --- a/city_model_structure/building.py +++ b/city_model_structure/building.py @@ -33,8 +33,6 @@ class Building(CityObject): self._function = function self._city_lower_corner = city_lower_corner self._building_lower_corner = None - self._heated = None - self._cooled = None self._average_storey_height = None self._storeys_above_ground = None self._floor_area = None @@ -59,7 +57,7 @@ class Building(CityObject): self._thermal_zones = [] if self.lod < 4: # for lod under 4 is just one thermal zone - self._thermal_zones.append(ThermalZone(self.surfaces, self._heated, self._cooled)) + self._thermal_zones.append(ThermalZone(self.surfaces)) for t_zones in self._thermal_zones: t_zones.bounded = [ThermalBoundary(s, [t_zones]) for s in t_zones.surfaces] @@ -81,6 +79,28 @@ class Building(CityObject): """ return self._grounds + @property + def is_heated(self): + """ + Get building heated flag + :return: Boolean + """ + for thermal_zone in self.thermal_zones: + if thermal_zone.is_heated: + return thermal_zone.is_heated + return False + + @property + def is_cooled(self): + """ + Get building cooled flag + :return: Boolean + """ + for thermal_zone in self.thermal_zones: + if thermal_zone.is_cooled: + return thermal_zone.is_cooled + return False + @property def roofs(self) -> [Surface]: """ @@ -110,10 +130,9 @@ class Building(CityObject): :param values: [UsageZones] :return: None """ - # ToDo: this is only valid for one usage zone need to be revised for multiple usage zones. self._usage_zones = values for thermal_zone in self.thermal_zones: - thermal_zone.usage_zones = [(100, usage_zone) for usage_zone in values] + thermal_zone.usage_zones = values @property def terrains(self) -> List[Surface]: diff --git a/exports/formats/energy_ade.py b/exports/formats/energy_ade.py index 2a293ac1..a0477aab 100644 --- a/exports/formats/energy_ade.py +++ b/exports/formats/energy_ade.py @@ -5,6 +5,7 @@ Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@conc """ import xmltodict import uuid +import datetime from pathlib import Path @@ -27,7 +28,7 @@ class EnergyAde: '@xmlns:tun':'http://www.opengis.net/citygml/tunnel/2.0', '@xmlns:tex':'http://www.opengis.net/citygml/texturedsurface/2.0', '@xmlns:gml':'http://www.opengis.net/gml', - '@xmlns:gen':'http://www.opengis.net/citygml/generics/2.0', + '@xmlns:genobj':'http://www.opengis.net/citygml/generics/2.0', '@xmlns:dem':'http://www.opengis.net/citygml/relief/2.0', '@xmlns:app':'http://www.opengis.net/citygml/appearance/2.0', '@xmlns:luse':'http://www.opengis.net/citygml/landuse/2.0', @@ -37,6 +38,7 @@ class EnergyAde: '@xmlns:pbase':'http://www.opengis.net/citygml/profiles/base/2.0', '@xmlns:smil20': 'http://www.w3.org/2001/SMIL20/', '@xmlns:bldg':'http://www.opengis.net/citygml/building/2.0', + '@xmlns:energy': "http://www.sig3d.org/citygml/2.0/energy/1.0", '@xmlns:core':'http://www.opengis.net/citygml/2.0', '@xmlns:grp':'http://www.opengis.net/citygml/cityobjectgroup/2.0', 'gml:boundedBy': { @@ -53,11 +55,31 @@ class EnergyAde: for building in self._city.buildings: building_dic = { 'bldg:Building': { - '@gml:id': building.name + '@gml:id': building.name, + 'gml:description': f'Building {building.name} at {self._city.name}', + 'gml:name': f'{building.name}', + 'core:creationDate': datetime.datetime.now().strftime('%Y-%m-%d') } } building_dic = EnergyAde._measures(building, building_dic) building_dic = EnergyAde._building_geometry(building, building_dic, self._city) + building_dic['bldg:Building']['energy:volume'] = { + 'energy:VolumeType': { + 'energy:type':'grossVolume', + 'energy:value': { + '@uom': 'm3', + 'energy:value': building.volume + } + } + } + building_dic['bldg:Building']['energy:referencePoint'] = { + 'gml:Point': { + '@srsName': self._city.srs_name, + '@gml:id': f'GML_{uuid.uuid4()}', + 'gml:Pos': f'{" ".join(map(str,building.centroid))}' + } + } + building_dic['bldg:Building']['energy:thermalZone'] = EnergyAde._thermal_zones(building,self._city) buildings.append(building_dic) energy_ade['core:CityModel']['core:cityObjectMember'] = buildings @@ -76,21 +98,20 @@ class EnergyAde: if measure is not None: measures.append(measure) if len(measures) != 0: - building_dic['genobj:measureAttribute'] = measures + building_dic['bldg:Building']['genobj:measureAttribute'] = measures - periods = [] + demands = [] for key in building.heating: if key != 'year': - period = EnergyAde._period(building.heating, key, 'Heating energy', 'INSEL') - periods.append(period) + demand = EnergyAde._demand(building.heating, key, 'Heating energy', 'INSEL') + demands.append(demand) for key in building.cooling: if key != 'year': - period = EnergyAde._period(building.cooling, key, 'Cooling energy', 'INSEL') - periods.append(period) - - if len(periods) != 0: - building_dic['energy:demands'] = {'energy:EnergyDemand': periods} + demand = EnergyAde._demand(building.cooling, key, 'Cooling energy', 'INSEL') + demands.append(demand) + if len(demands) != 0: + building_dic['bldg:Building']['energy:demands'] = demands return building_dic @@ -108,45 +129,46 @@ class EnergyAde: return measure @staticmethod - def _period(measure_dict, key_value, description, source): - period = { - '@gml:id': uuid.uuid4(), - 'energy:energyAmount': { - 'energy:RegularTimeSeries': { - 'energy:variableProperties': { - 'energy:TimeValuesProperties': { - 'energy:acquisitionMethod': 'simulation', - 'energy:source': source, - 'energy:thematicDescription': description, - }, - 'energy:timeInterval': { - '@unit': key_value, - '#text': '1', - }, - 'energy:values': { - '@uom': 'kWh', - '#text': ' '.join([str(float(e) / 1000) for e in measure_dict[key_value][source]]) + def _demand(measure_dict, key_value, description, source): + demand = { + 'energy:EnergyDemand': { + '@gml:id': f'GML_{uuid.uuid4()}', + 'energy:energyAmount': { + 'energy:RegularTimeSeries': { + 'energy:variableProperties': { + 'energy:TimeValuesProperties': { + 'energy:acquisitionMethod': 'simulation', + 'energy:source': source, + 'energy:thematicDescription': description, + }, + 'energy:timeInterval': { + '@unit': key_value, + '#text': '1', + }, + 'energy:values': { + '@uom': 'kWh', + '#text': ' '.join([str(float(e) / 1000) for e in measure_dict[key_value][source]]) + } } } - } + }, + 'energy:endUse': 'spaceHeating' } } - return period + return demand @staticmethod def _building_geometry(building, building_dic, city): - building_dic['bldg:function'] = building.function - building_dic['bldg:usage'] = ', '.join([u.usage for u in building.usage_zones]) - building_dic['bldg:yearOfConstruction'] = building.year_of_construction - building_dic['bldg:roofType'] = building.roof_type - - building_dic['bldg:measuredHeight'] = { + building_dic['bldg:Building']['bldg:function'] = building.function + building_dic['bldg:Building']['bldg:usage'] = ', '.join([u.usage for u in building.usage_zones]) + building_dic['bldg:Building']['bldg:yearOfConstruction'] = building.year_of_construction + building_dic['bldg:Building']['bldg:roofType'] = building.roof_type + building_dic['bldg:Building']['bldg:measuredHeight'] = { '@uom': 'm', '#text': f'{building.max_height}' } - - building_dic['bldg:storeysAboveGround'] = building.storeys_above_ground + building_dic['bldg:Building']['bldg:storeysAboveGround'] = building.storeys_above_ground if building.lod == 1: building_dic = EnergyAde._lod1(building, building_dic, city) elif building.lod == 2: @@ -178,8 +200,6 @@ class EnergyAde: else: surface_type = 'bldg:RoofSurface' - - surface_dic = { surface_type: { '@gml:id': f'GML_{uuid.uuid4()}', @@ -198,13 +218,13 @@ class EnergyAde: 'surfaceMember': { 'gml:Polygon': { '@srsName': city.srs_name, - '@gml:id': f'#PolyId{surface.name}', + '@gml:id': f'PolyId{surface.name}', 'gml:exterior': { 'gml:LinearRing': { - '@gml:id': f'#PolyId{surface.name}_0', + '@gml:id': f'PolyId{surface.name}_0', 'gml:posList': { '@srsDimension': '3', - '@count': len(surface.points_list) + 1, + '@count': len(surface.points) + 1, '#text': f'{" ".join(map(str, surface.points_list))} {" ".join(map(str, surface.points[0]))}' } } @@ -215,10 +235,8 @@ class EnergyAde: } } } - print(surface_dic) boundaries.append(surface_dic) - print(surface_dic) - building_dic['bldg:lod2Solid'] = { + building_dic['bldg:Building']['bldg:lod2Solid'] = { 'gml:Solid': { '@gml:id': f'GML_{uuid.uuid4()}', 'gml:exterior': { @@ -231,6 +249,47 @@ class EnergyAde: } } - building_dic['gml:boundedBy'] = boundaries - print(building_dic) - return building_dic \ No newline at end of file + building_dic['bldg:Building']['gml:boundedBy'] = boundaries + return building_dic + + @staticmethod + def _thermal_zones(building, city): + thermal_zones = [] + for index, thermal_zone in enumerate(building.thermal_zones): + usage_zones = [] + for usage_zone in thermal_zone.usage_zones: + usage_zones.append({'@xlink:href': f'#GML_{usage_zone.id}'}) + + thermal_zone_dic = { + 'energy:ThermalZone': { + '@gml:id': f'GML_{thermal_zone.id}', + 'gml:name': f'Thermal zone {index} in {building.name} building', + 'energy:contains': [], + 'energy:floorArea': { + 'energy:FloorArea' : { + 'energy:type': 'grossFloorArea', + 'energy:value': { + '@uom': 'm2', + '#text': f'{thermal_zone.floor_area}' + } + } + }, + 'energy:volume': { + 'energy:VolumeType': { + 'energy:type': 'grossVolume', + 'energy:value': { + '@uom': 'm3', + #todo: for now we have just one thermal zone, therefore is the building volume, this need to be changed + '#text': f'{building.volume}' + } + } + }, + 'energy:isCooled': f'{thermal_zone.is_cooled}', + 'energy:isHeated': f'{thermal_zone.is_heated}', + + } + } + thermal_zone_dic['energy:ThermalZone']['energy:contains'] = usage_zones + thermal_zones.append(thermal_zone_dic) + print(thermal_zones) + return thermal_zones \ No newline at end of file