diff --git a/.gitignore b/.gitignore index fd31855a..a456a415 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,12 @@ -# Default ignored files -.idea \ No newline at end of file +!.gitignore +**/venv/ +.idea/ +/development_tests/ +/data/energy_systems/heat_pumps/*.csv +/data/energy_systems/heat_pumps/*.insel +.DS_Store +**/.env +**/hub/logs/ +**/__pycache__/ +**/.idea/ +cerc_hub.egg-info diff --git a/cerc_hub.egg-info/.gitignore b/cerc_hub.egg-info/.gitignore deleted file mode 100644 index e0a497ab..00000000 --- a/cerc_hub.egg-info/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Except this file -* -!.gitignore \ No newline at end of file diff --git a/hub/.gitignore b/hub/.gitignore deleted file mode 100644 index e23b0042..00000000 --- a/hub/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -!.gitignore -**/venv/ -.idea/ -/development_tests/ -/data/energy_systems/heat_pumps/*.csv -/data/energy_systems/heat_pumps/*.insel -.DS_Store -**/.env -**/hub/logs/ -**/__pycache__/ -**/.idea/ - diff --git a/hub/LINUX_INSTALL.md b/hub/LINUX_INSTALL.md new file mode 100644 index 00000000..4de52244 --- /dev/null +++ b/hub/LINUX_INSTALL.md @@ -0,0 +1,50 @@ +# LINUX_INSTALL +## Prepare your environment +### Install Miniconda +1. Get the link for the latest version of Miniconda from https://docs.conda.io/en/latest/miniconda.html +2. Download the installer using wget + ```` + wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh + ```` +3. Make the installer executable + ```` + chmod +x ./Miniconda3-latest-Linux-x86_64.sh + ```` +4. Run the installer + ```` + ./Miniconda3-latest-Linux-x86_64.sh + ```` +5. Holder enter until you are prompted to accept the license terms. Enter yes. +6. Initialize the conda environment + ```` + conda init bash + ```` +7. Source .bashrc + ```` + source ~/.bashrc + ```` +8. Create a conda environment for the hub + ```` + conda create --name hub python=3.9.16 + ```` + +### Setup SRA +1. Get the sra binary and libshortwave.so library from Guille or Koa +2. Place the binary and the library into your directory of choice +3. Make a symlink for the binary and place it into /usr/local/bin/sra + ```` + sudo ln -s ~/sra /usr/local/bin/sra + ```` +4. Make a symlink for the library and place it into /usr/local/lib/libshortwave.so + ```` + sudo ln -s ~/libshortwave.so /usr/local/lib/libshortwave.so + ```` +### Setup INSEL +1. TBD + +### Get a Python editor +You are welcome to use the Python editor of your preference. The CERC team generally uses PyCharm to develop the hub. +The latest version of PyCharm can be downloaded from [JetBrains website](https://www.jetbrains.com/pycharm/promo/?source=google&medium=cpc&campaign=14127625109&term=pycharm&content=536947779504&gad=1&gclid=CjwKCAjw0ZiiBhBKEiwA4PT9z2AxPfy39x_RcBqlYxJ6sm_s55T9qvA_sZ8ZfkhIVX6FOD-ySbmzARoCcpQQAvD_BwE). + For setup and installation instructions, please view the "Get a Python Editor" +from the [WINDOWS_INSTALL](https://nextgenerations-cities.encs.concordia.ca/gitea/CERC/hub/src/branch/main/hub/WINDOWS_INSTALL.md) +documentation. \ No newline at end of file diff --git a/hub/WINDOWS_INSTALL.md b/hub/WINDOWS_INSTALL.md index 0eb13976..c31860a0 100644 --- a/hub/WINDOWS_INSTALL.md +++ b/hub/WINDOWS_INSTALL.md @@ -2,16 +2,16 @@ This is an installation guide for Windows, covering all the steps needed to begin developing code for the Urban Simulation Platform 'Hub'. At the end of this process you will have installed and configured all the necessary applications, -set up your own project on CERC's Gitlab and created your first python file. +set up your own project on CERC's Gitea and created your first python file. ## Prepare your environment -g To develop any new code for the Urban Simulation Platform you must have the right software applications installed and configured. The Platform is written in python and so the applications you need are: * Miniconda +* SRA Files * Python Editor -You also need to register a user account with the CERC's code repository on Gitlab and have the necessary permissions for +You also need to register a user account with the CERC's code repository on Gitea and have the necessary permissions for creating new code. For that purpose, please, contact Guillermo (guillermo.gutierrezmorote@concordia.ca) or Koa (kekoa.wells@concordia.ca) as soon as possible. @@ -47,6 +47,29 @@ _The term '...' is not recognized as the name of a cmdlet, function,..._ To solve it, type 'Set-ExecutionPolicy Unrestricted' as shown in the image. +### Setup SRA + +1. Get the SRA executable and dll files from Guille or Koa +2. Create a folder in "C:\Program Files\" called "sra" + +![create_sra](docs/img_windows_install/img_34.png) + +3. Copy shortwave_integer.exe and pthreadGC2.dll into the sra folder. + +![create_sra](docs/img_windows_install/img_35.png) + +4. Add the newly created sra folder to the Path, similar to step 2 from the Miniconda setup above. + +![create_sra](docs/img_windows_install/img_36.png) + +### Install and setup INSEL + +1. Get the INSEL installer from Guille or Koa +2. Run the installer to completion using the default installation path +3. Add the INSEL installation folder to the Path + +![create_sra](docs/img_windows_install/img_41.png) + ### Get a Python editor 1. You will need a python editor in order to import the existing Hub source code and to write your own python code. @@ -55,7 +78,7 @@ an excellent open-source python editor. 2. Run the installer, and follow the installation instructions for PyCharm, you may change a few options, but the default ones should be fine. -**NOTE:** If Pycharm asks you to create a Virtual Environment, click **Cancel**. You will do it later using Conda instead. +**NOTE:** If PyCharm asks you to create a Virtual Environment, click **Cancel**. You will do it later using Conda instead. ![creating_virtual_environment](docs/img_windows_install/img_31.png) @@ -70,14 +93,12 @@ You can find it also at **Git->Clone...** ![pycharm get from version control](docs/img_windows_install/img_6.png) -3. Select **Git** as the **Version control**. For the URL use the link to the Hub repository, as seen below. +3. Select **Git** as the **Version control**. Open the [hub repository](https://nextgenerations-cities.encs.concordia.ca/gitea/CERC/hub) +on Gitea and copy the URL from your browser to use as the URL inside PyCharm. ![pycharm get from version control screen](docs/img_windows_install/img_1.png) -(You can also copy this URL by going to the Hub repository in [Gitlab](https://rs-loy-gitlab.concordia.ca/Guille/hub.git) -and clicking on the **Copy URL** button, next to **Clone with HTTPS**) - -![gitlab get https](docs/img_windows_install/img_17.png) +![gitea get https](docs/img_windows_install/img_39.png) The Directory to store the Hub source code locally is automatically created for you. Edit this if you prefer it to be stored somewhere else. @@ -152,7 +173,7 @@ _lca_classes_,... And, click on the **Create** button. 3. Click on the **Git** button in the bottom-left corner to pop-up the window showing the Git information. See your new branch has been created under _Local_. -4. Now we need to let the CERC Gitlab repository know about this new branch. You do this by right-clicking on +4. Now we need to let the CERC Gitea repository know about this new branch. You do this by right-clicking on your branch and selecting **Push...** from the drop-down menu. 5. Then click on the **Push** button at the bottom-right of the **Push Commits** window. @@ -180,33 +201,35 @@ See the picture below. ![pycharm configuration screen](docs/img_windows_install/img_5.png) -## Set up a new project on Gitlab +## Set up a new project on Gitea +You will need an account before you can access the Gitea. Please contact Guillermo (guillermo.gutierrezmorote@concordia.ca) or +Koa (kekoa.wells@concordia.ca) to request an account. -1. Open a browser and to the [CERC Git](https://rs-loy-gitlab.concordia.ca/). Click on the blue **New project** button. +1. Open a browser and go to the [CERC Gitea](https://nextgenerations-cities.encs.concordia.ca/). Click on the **+** in the top right +and select "New Repository" or press the **+** below the Organization tab. -![git new project screen](docs/img_windows_install/img_14.png) +![git new project screen](docs/img_windows_install/img_37.png) 2. Choose the **Create blank project** option from the three options seen below. 3. Type in a name that describes your project: _hp_workflow_, _bus_system_optimization_... (remember to follow the CERC naming conventions described in the [Coding Style](PYGUIDE.md)). -Check the option **Initialize repository with a README**, and ideally, check the **Visibility Level** to be **Public**. +Ideally, uncheck the option **Make Repository Private**, and check the **Initialize Repository** Then click on the **Create project** button. -![git give a name](docs/img_windows_install/img_15.png) +![git give a name](docs/img_windows_install/img_38.png) You should then see a confirmation screen with all the information about your new project. ## Get your project into Pycharm -1. Now you can make a clone of this project, within PyCharm. First, copy the URL by clicking on the blue **Clone** button -and then click on the **Copy URL** button, next to the **Clone with HTTPS** link. +1. Now you can make a clone of this project, within PyCharm. First, go to the page of your repository on the Gitea and copy the URL. 2. Switch back to PyCharm and close the Hub project by choosing **File->Close Project**. You will then see the **Welcome To PyCharm** window again. 3. Clone a copy of your Project into PyCharm, following the steps 2-6 of the _GET THE CERC HUB SOURCE CODE_ -section above, but using the URL link that you just copied for your gitlab project. +section above, but using the URL link that you just copied for your Gitea project. 4. Select **File->Settings** to open the **Settings** window. From the panel on the left click on **Project: -> Project Structure**. @@ -242,5 +265,5 @@ city = GeometryFactory('citygml', path='myfile.gml').city 9. Always remember to push your own project changes as the last thing you do before ending your working day! First, commit your changes by clicking on the green check in the top-right corner of Pycharm. Add a comment that explains briefly your changes. -Then, pull by clicking on the blue arrow to be sure that there are no conflicts between your version (local) and the remote one (gitlab). +Then, pull by clicking on the blue arrow to be sure that there are no conflicts between your version (local) and the remote one (Gitea). Once the conflicts are solved and the merge in local is done, push the changes by clicking on the green arrow. diff --git a/hub/catalog_factories/usage/eilat_catalog.py b/hub/catalog_factories/usage/eilat_catalog.py index 4b70c47c..7318502a 100644 --- a/hub/catalog_factories/usage/eilat_catalog.py +++ b/hub/catalog_factories/usage/eilat_catalog.py @@ -188,7 +188,7 @@ class EilatCatalog(Catalog): schedules_key = {} for j in range(0, number_usage_types): usage_parameters = _extracted_data.iloc[j] - usage_type = usage_parameters[0] + usage_type = usage_parameters.iloc[0] lighting_data[usage_type] = usage_parameters[1:6].values.tolist() plug_loads_data[usage_type] = usage_parameters[8:13].values.tolist() occupancy_data[usage_type] = usage_parameters[17:20].values.tolist() diff --git a/hub/city_model_structure/building.py b/hub/city_model_structure/building.py index 9f907325..5a6e109e 100644 --- a/hub/city_model_structure/building.py +++ b/hub/city_model_structure/building.py @@ -70,6 +70,9 @@ class Building(CityObject): self._min_x = min(self._min_x, surface.lower_corner[0]) self._min_y = min(self._min_y, surface.lower_corner[1]) self._min_z = min(self._min_z, surface.lower_corner[2]) + self._max_x = max(self._max_x, surface.upper_corner[0]) + self._max_y = max(self._max_y, surface.upper_corner[1]) + self._max_z = max(self._max_z, surface.upper_corner[2]) surface.id = surface_id if surface.type == cte.GROUND: self._grounds.append(surface) @@ -440,8 +443,7 @@ class Building(CityObject): """ results = {} if cte.HOUR in self.heating_demand: - monthly_values = PeakLoads().\ - peak_loads_from_hourly(self.heating_demand[cte.HOUR]) + monthly_values = PeakLoads().peak_loads_from_hourly(self.heating_demand[cte.HOUR]) else: monthly_values = PeakLoads(self).heating_peak_loads_from_methodology if monthly_values is None: @@ -682,11 +684,17 @@ class Building(CityObject): for i, value in enumerate(item): _working_hours[key][i] = max(_working_hours[key][i], saved_values[i]) - _total_hours = 0 - for key in _working_hours: - hours = sum(_working_hours[key]) - _total_hours += hours * cte.WEEK_DAYS_A_YEAR[key] - return _total_hours + working_hours = {} + values_months = [] + for month in cte.WEEK_DAYS_A_MONTH.keys(): + _total_hours_month = 0 + for key in _working_hours: + hours = sum(_working_hours[key]) + _total_hours_month += hours * cte.WEEK_DAYS_A_MONTH[month][key] + values_months.append(_total_hours_month) + working_hours[cte.MONTH] = values_months + working_hours[cte.YEAR] = sum(working_hours[cte.MONTH]) + return working_hours @property def distribution_systems_electrical_consumption(self): @@ -735,8 +743,13 @@ class Building(CityObject): for key, item in self._distribution_systems_electrical_consumption.items(): for i in range(0, len(item)): - self._distribution_systems_electrical_consumption[key][i] += _peak_load * _consumption_fix_flow \ - * _working_hours + _working_hours_value = _working_hours[key] + if len(item) == 12: + _working_hours_value = _working_hours[key][i] + self._distribution_systems_electrical_consumption[key][i] += ( + _peak_load * _consumption_fix_flow * _working_hours_value * cte.WATTS_HOUR_TO_JULES + ) + return self._distribution_systems_electrical_consumption def _calculate_consumption(self, consumption_type, demand): @@ -797,3 +810,17 @@ class Building(CityObject): orientation_losses_factor[_key]['south'])] self._onsite_electrical_production[_key] = _results return self._onsite_electrical_production + + @property + def lower_corner(self): + """ + Get building lower corner. + """ + return [self._min_x, self._min_y, self._min_z] + + @property + def upper_corner(self): + """ + Get building upper corner. + """ + return [self._max_x, self._max_y, self._max_z] diff --git a/hub/city_model_structure/building_demand/construction.py b/hub/city_model_structure/building_demand/construction.py index ad82999d..e41d2981 100644 --- a/hub/city_model_structure/building_demand/construction.py +++ b/hub/city_model_structure/building_demand/construction.py @@ -14,6 +14,7 @@ class Construction: """ def __init__(self): self._type = None + self._name = None self._layers = None self._window_ratio = None self._window_frame_ratio = None @@ -37,6 +38,22 @@ class Construction: """ self._type = value + @property + def name(self): + """ + Get construction name + :return: str + """ + return self._name + + @name.setter + def name(self, value): + """ + Set construction name + :param value: str + """ + self._name = value + @property def layers(self) -> [Layer]: """ diff --git a/hub/city_model_structure/building_demand/internal_zone.py b/hub/city_model_structure/building_demand/internal_zone.py index c0494f0a..8bf2c98b 100644 --- a/hub/city_model_structure/building_demand/internal_zone.py +++ b/hub/city_model_structure/building_demand/internal_zone.py @@ -130,6 +130,7 @@ class InternalZone: for hole in surface.holes_polygons: windows_areas.append(hole.area) _thermal_boundary = ThermalBoundary(surface, surface.solid_polygon.area, windows_areas) + surface.associated_thermal_boundaries = [_thermal_boundary] _thermal_boundaries.append(_thermal_boundary) _number_of_storeys = int(self.volume / self.area / self.thermal_archetype.average_storey_height) _thermal_zone = ThermalZone(_thermal_boundaries, self, self.volume, self.area, _number_of_storeys) diff --git a/hub/city_model_structure/building_demand/layer.py b/hub/city_model_structure/building_demand/layer.py index 3075f0bc..eca4f1be 100644 --- a/hub/city_model_structure/building_demand/layer.py +++ b/hub/city_model_structure/building_demand/layer.py @@ -16,7 +16,7 @@ class Layer: def __init__(self): self._thickness = None self._id = None - self._name = None + self._material_name = None self._conductivity = None self._specific_heat = None self._density = None @@ -54,20 +54,20 @@ class Layer: self._thickness = float(value) @property - def name(self): + def material_name(self): """ Get material name :return: str """ - return self._name + return self._material_name - @name.setter - def name(self, value): + @material_name.setter + def material_name(self, value): """ Set material name :param value: string """ - self._name = str(value) + self._material_name = str(value) @property def conductivity(self) -> Union[None, float]: diff --git a/hub/city_model_structure/building_demand/surface.py b/hub/city_model_structure/building_demand/surface.py index 7fbef20d..2cd42755 100644 --- a/hub/city_model_structure/building_demand/surface.py +++ b/hub/city_model_structure/building_demand/surface.py @@ -18,6 +18,7 @@ from hub.city_model_structure.attributes.point import Point from hub.city_model_structure.greenery.vegetation import Vegetation from hub.city_model_structure.building_demand.thermal_boundary import ThermalBoundary import hub.helpers.constants as cte +from hub.helpers.configuration_helper import ConfigurationHelper class Surface: @@ -154,7 +155,6 @@ class Surface: if self._inclination is None: self._inclination = np.arccos(self.perimeter_polygon.normal[2]) return self._inclination - @property def type(self): """ diff --git a/hub/city_model_structure/building_demand/thermal_boundary.py b/hub/city_model_structure/building_demand/thermal_boundary.py index 66746037..1ff00244 100644 --- a/hub/city_model_structure/building_demand/thermal_boundary.py +++ b/hub/city_model_structure/building_demand/thermal_boundary.py @@ -256,6 +256,19 @@ class ThermalBoundary: raise TypeError('Constructions layers are not initialized') from TypeError return self._u_value + @property + def construction_name(self): + """ + Get construction name + :return: str + """ + if self._construction_archetype is not None: + self._construction_name = self._construction_archetype.name + else: + logging.error('Construction name not defined\n') + raise ValueError('Construction name not defined') + return self._construction_name + @u_value.setter def u_value(self, value): """ diff --git a/hub/city_model_structure/building_demand/thermal_control.py b/hub/city_model_structure/building_demand/thermal_control.py index d939c4eb..686a4e1d 100644 --- a/hub/city_model_structure/building_demand/thermal_control.py +++ b/hub/city_model_structure/building_demand/thermal_control.py @@ -4,6 +4,7 @@ SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2022 Concordia CERC group Project Coder Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ +from math import inf from typing import Union, List from hub.city_model_structure.attributes.schedule import Schedule @@ -22,20 +23,16 @@ class ThermalControl: @staticmethod def _maximum_value(schedules): - maximum = -1000 + maximum = -inf for schedule in schedules: - for value in schedule.values: - if value > maximum: - maximum = value + maximum = max(maximum, max(schedule.values)) return maximum @staticmethod def _minimum_value(schedules): - minimum = 1000 + minimum = inf for schedule in schedules: - for value in schedule.values: - if value < minimum: - minimum = value + minimum = min(minimum, min(schedule.values)) return minimum @property diff --git a/hub/city_model_structure/city.py b/hub/city_model_structure/city.py index a723b56c..97000fbb 100644 --- a/hub/city_model_structure/city.py +++ b/hub/city_model_structure/city.py @@ -14,6 +14,7 @@ import math import pickle import sys import pathlib +import os from pathlib import Path from typing import List, Union @@ -101,7 +102,7 @@ class City: Get city location :return: Location """ - return self._get_location().city + return self._get_location() @property def name(self): @@ -113,6 +114,15 @@ class City: return self._get_location().city return self._name + @name.setter + def name(self, value): + """ + Set city name + :param value:str + """ + if value is not None: + self._name = str(value) + @property def climate_reference_city(self) -> Union[None, str]: """ @@ -275,15 +285,6 @@ class City: """ return self._srs_name - @name.setter - def name(self, value): - """ - Set city name - :param value:str - """ - if value is not None: - self._name = str(value) - @staticmethod def load(city_filename) -> City: """ @@ -299,6 +300,20 @@ class City: with open(city_filename, 'rb') as file: return pickle.load(file) + @staticmethod + def load_compressed(compressed_city_filename, destination_filename) -> City: + """ + Load a city from compressed_city_filename + :param compressed_city_filename: Compressed pickle as source + :param destination_filename: Pickle file as destination + :return: City + """ + with open(str(compressed_city_filename), 'rb') as source, open(str(destination_filename), 'wb') as destination: + destination.write(bz2.decompress(source.read())) + loaded_city = City.load(destination_filename) + os.unlink(destination_filename) + return loaded_city + def save(self, city_filename): """ Save a city into the given filename diff --git a/hub/docs/img_windows_install/img_34.png b/hub/docs/img_windows_install/img_34.png new file mode 100644 index 00000000..a8d8a99d Binary files /dev/null and b/hub/docs/img_windows_install/img_34.png differ diff --git a/hub/docs/img_windows_install/img_35.png b/hub/docs/img_windows_install/img_35.png new file mode 100644 index 00000000..067b226e Binary files /dev/null and b/hub/docs/img_windows_install/img_35.png differ diff --git a/hub/docs/img_windows_install/img_36.png b/hub/docs/img_windows_install/img_36.png new file mode 100644 index 00000000..975eaf9b Binary files /dev/null and b/hub/docs/img_windows_install/img_36.png differ diff --git a/hub/docs/img_windows_install/img_37.png b/hub/docs/img_windows_install/img_37.png new file mode 100644 index 00000000..cb0679c0 Binary files /dev/null and b/hub/docs/img_windows_install/img_37.png differ diff --git a/hub/docs/img_windows_install/img_38.png b/hub/docs/img_windows_install/img_38.png new file mode 100644 index 00000000..5ea1783a Binary files /dev/null and b/hub/docs/img_windows_install/img_38.png differ diff --git a/hub/docs/img_windows_install/img_39.png b/hub/docs/img_windows_install/img_39.png new file mode 100644 index 00000000..0463d4e6 Binary files /dev/null and b/hub/docs/img_windows_install/img_39.png differ diff --git a/hub/docs/img_windows_install/img_40.png b/hub/docs/img_windows_install/img_40.png new file mode 100644 index 00000000..d01ff5b7 Binary files /dev/null and b/hub/docs/img_windows_install/img_40.png differ diff --git a/hub/docs/img_windows_install/img_41.png b/hub/docs/img_windows_install/img_41.png new file mode 100644 index 00000000..d77b8c2a Binary files /dev/null and b/hub/docs/img_windows_install/img_41.png differ diff --git a/hub/exports/building_energy/idf.py b/hub/exports/building_energy/idf.py index 02de155e..9ebf99f3 100644 --- a/hub/exports/building_energy/idf.py +++ b/hub/exports/building_energy/idf.py @@ -12,6 +12,7 @@ from geomeppy import IDF import hub.helpers.constants as cte from hub.city_model_structure.attributes.schedule import Schedule from hub.city_model_structure.building_demand.thermal_zone import ThermalZone +from hub.helpers.configuration_helper import ConfigurationHelper class Idf: @@ -55,7 +56,6 @@ class Idf: _SIMPLE = 'Simple' idf_surfaces = { - # todo: make an enum for all the surface types cte.WALL: 'wall', cte.GROUND: 'floor', cte.ROOF: 'roof' @@ -148,28 +148,28 @@ class Idf: def _add_material(self, layer): for material in self._idf.idfobjects[self._MATERIAL]: - if material.Name == layer.material.name: + if material.Name == layer.material_name: return for material in self._idf.idfobjects[self._MATERIAL_NOMASS]: - if material.Name == layer.material.name: + if material.Name == layer.material_name: return - if layer.material.no_mass: + if layer.no_mass: self._idf.newidfobject(self._MATERIAL_NOMASS, - Name=layer.material.name, + Name=layer.material_name, Roughness=self._ROUGHNESS, - Thermal_Resistance=layer.material.thermal_resistance + Thermal_Resistance=layer.thermal_resistance ) else: self._idf.newidfobject(self._MATERIAL, - Name=layer.material.name, + Name=layer.material_name, Roughness=self._ROUGHNESS, Thickness=layer.thickness, - Conductivity=layer.material.conductivity, - Density=layer.material.density, - Specific_Heat=layer.material.specific_heat, - Thermal_Absorptance=layer.material.thermal_absorptance, - Solar_Absorptance=layer.material.solar_absorptance, - Visible_Absorptance=layer.material.visible_absorptance + Conductivity=layer.conductivity, + Density=layer.density, + Specific_Heat=layer.specific_heat, + Thermal_Absorptance=layer.thermal_absorptance, + Solar_Absorptance=layer.solar_absorptance, + Visible_Absorptance=layer.visible_absorptance ) @staticmethod @@ -338,11 +338,11 @@ class Idf: _kwargs = {'Name': vegetation_name, 'Outside_Layer': thermal_boundary.parent_surface.vegetation.name} for i in range(0, len(layers) - 1): - _kwargs[f'Layer_{i + 2}'] = layers[i].material.name + _kwargs[f'Layer_{i + 2}'] = layers[i].material_name else: - _kwargs = {'Name': thermal_boundary.construction_name, 'Outside_Layer': layers[0].material.name} + _kwargs = {'Name': thermal_boundary.construction_name, 'Outside_Layer': layers[0].material_name} for i in range(1, len(layers) - 1): - _kwargs[f'Layer_{i + 1}'] = layers[i].material.name + _kwargs[f'Layer_{i + 1}'] = layers[i].material_name self._idf.newidfobject(self._CONSTRUCTION, **_kwargs) def _add_window_construction_and_material(self, thermal_opening): @@ -512,12 +512,12 @@ class Idf: self._rename_building(self._city.name) self._lod = self._city.level_of_detail.geometry for building in self._city.buildings: - print('building name', building.name) for internal_zone in building.internal_zones: if internal_zone.thermal_zones_from_internal_zones is None: continue for thermal_zone in internal_zone.thermal_zones_from_internal_zones: for thermal_boundary in thermal_zone.thermal_boundaries: + self._add_construction(thermal_boundary) if thermal_boundary.parent_surface.vegetation is not None: self._add_vegetation_material(thermal_boundary.parent_surface.vegetation) @@ -560,7 +560,7 @@ class Idf: self._add_dhw(thermal_zone, building.name) if self._export_type == "Surfaces": if building.name in self._target_buildings or building.name in self._adjacent_buildings: - if building.internal_zones[0].thermal_zones_from_internal_zones is not None: + if building.thermal_zones_from_internal_zones is not None: self._add_surfaces(building, building.name) else: self._add_pure_geometry(building, building.name) @@ -633,6 +633,8 @@ class Idf: self._city.lower_corner) shading.setcoords(coordinates) solar_reflectance = surface.short_wave_reflectance + if solar_reflectance is None: + solar_reflectance = ConfigurationHelper().short_wave_reflectance self._idf.newidfobject(self._SHADING_PROPERTY, Shading_Surface_Name=f'{surface.name}', Diffuse_Solar_Reflectance_of_Unglazed_Part_of_Shading_Surface=solar_reflectance, @@ -677,41 +679,40 @@ class Idf: self._idf.set_wwr(wwr) def _add_surfaces(self, building, zone_name): - for internal_zone in building.internal_zones: - for thermal_zone in internal_zone.thermal_zones_from_internal_zones: - for boundary in thermal_zone.thermal_boundaries: - idf_surface_type = self.idf_surfaces[boundary.parent_surface.type] - outside_boundary_condition = 'Outdoors' - sun_exposure = 'SunExposed' - wind_exposure = 'WindExposed' - _kwargs = {'Name': f'{boundary.parent_surface.name}', - 'Surface_Type': idf_surface_type, - 'Zone_Name': zone_name} - if boundary.parent_surface.type == cte.GROUND: - outside_boundary_condition = 'Ground' - sun_exposure = 'NoSun' - wind_exposure = 'NoWind' - if boundary.parent_surface.percentage_shared is not None and boundary.parent_surface.percentage_shared > 0.5: - outside_boundary_condition = 'Surface' - outside_boundary_condition_object = boundary.parent_surface.name - sun_exposure = 'NoSun' - wind_exposure = 'NoWind' - _kwargs['Outside_Boundary_Condition_Object'] = outside_boundary_condition_object - _kwargs['Outside_Boundary_Condition'] = outside_boundary_condition - _kwargs['Sun_Exposure'] = sun_exposure - _kwargs['Wind_Exposure'] = wind_exposure + for thermal_zone in building.thermal_zones_from_internal_zones: + for boundary in thermal_zone.thermal_boundaries: + idf_surface_type = self.idf_surfaces[boundary.parent_surface.type] + outside_boundary_condition = 'Outdoors' + sun_exposure = 'SunExposed' + wind_exposure = 'WindExposed' + _kwargs = {'Name': f'{boundary.parent_surface.name}', + 'Surface_Type': idf_surface_type, + 'Zone_Name': zone_name} + if boundary.parent_surface.type == cte.GROUND: + outside_boundary_condition = 'Ground' + sun_exposure = 'NoSun' + wind_exposure = 'NoWind' + if boundary.parent_surface.percentage_shared is not None and boundary.parent_surface.percentage_shared > 0.5: + outside_boundary_condition = 'Surface' + outside_boundary_condition_object = boundary.parent_surface.name + sun_exposure = 'NoSun' + wind_exposure = 'NoWind' + _kwargs['Outside_Boundary_Condition_Object'] = outside_boundary_condition_object + _kwargs['Outside_Boundary_Condition'] = outside_boundary_condition + _kwargs['Sun_Exposure'] = sun_exposure + _kwargs['Wind_Exposure'] = wind_exposure - if boundary.parent_surface.vegetation is not None: - construction_name = f'{boundary.construction_name}_{boundary.parent_surface.vegetation.name}' - else: - construction_name = boundary.construction_name - _kwargs['Construction_Name'] = construction_name + if boundary.parent_surface.vegetation is not None: + construction_name = f'{boundary.construction_name}_{boundary.parent_surface.vegetation.name}' + else: + construction_name = boundary.construction_name + _kwargs['Construction_Name'] = construction_name - surface = self._idf.newidfobject(self._SURFACE, **_kwargs) + surface = self._idf.newidfobject(self._SURFACE, **_kwargs) - coordinates = self._matrix_to_list(boundary.parent_surface.solid_polygon.coordinates, - self._city.lower_corner) - surface.setcoords(coordinates) + coordinates = self._matrix_to_list(boundary.parent_surface.solid_polygon.coordinates, + self._city.lower_corner) + surface.setcoords(coordinates) if self._lod >= 3: for internal_zone in building.internal_zones: diff --git a/hub/exports/building_energy/insel/insel_monthly_energy_balance.py b/hub/exports/building_energy/insel/insel_monthly_energy_balance.py index e6f77068..f14ec946 100644 --- a/hub/exports/building_energy/insel/insel_monthly_energy_balance.py +++ b/hub/exports/building_energy/insel/insel_monthly_energy_balance.py @@ -42,6 +42,14 @@ class InselMonthlyEnergyBalance: self._insel_files_paths.append(building.name + '.insel') file_name_out = building.name + '.out' output_path = Path(self._path / file_name_out).resolve() + skip_building = False + for internal_zone in building.internal_zones: + if internal_zone.thermal_archetype is None: + logging.warning('Building %s has missing values. Monthly Energy Balance cannot be processed', building.name) + skip_building = True + break + if skip_building: + continue if building.thermal_zones_from_internal_zones is None: logging.warning('Building %s has missing values. Monthly Energy Balance cannot be processed', building.name) diff --git a/hub/exports/exports_factory.py b/hub/exports/exports_factory.py index 4b7216ea..a0bcf805 100644 --- a/hub/exports/exports_factory.py +++ b/hub/exports/exports_factory.py @@ -7,9 +7,12 @@ Project Coder Guille Gutierrez guillermo.gutierrezmorote@concordia.ca from pathlib import Path +from hub.exports.formats.glb import Glb from hub.exports.formats.obj import Obj +from hub.exports.formats.geojson import Geojson from hub.exports.formats.simplified_radiosity_algorithm import SimplifiedRadiosityAlgorithm from hub.exports.formats.stl import Stl +from hub.exports.formats.cesiumjs_tileset import CesiumjsTileset from hub.helpers.utils import validate_import_export_type @@ -17,7 +20,7 @@ class ExportsFactory: """ Exports factory class """ - def __init__(self, handler, city, path, target_buildings=None, adjacent_buildings=None): + def __init__(self, handler, city, path, target_buildings=None, adjacent_buildings=None, base_uri=None): self._city = city self._handler = '_' + handler.lower() validate_import_export_type(ExportsFactory, handler) @@ -26,18 +29,7 @@ class ExportsFactory: self._path = path self._target_buildings = target_buildings self._adjacent_buildings = adjacent_buildings - - @property - def _citygml(self): - """ - Export to citygml - :return: None - """ - raise NotImplementedError - - @property - def _collada(self): - raise NotImplementedError + self._base_uri = base_uri @property def _stl(self): @@ -61,9 +53,30 @@ class ExportsFactory: Export the city to Simplified Radiosity Algorithm xml format :return: None """ - return SimplifiedRadiosityAlgorithm(self._city, - (self._path / f'{self._city.name}_sra.xml'), - target_buildings=self._target_buildings) + return SimplifiedRadiosityAlgorithm( + self._city, (self._path / f'{self._city.name}_sra.xml'), target_buildings=self._target_buildings + ) + + @property + def _cesiumjs_tileset(self): + """ + Export the city to a cesiumJs tileset format + :return: None + """ + return CesiumjsTileset( + self._city, + (self._path / f'{self._city.name}.json'), + target_buildings=self._target_buildings, + base_uri=self._base_uri + ) + + @property + def _glb(self): + return Glb(self._city, self._path, target_buildings=self._target_buildings) + + @property + def _geojson(self): + return Geojson(self._city, self._path, target_buildings=self._target_buildings) def export(self): """ diff --git a/hub/exports/formats/cesiumjs_tileset.py b/hub/exports/formats/cesiumjs_tileset.py new file mode 100644 index 00000000..df1adb52 --- /dev/null +++ b/hub/exports/formats/cesiumjs_tileset.py @@ -0,0 +1,159 @@ +""" +export a city into Cesium tileset format +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2022 Concordia CERC group +Project Coder Guille Gutierrez guillermo.gutierrezmorote@concordia.ca +""" +import json +import math + +import pyproj +from pyproj import Transformer + +from hub.helpers.geometry_helper import GeometryHelper + + +class CesiumjsTileset: + def __init__(self, city, file_name, target_buildings=None, base_uri=None): + self._city = city + self._file_name = file_name + self._target_buildings = target_buildings + if base_uri is None: + base_uri = '.' + self._base_uri = base_uri + try: + srs_name = self._city.srs_name + if self._city.srs_name in GeometryHelper.srs_transformations: + srs_name = GeometryHelper.srs_transformations[self._city.srs_name] + input_reference = pyproj.CRS(srs_name) # Projected coordinate system from input data + except pyproj.exceptions.CRSError as err: + raise pyproj.exceptions.CRSError from err + self._to_gps = Transformer.from_crs(input_reference, pyproj.CRS('EPSG:4326')) + city_upper_corner = [ + self._city.upper_corner[0] - self._city.lower_corner[0], + self._city.upper_corner[1] - self._city.lower_corner[1], + self._city.upper_corner[2] - self._city.lower_corner[2] + ] + city_lower_corner = [0, 0, 0] + self._tile_set = { + 'asset': { + 'version': '1.1', + "tilesetVersion": "1.2.3" + }, + 'position': self._to_gps.transform(self._city.lower_corner[0], self._city.lower_corner[1]), + 'schema': { + 'id': "building", + 'classes': { + 'building': { + "properties": { + 'name': { + 'type': 'STRING' + }, + 'position': { + 'type': 'SCALAR', + 'array': True, + 'componentType': 'FLOAT32' + }, + 'aliases': { + 'type': 'STRING', + 'array': True, + }, + 'volume': { + 'type': 'SCALAR', + 'componentType': 'FLOAT32' + }, + 'floor_area': { + 'type': 'SCALAR', + 'componentType': 'FLOAT32' + }, + 'max_height': { + 'type': 'SCALAR', + 'componentType': 'INT32' + }, + 'year_of_construction': { + 'type': 'SCALAR', + 'componentType': 'INT32' + }, + 'function': { + 'type': 'STRING' + }, + 'usages_percentage': { + 'type': 'STRING' + } + } + } + } + }, + 'geometricError': 500, + 'root': { + 'boundingVolume': { + 'box': CesiumjsTileset._box_values(city_upper_corner, city_lower_corner) + }, + 'geometricError': 70, + 'refine': 'ADD', + 'children': [] + } + } + + self._export() + + @staticmethod + def _box_values(upper_corner, lower_corner): + + x = (upper_corner[0] - lower_corner[0]) / 2 + x_center = ((upper_corner[0] - lower_corner[0]) / 2) + lower_corner[0] + y = (upper_corner[1] - lower_corner[1]) / 2 + y_center = ((upper_corner[1] - lower_corner[1]) / 2) + lower_corner[1] + z = (upper_corner[2] - lower_corner[2]) / 2 + return [x_center, y_center, z, x, 0, 0, 0, y, 0, 0, 0, z] + + def _ground_coordinates(self, coordinates): + ground_coordinates = [] + for coordinate in coordinates: + ground_coordinates.append( + (coordinate[0] - self._city.lower_corner[0], coordinate[1] - self._city.lower_corner[1]) + ) + return ground_coordinates + + def _export(self): + for building in self._city.buildings: + upper_corner = [-math.inf, -math.inf, 0] + lower_corner = [math.inf, math.inf, 0] + lower_corner_coordinates = lower_corner + for surface in building.grounds: # todo: maybe we should add the terrain? + coordinates = self._ground_coordinates(surface.solid_polygon.coordinates) + lower_corner = [min([c[0] for c in coordinates]), min([c[1] for c in coordinates]), 0] + lower_corner_coordinates = [ + min([c[0] for c in surface.solid_polygon.coordinates]), + min([c[1] for c in surface.solid_polygon.coordinates]), + 0 + ] + upper_corner = [max([c[0] for c in coordinates]), max([c[1] for c in coordinates]), building.max_height] + + tile = { + 'boundingVolume': { + 'box': CesiumjsTileset._box_values(upper_corner, lower_corner) + }, + 'geometricError': 250, + 'metadata': { + 'class': 'building', + 'properties': { + 'name': building.name, + 'position': self._to_gps.transform(lower_corner_coordinates[0], lower_corner_coordinates[1]), + 'aliases': building.aliases, + 'volume': building.volume, + 'floor_area': building.floor_area, + 'max_height': building.max_height, + 'year_of_construction': building.year_of_construction, + 'function': building.function, + 'usages_percentage': building.usages_percentage + } + }, + 'content': { + 'uri': f'{self._base_uri}/{building.name}.glb' + } + } + self._tile_set['root']['children'].append(tile) + + with open(self._file_name, 'w') as f: + json.dump(self._tile_set, f, indent=2) diff --git a/hub/exports/formats/geojson.py b/hub/exports/formats/geojson.py new file mode 100644 index 00000000..d9ef7dae --- /dev/null +++ b/hub/exports/formats/geojson.py @@ -0,0 +1,112 @@ +""" +export a city into Geojson format +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2022 Concordia CERC group +Project Coder Guille Gutierrez guillermo.gutierrezmorote@concordia.ca +""" +import json +from pathlib import Path + +import numpy as np +import pyproj +from pyproj import Transformer + +from hub.helpers.geometry_helper import GeometryHelper + + +class Geojson: + """ + Export to geojson format + """ + def __init__(self, city, path, target_buildings): + self._city = city + self._file_path = Path(path / f'{self._city.name}.geojson').resolve() + try: + srs_name = self._city.srs_name + if self._city.srs_name in GeometryHelper.srs_transformations: + srs_name = GeometryHelper.srs_transformations[self._city.srs_name] + input_reference = pyproj.CRS(srs_name) # Projected coordinate system from input data + except pyproj.exceptions.CRSError as err: + raise pyproj.exceptions.CRSError from err + self._to_gps = Transformer.from_crs(input_reference, pyproj.CRS('EPSG:4326')) + if target_buildings is None: + target_buildings = [b.name for b in self._city.buildings] + self._geojson_skeleton = { + 'type': 'FeatureCollection', + 'features': [] + } + self._feature_skeleton = { + 'type': 'Feature', + 'geometry': { + 'type': 'Polygon', + 'coordinates': [] + }, + 'properties': {} + } + self._export() + + def _export(self): + for building in self._city.buildings: + if len(building.grounds) == 1: + ground = building.grounds[0] + feature = self._polygon(ground) + else: + feature = self._multipolygon(building.grounds) + feature['id'] = building.name + feature['properties']['height'] = f'{building.max_height - building.lower_corner[2]}' + feature['properties']['function'] = f'{building.function}' + feature['properties']['year_of_construction'] = f'{building.year_of_construction}' + feature['properties']['aliases'] = building.aliases + feature['properties']['elevation'] = f'{building.lower_corner[2]}' + self._geojson_skeleton['features'].append(feature) + with open(self._file_path, 'w', encoding='utf-8') as f: + json.dump(self._geojson_skeleton, f, indent=2) + + def _polygon(self, ground): + feature = { + 'type': 'Feature', + 'geometry': { + 'type': 'Polygon', + 'coordinates': [] + }, + 'properties': {} + } + ground_coordinates = [] + for coordinate in ground.solid_polygon.coordinates: + gps_coordinate = self._to_gps.transform(coordinate[0], coordinate[1]) + ground_coordinates.insert(0, [gps_coordinate[1], gps_coordinate[0]]) + + first_gps_coordinate = self._to_gps.transform( + ground.solid_polygon.coordinates[0][0], + ground.solid_polygon.coordinates[0][1] + ) + ground_coordinates.insert(0, [first_gps_coordinate[1], first_gps_coordinate[0]]) + feature['geometry']['coordinates'].append(ground_coordinates) + return feature + + def _multipolygon(self, grounds): + feature = { + 'type': 'Feature', + 'geometry': { + 'type': 'MultiPolygon', + 'coordinates': [] + }, + 'properties': {} + } + polygons = [] + for ground in grounds: + ground_coordinates = [] + for coordinate in ground.solid_polygon.coordinates: + gps_coordinate = self._to_gps.transform(coordinate[0], coordinate[1]) + ground_coordinates.insert(0, [gps_coordinate[1], gps_coordinate[0]]) + + first_gps_coordinate = self._to_gps.transform( + ground.solid_polygon.coordinates[0][0], + ground.solid_polygon.coordinates[0][1] + ) + ground_coordinates.insert(0, [first_gps_coordinate[1], first_gps_coordinate[0]]) + polygons.append(ground_coordinates) + feature['geometry']['coordinates'].append(polygons) + return feature + + diff --git a/hub/exports/formats/glb.py b/hub/exports/formats/glb.py new file mode 100644 index 00000000..1ac5570b --- /dev/null +++ b/hub/exports/formats/glb.py @@ -0,0 +1,54 @@ +""" +export a city into Glb format +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2022 Concordia CERC group +Project Coder Guille Gutierrez guillermo.gutierrezmorote@concordia.ca +""" +import os +import shutil +import subprocess + +from hub.city_model_structure.city import City +from hub.exports.formats.obj import Obj + + +class GltExceptionError(Exception): + """ + Glt execution error + """ + + +class Glb: + """ + Glb class + """ + def __init__(self, city, path, target_buildings=None): + self._city = city + self._path = path + if target_buildings is None: + target_buildings = [b.name for b in self._city.buildings] + self._target_buildings = target_buildings + self._export() + + @property + def _obj2gltf(self): + return shutil.which('obj2gltf') + + def _export(self): + try: + for building in self._city.buildings: + city = City(self._city.lower_corner, self._city.upper_corner, self._city.srs_name) + city.add_city_object(building) + city.name = building.name + Obj(city, self._path) + glb = f'{self._path}/{building.name}.glb' + subprocess.run([ + self._obj2gltf, + '-i', f'{self._path}/{building.name}.obj', + '-b', + '-o', f'{glb}' + ]) + os.unlink(f'{self._path}/{building.name}.obj') + os.unlink(f'{self._path}/{building.name}.mtl') + except (subprocess.SubprocessError, subprocess.TimeoutExpired, subprocess.CalledProcessError) as err: + raise GltExceptionError from err diff --git a/hub/exports/formats/obj.py b/hub/exports/formats/obj.py index 2d0776d6..d212740c 100644 --- a/hub/exports/formats/obj.py +++ b/hub/exports/formats/obj.py @@ -4,9 +4,10 @@ SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2022 Concordia CERC group Project Coder Guille Gutierrez guillermo.gutierrezmorote@concordia.ca """ - from pathlib import Path +import numpy as np + class Obj: """ @@ -17,29 +18,64 @@ class Obj: self._path = path self._export() - def _to_vertex(self, coordinate): + def _ground(self, coordinate): x = coordinate[0] - self._city.lower_corner[0] y = coordinate[1] - self._city.lower_corner[1] z = coordinate[2] - self._city.lower_corner[2] - return f'v {x} {y} {z}\n' + return x, y, z + + def _to_vertex(self, coordinate): + x, y, z = self._ground(coordinate) + return f'v {x} {z} -{y}\n' # to match opengl expectations + + def _to_texture_vertex(self, coordinate): + u, v, _ = self._ground(coordinate) + return f'vt {u} {v}\n' + + def _to_normal_vertex(self, coordinates): + ground_vertex = [] + for coordinate in coordinates: + x, y, z = self._ground(coordinate) + ground_vertex.append(np.array([x, y, z])) + # recalculate the normal to get grounded values + edge_1 = ground_vertex[1] - ground_vertex[0] + edge_2 = ground_vertex[2] - ground_vertex[0] + normal = np.cross(edge_1, edge_2) + normal = normal / np.linalg.norm(normal) + return f'vn {normal[0]} {normal[1]} {normal[2]}\n' def _export(self): if self._city.name is None: self._city.name = 'unknown_city' - file_name = self._city.name + '.obj' - file_path = (Path(self._path).resolve() / file_name).resolve() + obj_name = f'{self._city.name}.obj' + mtl_name = f'{self._city.name}.mtl' + obj_file_path = (Path(self._path).resolve() / obj_name).resolve() + mtl_file_path = (Path(self._path).resolve() / mtl_name).resolve() + with open(mtl_file_path, 'w', encoding='utf-8') as mtl: + mtl.write("newmtl cerc_base_material\n") + mtl.write("Ka 1.0 1.0 1.0 # Ambient color (white)\n") + mtl.write("Kd 0.1 0.3 0.1 # Diffuse color (greenish)\n") + mtl.write("Ks 1.0 1.0 1.0 # Specular color (white)\n") + mtl.write("Ns 400.0 # Specular exponent (defines shininess)\n") vertices = {} - with open(file_path, 'w', encoding='utf-8') as obj: + faces = [] + vertex_index = 0 + normal_index = 0 + with open(obj_file_path, 'w', encoding='utf-8') as obj: obj.write("# cerc-hub export\n") - vertex_index = 0 - faces = [] + obj.write(f'mtllib {mtl_name}\n') + for building in self._city.buildings: obj.write(f'# building {building.name}\n') obj.write(f'g {building.name}\n') obj.write('s off\n') + for surface in building.surfaces: obj.write(f'# surface {surface.name}\n') - face = 'f ' + face = [] + normal = self._to_normal_vertex(surface.perimeter_polygon.coordinates) + normal_index += 1 + textures = [] for coordinate in surface.perimeter_polygon.coordinates: vertex = self._to_vertex(coordinate) if vertex not in vertices: @@ -47,11 +83,13 @@ class Obj: vertices[vertex] = vertex_index current = vertex_index obj.write(vertex) + textures.append(self._to_texture_vertex(coordinate)) # only append if non-existing else: current = vertices[vertex] + face.append(f'{current}/{current}/{normal_index}') # insert clockwise + obj.writelines(normal) # add the normal + obj.writelines(textures) # add the texture vertex - face = f'{face} {current}' - - faces.append(f'{face} {face.split(" ")[1]}\n') + faces.append(f"f {' '.join(face)}\n") obj.writelines(faces) faces = [] diff --git a/hub/helpers/constants.py b/hub/helpers/constants.py index 7cd6151e..c2d9d642 100644 --- a/hub/helpers/constants.py +++ b/hub/helpers/constants.py @@ -49,36 +49,140 @@ WEEK_DAYS = 'Weekdays' WEEK_ENDS = 'Weekends' ALL_DAYS = 'Alldays' -WEEK_DAYS_A_MONTH = {'monday': [5, 4, 4, 5, 4, 4, 5, 4, 4, 5, 4, 5], - 'tuesday': [5, 4, 4, 4, 5, 4, 5, 4, 4, 5, 4, 4], - 'wednesday': [5, 4, 4, 4, 5, 4, 4, 5, 4, 5, 4, 4], - 'thursday': [4, 4, 5, 4, 5, 4, 4, 5, 4, 4, 5, 4], - 'friday': [4, 4, 5, 4, 4, 5, 4, 5, 4, 4, 5, 4], - 'saturday': [4, 4, 5, 4, 4, 5, 4, 4, 5, 4, 4, 5], - 'sunday': [4, 4, 4, 5, 4, 4, 5, 4, 5, 4, 4, 5], - 'holiday': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]} +JANUARY = 'January' +FEBRUARY = 'February' +MARCH = 'March' +APRIL = 'April' +MAY = 'May' +JUNE = 'June' +JULY = 'July' +AUGUST = 'August' +SEPTEMBER = 'September' +OCTOBER = 'October' +NOVEMBER = 'November' +DECEMBER = 'December' -WEEK_DAYS_A_YEAR = {'monday': 51, - 'tuesday': 50, - 'wednesday': 50, - 'thursday': 50, - 'friday': 50, - 'saturday': 52, - 'sunday': 52, - 'holiday': 10} +MONTHS = [JANUARY, FEBRUARY, MARCH, APRIL, MAY, JUNE, JULY, AUGUST, SEPTEMBER, OCTOBER, NOVEMBER, DECEMBER] -DAYS_A_MONTH = {'January': 31, - 'February': 28, - 'March': 31, - 'April': 30, - 'May': 31, - 'June': 30, - 'July': 31, - 'August': 31, - 'September': 30, - 'October': 31, - 'November': 30, - 'December': 31} +WEEK_DAYS_A_MONTH = {JANUARY: {MONDAY: 5, + TUESDAY: 5, + WEDNESDAY: 5, + THURSDAY: 4, + FRIDAY: 4, + SATURDAY: 4, + SUNDAY: 4, + HOLIDAY: 0}, + FEBRUARY: {MONDAY: 4, + TUESDAY: 4, + WEDNESDAY: 4, + THURSDAY: 4, + FRIDAY: 4, + SATURDAY: 4, + SUNDAY: 4, + HOLIDAY: 0}, + MARCH: {MONDAY: 4, + TUESDAY: 4, + WEDNESDAY: 4, + THURSDAY: 5, + FRIDAY: 5, + SATURDAY: 5, + SUNDAY: 4, + HOLIDAY: 0}, + APRIL: {MONDAY: 5, + TUESDAY: 4, + WEDNESDAY: 4, + THURSDAY: 4, + FRIDAY: 4, + SATURDAY: 4, + SUNDAY: 5, + HOLIDAY: 0}, + MAY: {MONDAY: 4, + TUESDAY: 5, + WEDNESDAY: 5, + THURSDAY: 5, + FRIDAY: 4, + SATURDAY: 4, + SUNDAY: 4, + HOLIDAY: 0}, + JUNE: {MONDAY: 4, + TUESDAY: 4, + WEDNESDAY: 4, + THURSDAY: 4, + FRIDAY: 5, + SATURDAY: 5, + SUNDAY: 4, + HOLIDAY: 0}, + JULY: {MONDAY: 5, + TUESDAY: 5, + WEDNESDAY: 4, + THURSDAY: 4, + FRIDAY: 4, + SATURDAY: 4, + SUNDAY: 5, + HOLIDAY: 0}, + AUGUST: {MONDAY: 4, + TUESDAY: 4, + WEDNESDAY: 5, + THURSDAY: 5, + FRIDAY: 5, + SATURDAY: 4, + SUNDAY: 4, + HOLIDAY: 0}, + SEPTEMBER: {MONDAY: 4, + TUESDAY: 4, + WEDNESDAY: 4, + THURSDAY: 4, + FRIDAY: 4, + SATURDAY: 5, + SUNDAY: 5, + HOLIDAY: 0}, + OCTOBER: {MONDAY: 5, + TUESDAY: 5, + WEDNESDAY: 5, + THURSDAY: 4, + FRIDAY: 4, + SATURDAY: 4, + SUNDAY: 4, + HOLIDAY: 0}, + NOVEMBER: {MONDAY: 4, + TUESDAY: 4, + WEDNESDAY: 4, + THURSDAY: 5, + FRIDAY: 5, + SATURDAY: 4, + SUNDAY: 4, + HOLIDAY: 0}, + DECEMBER: {MONDAY: 5, + TUESDAY: 4, + WEDNESDAY: 4, + THURSDAY: 4, + FRIDAY: 4, + SATURDAY: 5, + SUNDAY: 5, + HOLIDAY: 0}, + } + +WEEK_DAYS_A_YEAR = {MONDAY: 51, + TUESDAY: 50, + WEDNESDAY: 50, + THURSDAY: 50, + FRIDAY: 50, + SATURDAY: 52, + SUNDAY: 52, + HOLIDAY: 10} + +DAYS_A_MONTH = {JANUARY: 31, + FEBRUARY: 28, + MARCH: 31, + APRIL: 30, + MAY: 31, + JUNE: 30, + JULY: 31, + AUGUST: 31, + SEPTEMBER: 30, + OCTOBER: 31, + NOVEMBER: 30, + DECEMBER: 31} # data types ANY_NUMBER = 'any_number' diff --git a/hub/helpers/data/hub_function_to_eilat_construction_function.py b/hub/helpers/data/hub_function_to_eilat_construction_function.py index 2e8cebc6..e978ac32 100644 --- a/hub/helpers/data/hub_function_to_eilat_construction_function.py +++ b/hub/helpers/data/hub_function_to_eilat_construction_function.py @@ -16,7 +16,9 @@ class HubFunctionToEilatConstructionFunction: self._dictionary = { cte.RESIDENTIAL: 'Residential_building', cte.HOTEL: 'Residential_building', - cte.DORMITORY: 'Residential_building' + cte.DORMITORY: 'Residential_building', + cte.DATACENTER: 'n/a', + cte.FARM: 'n/a' } @property diff --git a/hub/helpers/data/hub_function_to_montreal_custom_costs_function.py b/hub/helpers/data/hub_function_to_montreal_custom_costs_function.py index 06b2e16b..6c530aad 100644 --- a/hub/helpers/data/hub_function_to_montreal_custom_costs_function.py +++ b/hub/helpers/data/hub_function_to_montreal_custom_costs_function.py @@ -73,7 +73,9 @@ class HubFunctionToMontrealCustomCostsFunction: cte.AUTOMOTIVE_FACILITY: 'non-residential', cte.PARKING_GARAGE: 'non-residential', cte.RELIGIOUS: 'non-residential', - cte.NON_HEATED: 'non-residential' + cte.NON_HEATED: 'non-residential', + cte.DATACENTER: 'n/a', + cte.FARM: 'n/a' } @property diff --git a/hub/helpers/data/hub_function_to_nrcan_construction_function.py b/hub/helpers/data/hub_function_to_nrcan_construction_function.py index 8cec3715..d8618f07 100644 --- a/hub/helpers/data/hub_function_to_nrcan_construction_function.py +++ b/hub/helpers/data/hub_function_to_nrcan_construction_function.py @@ -72,7 +72,9 @@ class HubFunctionToNrcanConstructionFunction: cte.AUTOMOTIVE_FACILITY: 'n/a', cte.PARKING_GARAGE: 'n/a', cte.RELIGIOUS: 'n/a', - cte.NON_HEATED: 'n/a' + cte.NON_HEATED: 'n/a', + cte.DATACENTER: 'n/a', + cte.FARM: 'n/a' } @property diff --git a/hub/helpers/data/hub_function_to_nrel_construction_function.py b/hub/helpers/data/hub_function_to_nrel_construction_function.py index ca4144a3..39620b7b 100644 --- a/hub/helpers/data/hub_function_to_nrel_construction_function.py +++ b/hub/helpers/data/hub_function_to_nrel_construction_function.py @@ -73,7 +73,9 @@ class HubFunctionToNrelConstructionFunction: cte.AUTOMOTIVE_FACILITY: 'n/a', cte.PARKING_GARAGE: 'n/a', cte.RELIGIOUS: 'n/a', - cte.NON_HEATED: 'n/a' + cte.NON_HEATED: 'n/a', + cte.DATACENTER: 'n/a', + cte.FARM: 'n/a' } @property diff --git a/hub/helpers/data/hub_usage_to_comnet_usage.py b/hub/helpers/data/hub_usage_to_comnet_usage.py index f58193ac..a0304208 100644 --- a/hub/helpers/data/hub_usage_to_comnet_usage.py +++ b/hub/helpers/data/hub_usage_to_comnet_usage.py @@ -73,7 +73,9 @@ class HubUsageToComnetUsage: cte.AUTOMOTIVE_FACILITY: 'BA Automotive Facility', cte.PARKING_GARAGE: 'BA Parking Garage', cte.RELIGIOUS: 'BA Religious Building', - cte.NON_HEATED: 'n/a' + cte.NON_HEATED: 'n/a', + cte.DATACENTER: 'n/a', + cte.FARM: 'n/a' } @property diff --git a/hub/helpers/data/hub_usage_to_eilat_usage.py b/hub/helpers/data/hub_usage_to_eilat_usage.py index e4d8a4cc..04da3203 100644 --- a/hub/helpers/data/hub_usage_to_eilat_usage.py +++ b/hub/helpers/data/hub_usage_to_eilat_usage.py @@ -17,7 +17,9 @@ class HubUsageToEilatUsage: self._dictionary = { cte.RESIDENTIAL: 'Residential', cte.HOTEL: 'Hotel employees', - cte.DORMITORY: 'Dormitory' + cte.DORMITORY: 'Dormitory', + cte.DATACENTER: 'n/a', + cte.FARM: 'n/a' } @property diff --git a/hub/helpers/data/hub_usage_to_hft_usage.py b/hub/helpers/data/hub_usage_to_hft_usage.py index dbc4847e..5d6d3e8f 100644 --- a/hub/helpers/data/hub_usage_to_hft_usage.py +++ b/hub/helpers/data/hub_usage_to_hft_usage.py @@ -73,7 +73,9 @@ class HubUsageToHftUsage: cte.AUTOMOTIVE_FACILITY: 'n/a', cte.PARKING_GARAGE: 'n/a', cte.RELIGIOUS: 'event location', - cte.NON_HEATED: 'non-heated' + cte.NON_HEATED: 'non-heated', + cte.DATACENTER: 'n/a', + cte.FARM: 'n/a' } @property diff --git a/hub/helpers/data/hub_usage_to_nrcan_usage.py b/hub/helpers/data/hub_usage_to_nrcan_usage.py index ed280903..e3868a6f 100644 --- a/hub/helpers/data/hub_usage_to_nrcan_usage.py +++ b/hub/helpers/data/hub_usage_to_nrcan_usage.py @@ -73,7 +73,9 @@ class HubUsageToNrcanUsage: cte.AUTOMOTIVE_FACILITY: 'Automotive facility', cte.PARKING_GARAGE: 'Storage garage', cte.RELIGIOUS: 'Religious building', - cte.NON_HEATED: 'n/a' + cte.NON_HEATED: 'n/a', + cte.DATACENTER: 'n/a', + cte.FARM: 'n/a' } @property diff --git a/hub/helpers/data/montreal_function_to_hub_function.py b/hub/helpers/data/montreal_function_to_hub_function.py index e03020af..16353dab 100644 --- a/hub/helpers/data/montreal_function_to_hub_function.py +++ b/hub/helpers/data/montreal_function_to_hub_function.py @@ -623,8 +623,12 @@ class MontrealFunctionToHubFunction: '8192': cte.FARM, '2439': cte.INDUSTRY, '3891': cte.INDUSTRY, - '6354': cte.WORKSHOP - } + '6354': cte.WORKSHOP, + '4815': cte.NON_HEATED, + '6651': cte.WORKSHOP, + '2822': cte.INDUSTRY, + '2821': cte.INDUSTRY + } @property def dictionary(self) -> dict: diff --git a/hub/helpers/geometry_helper.py b/hub/helpers/geometry_helper.py index eeb6edfc..e6361afd 100644 --- a/hub/helpers/geometry_helper.py +++ b/hub/helpers/geometry_helper.py @@ -312,7 +312,7 @@ class GeometryHelper: country = file_country_code city = file_city_name region_code = f'{file_country_code}.{admin1_code}.{admin2_code}' - return Location(country, city, region_code) + return Location(country, city, region_code, latitude, longitude) @staticmethod def distance_between_points(vertex1, vertex2): diff --git a/hub/helpers/location.py b/hub/helpers/location.py index 5af996fb..b3a3c256 100644 --- a/hub/helpers/location.py +++ b/hub/helpers/location.py @@ -11,10 +11,12 @@ class Location: """ Location """ - def __init__(self, country, city, region_code): + def __init__(self, country, city, region_code, climate_reference_city_latitude, climate_reference_city_longitude): self._country = country self._city = city self._region_code = region_code + self._climate_reference_city_latitude = climate_reference_city_latitude + self._climate_reference_city_longitude = climate_reference_city_longitude @property def city(self): @@ -36,3 +38,17 @@ class Location: Get region """ return self._region_code + + @property + def climate_reference_city_latitude(self): + """ + Get climate-reference-city latitude + """ + return self._climate_reference_city_latitude + + @property + def climate_reference_city_longitude(self): + """ + Get climate-reference-city longitude + """ + return self._climate_reference_city_longitude diff --git a/hub/helpers/peak_calculation/loads_calculation.py b/hub/helpers/peak_calculation/loads_calculation.py index 41a693ba..d10b9715 100644 --- a/hub/helpers/peak_calculation/loads_calculation.py +++ b/hub/helpers/peak_calculation/loads_calculation.py @@ -63,11 +63,10 @@ class LoadsCalculation: :return: int """ heating_load_transmitted = 0 - for internal_zone in self._building.internal_zones: - for thermal_zone in internal_zone.thermal_zones_from_internal_zones: - internal_temperature = thermal_zone.thermal_control.mean_heating_set_point - heating_load_transmitted += self._get_load_transmitted(thermal_zone, internal_temperature, ambient_temperature, - ground_temperature) + for thermal_zone in self._building.thermal_zones_from_internal_zones: + internal_temperature = thermal_zone.thermal_control.mean_heating_set_point + heating_load_transmitted += self._get_load_transmitted(thermal_zone, internal_temperature, ambient_temperature, + ground_temperature) return heating_load_transmitted def get_cooling_transmitted_load(self, ambient_temperature, ground_temperature): @@ -76,11 +75,10 @@ class LoadsCalculation: :return: int """ cooling_load_transmitted = 0 - for internal_zone in self._building.internal_zones: - for thermal_zone in internal_zone.thermal_zones_from_internal_zones: - internal_temperature = thermal_zone.thermal_control.mean_cooling_set_point - cooling_load_transmitted += self._get_load_transmitted(thermal_zone, internal_temperature, ambient_temperature, - ground_temperature) + for thermal_zone in self._building.thermal_zones_from_internal_zones: + internal_temperature = thermal_zone.thermal_control.mean_cooling_set_point + cooling_load_transmitted += self._get_load_transmitted(thermal_zone, internal_temperature, ambient_temperature, + ground_temperature) return cooling_load_transmitted def get_heating_ventilation_load_sensible(self, ambient_temperature): @@ -89,10 +87,9 @@ class LoadsCalculation: :return: int """ heating_ventilation_load = 0 - for internal_zone in self._building.internal_zones: - for thermal_zone in internal_zone.thermal_zones_from_internal_zones: - internal_temperature = thermal_zone.thermal_control.mean_heating_set_point - heating_ventilation_load += self._get_load_ventilation(thermal_zone, internal_temperature, ambient_temperature) + for thermal_zone in self._building.thermal_zones_from_internal_zones: + internal_temperature = thermal_zone.thermal_control.mean_heating_set_point + heating_ventilation_load += self._get_load_ventilation(thermal_zone, internal_temperature, ambient_temperature) return heating_ventilation_load def get_cooling_ventilation_load_sensible(self, ambient_temperature): @@ -101,10 +98,9 @@ class LoadsCalculation: :return: int """ cooling_ventilation_load = 0 - for internal_zone in self._building.internal_zones: - for thermal_zone in internal_zone.thermal_zones_from_internal_zones: - internal_temperature = thermal_zone.thermal_control.mean_cooling_set_point - cooling_ventilation_load += self._get_load_ventilation(thermal_zone, internal_temperature, ambient_temperature) + for thermal_zone in self._building.thermal_zones_from_internal_zones: + internal_temperature = thermal_zone.thermal_control.mean_cooling_set_point + cooling_ventilation_load += self._get_load_ventilation(thermal_zone, internal_temperature, ambient_temperature) return cooling_ventilation_load def get_internal_load_sensible(self): @@ -115,19 +111,18 @@ class LoadsCalculation: cooling_load_occupancy_sensible = 0 cooling_load_lighting = 0 cooling_load_equipment_sensible = 0 - for internal_zone in self._building.internal_zones: - for thermal_zone in internal_zone.thermal_zones_from_internal_zones: - cooling_load_occupancy_sensible += (thermal_zone.occupancy.sensible_convective_internal_gain - + thermal_zone.occupancy.sensible_radiative_internal_gain) \ - * thermal_zone.footprint_area - cooling_load_lighting += ( - thermal_zone.lighting.density * thermal_zone.lighting.convective_fraction + thermal_zone.lighting.density * - thermal_zone.lighting.radiative_fraction - ) * thermal_zone.footprint_area - cooling_load_equipment_sensible += ( - thermal_zone.appliances.density * thermal_zone.appliances.convective_fraction + - thermal_zone.appliances.density * thermal_zone.appliances.radiative_fraction - ) * thermal_zone.footprint_area + for thermal_zone in self._building.thermal_zones_from_internal_zones: + cooling_load_occupancy_sensible += (thermal_zone.occupancy.sensible_convective_internal_gain + + thermal_zone.occupancy.sensible_radiative_internal_gain) \ + * thermal_zone.footprint_area + cooling_load_lighting += ( + thermal_zone.lighting.density * thermal_zone.lighting.convective_fraction + thermal_zone.lighting.density * + thermal_zone.lighting.radiative_fraction + ) * thermal_zone.footprint_area + cooling_load_equipment_sensible += ( + thermal_zone.appliances.density * thermal_zone.appliances.convective_fraction + + thermal_zone.appliances.density * thermal_zone.appliances.radiative_fraction + ) * thermal_zone.footprint_area internal_load = cooling_load_occupancy_sensible + cooling_load_lighting + cooling_load_equipment_sensible return internal_load @@ -137,12 +132,11 @@ class LoadsCalculation: :return: int """ cooling_load_radiation = 0 - for internal_zone in self._building.internal_zones: - for thermal_zone in internal_zone.thermal_zones_from_internal_zones: - for thermal_boundary in thermal_zone.thermal_boundaries: - for thermal_opening in thermal_boundary.thermal_openings: - radiation = thermal_boundary.parent_surface.global_irradiance[cte.HOUR][hour] * cte.WATTS_HOUR_TO_JULES - cooling_load_radiation += ( - thermal_opening.area * (1 - thermal_opening.frame_ratio) * thermal_opening.g_value * radiation - ) + for thermal_zone in self._building.thermal_zones_from_internal_zones: + for thermal_boundary in thermal_zone.thermal_boundaries: + for thermal_opening in thermal_boundary.thermal_openings: + radiation = thermal_boundary.parent_surface.global_irradiance[cte.HOUR][hour] * cte.WATTS_HOUR_TO_JULES + cooling_load_radiation += ( + thermal_opening.area * (1 - thermal_opening.frame_ratio) * thermal_opening.g_value * radiation + ) return cooling_load_radiation diff --git a/hub/helpers/peak_loads.py b/hub/helpers/peak_loads.py index 995e2ada..bd3fd1f5 100644 --- a/hub/helpers/peak_loads.py +++ b/hub/helpers/peak_loads.py @@ -54,13 +54,11 @@ class PeakLoads: """ month = 1 peaks = [0 for _ in range(12)] - print('hv', hourly_values) for i in range(0, len(hourly_values)): if _MONTH_STARTING_HOUR[month] <= i: month += 1 if hourly_values[i] > peaks[month-1]: peaks[month-1] = hourly_values[i] - print('peak', peaks) return peaks @property @@ -75,18 +73,15 @@ class PeakLoads: ambient_temperature = self._building.external_temperature[cte.HOUR] for month in range(0, 12): ground_temperature = self._building.ground_temperature[cte.MONTH]['2'][month] - heating_ambient_temperature = 100 start_hour = _MONTH_STARTING_HOUR[month] end_hour = 8760 if month < 11: end_hour = _MONTH_STARTING_HOUR[month + 1] - for hour in range(start_hour, end_hour): - temperature = ambient_temperature[hour] - if temperature < heating_ambient_temperature: - heating_ambient_temperature = temperature + heating_ambient_temperature = min(ambient_temperature[start_hour:end_hour]) loads = LoadsCalculation(self._building) heating_load_transmitted = loads.get_heating_transmitted_load(heating_ambient_temperature, ground_temperature) heating_load_ventilation_sensible = loads.get_heating_ventilation_load_sensible(heating_ambient_temperature) + # todo: include heating ventilation latent heating_load_ventilation_latent = 0 heating_load = heating_load_transmitted + heating_load_ventilation_sensible + heating_load_ventilation_latent heating_load = max(heating_load, 0) diff --git a/hub/imports/construction/eilat_physics_parameters.py b/hub/imports/construction/eilat_physics_parameters.py index ea088178..57feba0d 100644 --- a/hub/imports/construction/eilat_physics_parameters.py +++ b/hub/imports/construction/eilat_physics_parameters.py @@ -71,6 +71,7 @@ class EilatPhysicsParameters: for catalog_construction in catalog_archetype.constructions: construction = Construction() construction.type = catalog_construction.type + construction.name = catalog_construction.name if catalog_construction.window_ratio is not None: for _orientation in catalog_construction.window_ratio: if catalog_construction.window_ratio[_orientation] is None: @@ -83,7 +84,7 @@ class EilatPhysicsParameters: layer.thickness = layer_archetype.thickness total_thickness += layer_archetype.thickness archetype_material = layer_archetype.material - layer.name = archetype_material.name + layer.material_name = archetype_material.name layer.no_mass = archetype_material.no_mass if archetype_material.no_mass: layer.thermal_resistance = archetype_material.thermal_resistance diff --git a/hub/imports/construction/helpers/construction_helper.py b/hub/imports/construction/helpers/construction_helper.py index eddabd33..d75d8424 100644 --- a/hub/imports/construction/helpers/construction_helper.py +++ b/hub/imports/construction/helpers/construction_helper.py @@ -42,7 +42,6 @@ class ConstructionHelper: 'Varennes': '6', 'Laval': '6', 'Longueuil': '6', - 'Saint-Leonard': '6', 'Mont-Royal': '6', 'Deux-Montagnes': '6', 'Dorval': '6', @@ -58,6 +57,8 @@ class ConstructionHelper: 'Pointe-Claire': '6', 'Boucherville': '6', 'Mascouche': '6', + 'Saint-Leonard': '6', + 'La Prairie': '6' } _reference_city_to_israel_climate_zone = { diff --git a/hub/imports/construction/nrcan_physics_parameters.py b/hub/imports/construction/nrcan_physics_parameters.py index 9ab9964c..87a91d80 100644 --- a/hub/imports/construction/nrcan_physics_parameters.py +++ b/hub/imports/construction/nrcan_physics_parameters.py @@ -71,6 +71,7 @@ class NrcanPhysicsParameters: for catalog_construction in catalog_archetype.constructions: construction = Construction() construction.type = catalog_construction.type + construction.name = catalog_construction.name if catalog_construction.window_ratio is not None: for _orientation in catalog_construction.window_ratio: if catalog_construction.window_ratio[_orientation] is None: @@ -81,7 +82,7 @@ class NrcanPhysicsParameters: layer = Layer() layer.thickness = layer_archetype.thickness archetype_material = layer_archetype.material - layer.name = archetype_material.name + layer.material_name = archetype_material.name layer.no_mass = archetype_material.no_mass if archetype_material.no_mass: layer.thermal_resistance = archetype_material.thermal_resistance diff --git a/hub/imports/construction/nrel_physics_parameters.py b/hub/imports/construction/nrel_physics_parameters.py index 55b4d3b7..679e66cb 100644 --- a/hub/imports/construction/nrel_physics_parameters.py +++ b/hub/imports/construction/nrel_physics_parameters.py @@ -73,6 +73,7 @@ class NrelPhysicsParameters: for catalog_construction in catalog_archetype.constructions: construction = Construction() construction.type = catalog_construction.type + construction.name = catalog_construction.name if catalog_construction.window_ratio is not None: construction.window_ratio = {'north': catalog_construction.window_ratio, 'east': catalog_construction.window_ratio, @@ -84,7 +85,7 @@ class NrelPhysicsParameters: layer = Layer() layer.thickness = layer_archetype.thickness archetype_material = layer_archetype.material - layer.name = archetype_material.name + layer.material_name = archetype_material.name layer.no_mass = archetype_material.no_mass if archetype_material.no_mass: layer.thermal_resistance = archetype_material.thermal_resistance diff --git a/hub/imports/geometry/citygml.py b/hub/imports/geometry/citygml.py index ab36522f..aa352ef8 100644 --- a/hub/imports/geometry/citygml.py +++ b/hub/imports/geometry/citygml.py @@ -4,6 +4,7 @@ SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2022 Concordia CERC group Project Coder Guille Gutierrez guillermo.gutierrezmorote@concordia.ca """ +import logging import numpy as np import xmltodict @@ -22,16 +23,17 @@ class CityGml: def __init__(self, path, - extrusion_height_field=None, year_of_construction_field=None, function_field=None, - function_to_hub=None): + function_to_hub=None, + hub_crs=None): self._city = None self._lod = None self._lod1_tags = ['lod1Solid', 'lod1MultiSurface'] self._lod2_tags = ['lod2Solid', 'lod2MultiSurface', 'lod2MultiCurve'] - self._extrusion_height_field = extrusion_height_field self._function_to_hub = function_to_hub + if hub_crs is None: + hub_crs = 'EPSG:26911' if function_field is None: function_field = 'function' if year_of_construction_field is None: @@ -79,7 +81,8 @@ class CityGml: self._srs_name = envelope['@srsName'] else: # If not coordinate system given assuming hub standard - self._srs_name = "EPSG:26911" + logging.warning(f'gml file contains no coordinate system assuming {hub_crs}') + self._srs_name = hub_crs else: # get the boundary from the city objects instead for city_object_member in self._gml['CityModel']['cityObjectMember']: diff --git a/hub/imports/geometry/citygml_classes/citygml_lod2.py b/hub/imports/geometry/citygml_classes/citygml_lod2.py index 3705f99a..0fa4748a 100644 --- a/hub/imports/geometry/citygml_classes/citygml_lod2.py +++ b/hub/imports/geometry/citygml_classes/citygml_lod2.py @@ -49,6 +49,8 @@ class CityGmlLod2(CityGmlBase): surface_encoding, surface_subtype = cls._surface_encoding(bounded[surface_type]) except NotImplementedError: continue + if 'surfaceMember' not in bounded[surface_type][surface_encoding][surface_subtype]: + continue for member in bounded[surface_type][surface_encoding][surface_subtype]['surfaceMember']: if 'CompositeSurface' in member: for composite_members in member['CompositeSurface']['surfaceMember']: diff --git a/hub/imports/geometry/geojson.py b/hub/imports/geometry/geojson.py index 84269cd1..20b74534 100644 --- a/hub/imports/geometry/geojson.py +++ b/hub/imports/geometry/geojson.py @@ -34,9 +34,13 @@ class Geojson: extrusion_height_field=None, year_of_construction_field=None, function_field=None, - function_to_hub=None): - # todo: destination epsg should change according actual the location - self._transformer = Transformer.from_crs('epsg:4326', 'epsg:26911') + function_to_hub=None, + hub_crs=None + ): + self._hub_crs = hub_crs + if hub_crs is None : + self._hub_crs = 'epsg:26911' + self._transformer = Transformer.from_crs('epsg:4326', self._hub_crs) self._min_x = cte.MAX_FLOAT self._min_y = cte.MAX_FLOAT self._max_x = cte.MIN_FLOAT @@ -116,6 +120,7 @@ class Geojson: if self._extrusion_height_field is not None: extrusion_height = float(feature['properties'][self._extrusion_height_field]) lod = 1 + self._max_z = max(self._max_z, extrusion_height) year_of_construction = None if self._year_of_construction_field is not None: year_of_construction = int(feature['properties'][self._year_of_construction_field]) @@ -154,7 +159,7 @@ class Geojson: extrusion_height)) else: raise NotImplementedError(f'Geojson geometry type [{geometry["type"]}] unknown') - self._city = City([self._min_x, self._min_y, 0.0], [self._max_x, self._max_y, self._max_z], 'epsg:26911') + self._city = City([self._min_x, self._min_y, 0.0], [self._max_x, self._max_y, self._max_z], self._hub_crs) for building in buildings: # Do not include "small building-like structures" to buildings if building.floor_area >= 25: @@ -165,7 +170,6 @@ class Geojson: if lod > 0: lines_information = GeometryHelper.city_mapping(self._city, plot=False) self._store_shared_percentage_to_walls(self._city, lines_information) - return self._city def _polygon_coordinates_to_3d(self, polygon_coordinates): diff --git a/hub/imports/geometry_factory.py b/hub/imports/geometry_factory.py index 3e6ca0b3..21dba708 100644 --- a/hub/imports/geometry_factory.py +++ b/hub/imports/geometry_factory.py @@ -18,21 +18,21 @@ class GeometryFactory: """ def __init__(self, file_type, path=None, - data_frame=None, aliases_field=None, height_field=None, year_of_construction_field=None, function_field=None, - function_to_hub=None): + function_to_hub=None, + hub_crs=None): self._file_type = '_' + file_type.lower() validate_import_export_type(GeometryFactory, file_type) self._path = path - self._data_frame = data_frame self._aliases_field = aliases_field self._height_field = height_field self._year_of_construction_field = year_of_construction_field self._function_field = function_field self._function_to_hub = function_to_hub + self._hub_crs = hub_crs @property def _citygml(self) -> City: @@ -41,10 +41,10 @@ class GeometryFactory: :return: City """ return CityGml(self._path, - self._height_field, self._year_of_construction_field, self._function_field, - self._function_to_hub).city + self._function_to_hub, + self._hub_crs).city @property def _obj(self) -> City: @@ -65,7 +65,8 @@ class GeometryFactory: self._height_field, self._year_of_construction_field, self._function_field, - self._function_to_hub).city + self._function_to_hub, + self._hub_crs).city @property def city(self) -> City: diff --git a/hub/imports/results/insel_monthly_energry_balance.py b/hub/imports/results/insel_monthly_energry_balance.py index 1215ab46..37783185 100644 --- a/hub/imports/results/insel_monthly_energry_balance.py +++ b/hub/imports/results/insel_monthly_energry_balance.py @@ -4,7 +4,7 @@ SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2022 Concordia CERC group Project Coder Guillermo.GutierrezMorote@concordia.ca """ - +import logging from pathlib import Path import csv @@ -43,53 +43,61 @@ class InselMonthlyEnergyBalance: domestic_hot_water_demand = [] lighting_demand = [] appliances_demand = [] - if building.internal_zones[0].thermal_zones_from_internal_zones is None: + + # todo: REFACTOR after retrofit project, this is a hack for the pickle files + try: + if building.internal_zones[0].thermal_zones_from_internal_zones is None: + domestic_hot_water_demand = [0] * 12 + lighting_demand = [0] * 12 + appliances_demand = [0] * 12 + else: + thermal_zone = building.internal_zones[0].thermal_zones_from_internal_zones[0] + area = thermal_zone.total_floor_area + cold_water = building.cold_water_temperature[cte.MONTH] + peak_flow = thermal_zone.domestic_hot_water.peak_flow + service_temperature = thermal_zone.domestic_hot_water.service_temperature + lighting_density = thermal_zone.lighting.density + appliances_density = thermal_zone.appliances.density + + for i_month, month in enumerate(cte.MONTHS): + total_dhw_demand = 0 + total_lighting = 0 + total_appliances = 0 + + for schedule in thermal_zone.lighting.schedules: + total_day = 0 + for value in schedule.values: + total_day += value + for day_type in schedule.day_types: + total_lighting += total_day * cte.WEEK_DAYS_A_MONTH[month][day_type] \ + * lighting_density / cte.WATTS_HOUR_TO_JULES + lighting_demand.append(total_lighting * area) + + for schedule in thermal_zone.appliances.schedules: + total_day = 0 + for value in schedule.values: + total_day += value + for day_type in schedule.day_types: + total_appliances += total_day * cte.WEEK_DAYS_A_MONTH[month][day_type] \ + * appliances_density / cte.WATTS_HOUR_TO_JULES + appliances_demand.append(total_appliances * area) + + for schedule in thermal_zone.domestic_hot_water.schedules: + total_day = 0 + for value in schedule.values: + total_day += value + for day_type in schedule.day_types: + demand = ( + peak_flow * cte.WATER_DENSITY * cte.WATER_HEAT_CAPACITY + * (service_temperature - cold_water[i_month]) / cte.WATTS_HOUR_TO_JULES + ) + total_dhw_demand += total_day * cte.WEEK_DAYS_A_MONTH[month][day_type] * demand + domestic_hot_water_demand.append(total_dhw_demand * area) + except AttributeError: domestic_hot_water_demand = [0] * 12 lighting_demand = [0] * 12 appliances_demand = [0] * 12 - else: - thermal_zone = building.internal_zones[0].thermal_zones_from_internal_zones[0] - area = thermal_zone.total_floor_area - cold_water = building.cold_water_temperature[cte.MONTH] - peak_flow = thermal_zone.domestic_hot_water.peak_flow - service_temperature = thermal_zone.domestic_hot_water.service_temperature - lighting_density = thermal_zone.lighting.density - appliances_density = thermal_zone.appliances.density - - for month in range(0, 12): - total_dhw_demand = 0 - total_lighting = 0 - total_appliances = 0 - - for schedule in thermal_zone.lighting.schedules: - total_day = 0 - for value in schedule.values: - total_day += value - for day_type in schedule.day_types: - total_lighting += total_day * cte.WEEK_DAYS_A_MONTH[day_type][month] \ - * lighting_density / cte.WATTS_HOUR_TO_JULES - lighting_demand.append(total_lighting * area) - - for schedule in thermal_zone.appliances.schedules: - total_day = 0 - for value in schedule.values: - total_day += value - for day_type in schedule.day_types: - total_appliances += total_day * cte.WEEK_DAYS_A_MONTH[day_type][month] \ - * appliances_density / cte.WATTS_HOUR_TO_JULES - appliances_demand.append(total_appliances * area) - - for schedule in thermal_zone.domestic_hot_water.schedules: - total_day = 0 - for value in schedule.values: - total_day += value - for day_type in schedule.day_types: - demand = ( - peak_flow * cte.WATER_DENSITY * cte.WATER_HEAT_CAPACITY - * (service_temperature - cold_water[month]) / cte.WATTS_HOUR_TO_JULES - ) - total_dhw_demand += total_day * cte.WEEK_DAYS_A_MONTH[day_type][month] * demand - domestic_hot_water_demand.append(total_dhw_demand * area) + logging.warning('Building internal zone raised an error, most likely the building has missing archetypes') building.domestic_hot_water_heat_demand[cte.MONTH] = domestic_hot_water_demand building.domestic_hot_water_heat_demand[cte.YEAR] = [sum(domestic_hot_water_demand)] diff --git a/hub/imports/results_factory.py b/hub/imports/results_factory.py index c5f092b1..c277b122 100644 --- a/hub/imports/results_factory.py +++ b/hub/imports/results_factory.py @@ -31,7 +31,7 @@ class ResultFactory: self._handler = '_' + handler.lower().replace(' ', '_') validate_import_export_type(ResultFactory, handler) self._city = city - self._base_path = base_path + self._base_path = Path(base_path) self._hp_model = hp_model def _sra(self): diff --git a/hub/imports/weather/helpers/weather.py b/hub/imports/weather/helpers/weather.py index 9d894991..7603cb5b 100644 --- a/hub/imports/weather/helpers/weather.py +++ b/hub/imports/weather/helpers/weather.py @@ -24,7 +24,8 @@ class Weather: 'DE.01.082': 'https://energyplus-weather.s3.amazonaws.com/europe_wmo_region_6/DEU/DEU_Stuttgart.107380_IWEC/DEU_Stuttgart.107380_IWEC.epw', 'US.NY.047': 'https://energyplus-weather.s3.amazonaws.com/north_and_central_america_wmo_region_4/USA/NY/USA_NY_New.York.City-Central.Park.94728_TMY/USA_NY_New.York.City-Central.Park.94728_TMY.epw', 'CA.10.12': 'https://energyplus-weather.s3.amazonaws.com/north_and_central_america_wmo_region_4/CAN/PQ/CAN_PQ_Quebec.717140_CWEC/CAN_PQ_Quebec.717140_CWEC.epw', - 'IL.01.': 'https://energyplus-weather.s3.amazonaws.com/europe_wmo_region_6/ISR/ISR_Eilat.401990_MSI/ISR_Eilat.401990_MSI.epw' + 'IL.01.': 'https://energyplus-weather.s3.amazonaws.com/europe_wmo_region_6/ISR/ISR_Eilat.401990_MSI/ISR_Eilat.401990_MSI.epw', + 'ES.07.PM': 'https://energyplus-weather.s3.amazonaws.com/europe_wmo_region_6/ESP/ESP_Palma.083060_SWEC/ESP_Palma.083060_SWEC.epw' } # todo: this dictionary need to be completed, a data science student task? diff --git a/hub/persistence/db_control.py b/hub/persistence/db_control.py index 5bc7df35..e060559b 100644 --- a/hub/persistence/db_control.py +++ b/hub/persistence/db_control.py @@ -75,10 +75,10 @@ class DBControl: : """ cities = self._city.get_by_user_id_application_id_and_scenario(user_id, application_id, scenario) - for city in cities: - result = self.building_info(name, city[0].id) - if result is not None: - return result + c = [c[0].id for c in cities] + result = self._city_object.building_in_cities_info(name, c) + if result is not None: + return result return None def building_info(self, name, city_id) -> CityObject: @@ -90,6 +90,15 @@ class DBControl: """ return self._city_object.get_by_name_or_alias_and_city(name, city_id) + def building_info_in_cities(self, name, cities) -> CityObject: + """ + Retrieve the building info from the database + :param name: Building name + :param cities: [City ID] + :return: CityObject + """ + return self._city_object.get_by_name_or_alias_in_cities(name, cities) + def buildings_info(self, request_values, city_id) -> [CityObject]: """ Retrieve the buildings info from the database @@ -114,10 +123,7 @@ class DBControl: result_names = [] results = {} for scenario in request_values['scenarios']: - print('scenario', scenario, results) for scenario_name in scenario.keys(): - print('scenario name', scenario_name) - result_sets = self._city.get_by_user_id_application_id_and_scenario( user_id, application_id, @@ -125,25 +131,21 @@ class DBControl: ) if result_sets is None: continue - for result_set in result_sets: - city_id = result_set[0].id + results[scenario_name] = [] + city_ids = [r[0].id for r in result_sets] + for building_name in scenario[scenario_name]: + _building = self._city_object.get_by_name_or_alias_in_cities(building_name, city_ids) + if _building is None: + continue + city_object_id = _building.id + _ = self._simulation_results.get_simulation_results_by_city_object_id_and_names( + city_object_id, + result_names) - results[scenario_name] = [] - for building_name in scenario[scenario_name]: - _building = self._city_object.get_by_name_or_alias_and_city(building_name, city_id) - if _building is None: - continue - city_object_id = _building.id - _ = self._simulation_results.get_simulation_results_by_city_id_city_object_id_and_names( - city_id, - city_object_id, - result_names) - - for value in _: - values = json.loads(value.values) - values["building"] = building_name - results[scenario_name].append(values) - print(scenario, results) + for value in _: + values = value.values + values["building"] = building_name + results[scenario_name].append(values) return results def persist_city(self, city: City, pickle_path, scenario, application_id: int, user_id: int): diff --git a/hub/persistence/models/application.py b/hub/persistence/models/application.py index 3da947f4..23f05a27 100644 --- a/hub/persistence/models/application.py +++ b/hub/persistence/models/application.py @@ -7,9 +7,10 @@ Project Coder Guille Gutierrez Guillermo.GutierrezMorote@concordia.ca import datetime -from sqlalchemy.dialects.postgresql import UUID from sqlalchemy import Column, Integer, String, Sequence from sqlalchemy import DateTime +from sqlalchemy.dialects.postgresql import UUID + from hub.persistence.configuration import Models diff --git a/hub/persistence/models/city.py b/hub/persistence/models/city.py index e77e51de..35f529b1 100644 --- a/hub/persistence/models/city.py +++ b/hub/persistence/models/city.py @@ -21,8 +21,8 @@ class City(Models): pickle_path = Column(String, nullable=False) name = Column(String, nullable=False) scenario = Column(String, nullable=False) - application_id = Column(Integer, ForeignKey('application.id'), nullable=False) - user_id = Column(Integer, ForeignKey('user.id'), nullable=True) + application_id = Column(Integer, ForeignKey('application.id', ondelete='CASCADE'), nullable=False) + user_id = Column(Integer, ForeignKey('user.id', ondelete='CASCADE'), nullable=True) hub_release = Column(String, nullable=False) created = Column(DateTime, default=datetime.datetime.utcnow) updated = Column(DateTime, default=datetime.datetime.utcnow) diff --git a/hub/persistence/models/city_object.py b/hub/persistence/models/city_object.py index 5face637..1d88b9ec 100644 --- a/hub/persistence/models/city_object.py +++ b/hub/persistence/models/city_object.py @@ -6,6 +6,7 @@ Project Coder Guille Gutierrez Guillermo.GutierrezMorote@concordia.ca """ import datetime +import logging from sqlalchemy import Column, Integer, String, Sequence, ForeignKey, Float from sqlalchemy import DateTime @@ -20,7 +21,7 @@ class CityObject(Models): """ __tablename__ = 'city_object' id = Column(Integer, Sequence('city_object_id_seq'), primary_key=True) - city_id = Column(Integer, ForeignKey('city.id'), nullable=False) + city_id = Column(Integer, ForeignKey('city.id', ondelete='CASCADE'), nullable=False) name = Column(String, nullable=False) aliases = Column(String, nullable=True) type = Column(String, nullable=False) @@ -51,19 +52,27 @@ class CityObject(Models): self.roof_area = sum(roof.solid_polygon.area for roof in building.roofs) self.total_pv_area = sum(roof.solid_polygon.area * roof.solar_collectors_area_reduction_factor for roof in building.roofs) storeys = building.storeys_above_ground - if storeys is None: - storeys = building.max_height / building.average_storey_height - self.total_heating_area = building.floor_area * storeys wall_area = 0 + window_ratio = 0 + try: + if storeys is None: + storeys = building.max_height / building.average_storey_height + for internal_zone in building.internal_zones: + for thermal_zone in internal_zone.thermal_zones_from_internal_zones: + for thermal_boundary in thermal_zone.thermal_boundaries: + window_ratio = thermal_boundary.window_ratio + break + except TypeError: + storeys = 0 + logging.warning( + 'building %s has no storey height so heating area, storeys and window ratio cannot be calculated', + self.name + ) + self.total_heating_area = building.floor_area * storeys + for wall in building.walls: wall_area += wall.solid_polygon.area self.wall_area = wall_area - window_ratio = 0 - for internal_zone in building.internal_zones: - for thermal_zone in internal_zone.thermal_zones_from_internal_zones: - for thermal_boundary in thermal_zone.thermal_boundaries: - window_ratio = thermal_boundary.window_ratio - break self.windows_area = wall_area * window_ratio system_name = building.energy_systems_archetype_name if system_name is None: diff --git a/hub/persistence/models/simulation_results.py b/hub/persistence/models/simulation_results.py index d4dea88d..a40c077e 100644 --- a/hub/persistence/models/simulation_results.py +++ b/hub/persistence/models/simulation_results.py @@ -19,8 +19,8 @@ class SimulationResults(Models): """ __tablename__ = 'simulation_results' id = Column(Integer, Sequence('simulation_results_id_seq'), primary_key=True) - city_id = Column(Integer, ForeignKey('city.id'), nullable=True) - city_object_id = Column(Integer, ForeignKey('city_object.id'), nullable=True) + city_id = Column(Integer, ForeignKey('city.id', ondelete='CASCADE'), nullable=True) + city_object_id = Column(Integer, ForeignKey('city_object.id', ondelete='CASCADE'), nullable=True) name = Column(String, nullable=False) values = Column(JSONB, nullable=False) created = Column(DateTime, default=datetime.datetime.utcnow) diff --git a/hub/persistence/repositories/application.py b/hub/persistence/repositories/application.py index f68fd0bb..a70fde37 100644 --- a/hub/persistence/repositories/application.py +++ b/hub/persistence/repositories/application.py @@ -10,6 +10,7 @@ import logging from sqlalchemy import select from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.orm.session import Session from hub.persistence.repository import Repository from hub.persistence.models import Application as Model @@ -48,10 +49,11 @@ class Application(Repository): pass try: application = Model(name=name, description=description, application_uuid=application_uuid) - self.session.add(application) - self.session.commit() - self.session.refresh(application) - return application.id + with Session(self.engine) as session: + session.add(application) + session.commit() + session.refresh(application) + return application.id except SQLAlchemyError as err: logging.error('An error occurred while creating application %s', err) raise SQLAlchemyError from err @@ -65,10 +67,11 @@ class Application(Repository): :return: None """ try: - self.session.query(Model).filter( - Model.application_uuid == application_uuid - ).update({'name': name, 'description': description, 'updated': datetime.datetime.utcnow()}) - self.session.commit() + with Session(self.engine) as session: + session.query(Model).filter( + Model.application_uuid == application_uuid + ).update({'name': name, 'description': description, 'updated': datetime.datetime.utcnow()}) + session.commit() except SQLAlchemyError as err: logging.error('Error while updating application %s', err) raise SQLAlchemyError from err @@ -80,9 +83,10 @@ class Application(Repository): :return: None """ try: - self.session.query(Model).filter(Model.application_uuid == application_uuid).delete() - self.session.flush() - self.session.commit() + with Session(self.engine) as session: + session.query(Model).filter(Model.application_uuid == application_uuid).delete() + session.flush() + session.commit() except SQLAlchemyError as err: logging.error('Error while deleting application %s', err) raise SQLAlchemyError from err @@ -94,10 +98,11 @@ class Application(Repository): :return: Application with the provided application_uuid """ try: - result_set = self.session.execute(select(Model).where( - Model.application_uuid == application_uuid) - ).first() - return result_set[0] + with Session(self.engine) as session: + result_set = session.execute(select(Model).where( + Model.application_uuid == application_uuid) + ).first() + return result_set[0] except SQLAlchemyError as err: logging.error('Error while fetching application by application_uuid %s', err) raise SQLAlchemyError from err diff --git a/hub/persistence/repositories/city.py b/hub/persistence/repositories/city.py index 9c20346f..17e018a6 100644 --- a/hub/persistence/repositories/city.py +++ b/hub/persistence/repositories/city.py @@ -54,18 +54,18 @@ class City(Repository): application_id, user_id, __version__) - - self.session.add(db_city) - self.session.flush() - self.session.commit() - for building in city.buildings: - db_city_object = CityObject(db_city.id, - building) - self.session.add(db_city_object) - self.session.flush() - self.session.commit() - self.session.refresh(db_city) - return db_city.id + with Session(self.engine) as session: + session.add(db_city) + session.flush() + session.commit() + for building in city.buildings: + db_city_object = CityObject(db_city.id, + building) + session.add(db_city_object) + session.flush() + session.commit() + session.refresh(db_city) + return db_city.id except SQLAlchemyError as err: logging.error('An error occurred while creating a city %s', err) raise SQLAlchemyError from err @@ -79,8 +79,9 @@ class City(Repository): """ try: now = datetime.datetime.utcnow() - self.session.query(Model).filter(Model.id == city_id).update({'name': city.name, 'updated': now}) - self.session.commit() + with Session(self.engine) as session: + session.query(Model).filter(Model.id == city_id).update({'name': city.name, 'updated': now}) + session.commit() except SQLAlchemyError as err: logging.error('Error while updating city %s', err) raise SQLAlchemyError from err @@ -92,9 +93,10 @@ class City(Repository): :return: None """ try: - self.session.query(CityObject).filter(CityObject.city_id == city_id).delete() - self.session.query(Model).filter(Model.id == city_id).delete() - self.session.commit() + with Session(self.engine) as session: + session.query(CityObject).filter(CityObject.city_id == city_id).delete() + session.query(Model).filter(Model.id == city_id).delete() + session.commit() except SQLAlchemyError as err: logging.error('Error while fetching city %s', err) raise SQLAlchemyError from err @@ -108,13 +110,12 @@ class City(Repository): :return: [ModelCity] """ try: - result_set = self.session.execute(select(Model).where(Model.user_id == user_id, - Model.application_id == application_id, - Model.scenario == scenario - )).all() - self.session.close() - self.session = Session(self.engine) - return result_set + with Session(self.engine) as session: + result_set = session.execute(select(Model).where(Model.user_id == user_id, + Model.application_id == application_id, + Model.scenario == scenario + )).all() + return result_set except SQLAlchemyError as err: logging.error('Error while fetching city by name %s', err) raise SQLAlchemyError from err @@ -127,11 +128,11 @@ class City(Repository): :return: ModelCity """ try: - result_set = self.session.execute( - select(Model).where(Model.user_id == user_id, Model.application_id == application_id) - ) - return [r[0] for r in result_set] + with Session(self.engine) as session: + result_set = session.execute( + select(Model).where(Model.user_id == user_id, Model.application_id == application_id) + ) + return [r[0] for r in result_set] except SQLAlchemyError as err: logging.error('Error while fetching city by name %s', err) raise SQLAlchemyError from err - diff --git a/hub/persistence/repositories/city_object.py b/hub/persistence/repositories/city_object.py index aa69a0e3..bba01ef5 100644 --- a/hub/persistence/repositories/city_object.py +++ b/hub/persistence/repositories/city_object.py @@ -6,14 +6,15 @@ Project Coder Guille Gutierrez Guillermo.GutierrezMorote@concordia.ca """ import datetime import logging +from typing import Union -from sqlalchemy import select, or_ +from sqlalchemy import select from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.orm import Session from hub.city_model_structure.building import Building -from hub.persistence.repository import Repository from hub.persistence.models import CityObject as Model +from hub.persistence.repository import Repository class CityObject(Repository): @@ -46,10 +47,11 @@ class CityObject(Repository): try: city_object = Model(city_id=city_id, building=building) - self.session.add(city_object) - self.session.flush() - self.session.commit() - self.session.refresh(city_object) + with Session(self.engine) as session: + session.add(city_object) + session.flush() + session.commit() + session.refresh(city_object) return city_object.id except SQLAlchemyError as err: logging.error('An error occurred while creating city_object %s', err) @@ -68,17 +70,18 @@ class CityObject(Repository): for usage in internal_zone.usages: object_usage = f'{object_usage}{usage.name}_{usage.percentage} ' object_usage = object_usage.rstrip() - self.session.query(Model).filter(Model.name == building.name, Model.city_id == city_id).update( - {'name': building.name, - 'alias': building.alias, - 'object_type': building.type, - 'year_of_construction': building.year_of_construction, - 'function': building.function, - 'usage': object_usage, - 'volume': building.volume, - 'area': building.floor_area, - 'updated': datetime.datetime.utcnow()}) - self.session.commit() + with Session(self.engine) as session: + session.query(Model).filter(Model.name == building.name, Model.city_id == city_id).update( + {'name': building.name, + 'alias': building.alias, + 'object_type': building.type, + 'year_of_construction': building.year_of_construction, + 'function': building.function, + 'usage': object_usage, + 'volume': building.volume, + 'area': building.floor_area, + 'updated': datetime.datetime.utcnow()}) + session.commit() except SQLAlchemyError as err: logging.error('Error while updating city object %s', err) raise SQLAlchemyError from err @@ -91,30 +94,96 @@ class CityObject(Repository): :return: None """ try: - self.session.query(Model).filter(Model.city_id == city_id, Model.name == name).delete() - self.session.commit() + with Session(self.engine) as session: + session.query(Model).filter(Model.city_id == city_id, Model.name == name).delete() + session.commit() except SQLAlchemyError as err: logging.error('Error while deleting application %s', err) raise SQLAlchemyError from err - def get_by_name_or_alias_and_city(self, name, city_id) -> Model: + def building_in_cities_info(self, name, cities): """ Fetch a city object based on name and city id :param name: city object name - :param city_id: a city identifier + :param cities: city identifiers :return: [CityObject] with the provided name or alias belonging to the city with id city_id """ try: # search by name first - city_object = self.session.execute(select(Model).where(Model.name == name, Model.city_id == city_id)).first() - if city_object is not None: - return city_object[0] - # name not found, so search by alias instead - city_objects = self.session.execute( - select(Model).where(Model.aliases.contains(name), Model.city_id == city_id) - ).all() - self.session.close() - self.session = Session(self.engine) + with Session(self.engine) as session: + city_object = session.execute(select(Model).where( + Model.name == name, Model.city_id.in_(cities)) + ).first() + if city_object is not None: + return city_object[0] + # name not found, so search by alias instead + city_objects = session.execute( + select(Model).where(Model.aliases.contains(name), Model.city_id.in_(cities)) + ).all() + for city_object in city_objects: + aliases = city_object[0].aliases.replace('{', '').replace('}', '').split(',') + for alias in aliases: + if alias == name: + # force the name as the alias + city_object[0].name = name + return city_object[0] + return None + except SQLAlchemyError as err: + logging.error('Error while fetching city object by name and city: %s', err) + raise SQLAlchemyError from err + except IndexError as err: + logging.error('Error while fetching city object by name and city, empty result %s', err) + raise IndexError from err + + def get_by_name_or_alias_and_city(self, name, city_id) -> Union[Model, None]: + """ + Fetch a city object based on name and city id + :param name: city object name + :param city_id: a city identifier + :return: [CityObject] with the provided name or alias belonging to the city with id city_id + """ + try: + # search by name first + with Session(self.engine) as session: + city_object = session.execute(select(Model).where(Model.name == name, Model.city_id == city_id)).first() + if city_object is not None: + return city_object[0] + # name not found, so search by alias instead + city_objects = session.execute( + select(Model).where(Model.aliases.contains(name), Model.city_id == city_id) + ).all() + for city_object in city_objects: + aliases = city_object[0].aliases.replace('{', '').replace('}', '').split(',') + for alias in aliases: + if alias == name: + # force the name as the alias + city_object[0].name = name + return city_object[0] + return None + except SQLAlchemyError as err: + logging.error('Error while fetching city object by name and city: %s', err) + raise SQLAlchemyError from err + except IndexError as err: + logging.error('Error while fetching city object by name and city, empty result %s', err) + raise IndexError from err + + def get_by_name_or_alias_in_cities(self, name, city_ids) -> Model: + """ + Fetch a city object based on name and city ids + :param name: city object name + :param city_ids: a list of city identifiers + :return: [CityObject] with the provided name or alias belonging to the city with id city_id + """ + try: + # search by name first + with Session(self.engine) as session: + city_object = session.execute(select(Model).where(Model.name == name, Model.city_id.in_(tuple(city_ids)))).first() + if city_object is not None: + return city_object[0] + # name not found, so search by alias instead + city_objects = session.execute( + select(Model).where(Model.aliases.contains(name), Model.city_id.in_(tuple(city_ids))) + ).all() for city_object in city_objects: aliases = city_object[0].aliases.replace('{', '').replace('}', '').split(',') for alias in aliases: diff --git a/hub/persistence/repositories/simulation_results.py b/hub/persistence/repositories/simulation_results.py index 0147eafc..b2757f40 100644 --- a/hub/persistence/repositories/simulation_results.py +++ b/hub/persistence/repositories/simulation_results.py @@ -10,6 +10,7 @@ import logging from sqlalchemy import or_ from sqlalchemy import select from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.orm import Session from hub.persistence.repository import Repository from hub.persistence.models import City @@ -52,11 +53,12 @@ class SimulationResults(Repository): values=values, city_id=city_id, city_object_id=city_object_id) - self.session.add(simulation_result) - self.session.flush() - self.session.commit() - self.session.refresh(simulation_result) - return simulation_result.id + with Session(self.engine) as session: + session.add(simulation_result) + session.flush() + session.commit() + session.refresh(simulation_result) + return simulation_result.id except SQLAlchemyError as err: logging.error('An error occurred while creating city_object %s', err) raise SQLAlchemyError from err @@ -71,22 +73,23 @@ class SimulationResults(Repository): :return: None """ try: - if city_id is not None: - self.session.query(Model).filter(Model.name == name, Model.city_id == city_id).update( + with Session(self.engine) as session: + if city_id is not None: + session.query(Model).filter(Model.name == name, Model.city_id == city_id).update( { 'values': values, 'updated': datetime.datetime.utcnow() }) - self.session.commit() - elif city_object_id is not None: - self.session.query(Model).filter(Model.name == name, Model.city_object_id == city_object_id).update( - { - 'values': values, - 'updated': datetime.datetime.utcnow() - }) - self.session.commit() - else: - raise NotImplementedError('Missing either city_id or city_object_id') + session.commit() + elif city_object_id is not None: + session.query(Model).filter(Model.name == name, Model.city_object_id == city_object_id).update( + { + 'values': values, + 'updated': datetime.datetime.utcnow() + }) + session.commit() + else: + raise NotImplementedError('Missing either city_id or city_object_id') except SQLAlchemyError as err: logging.error('Error while updating city object %s', err) raise SQLAlchemyError from err @@ -100,14 +103,15 @@ class SimulationResults(Repository): :return: None """ try: - if city_id is not None: - self.session.query(Model).filter(Model.name == name, Model.city_id == city_id).delete() - self.session.commit() - elif city_object_id is not None: - self.session.query(Model).filter(Model.name == name, Model.city_object_id == city_object_id).delete() - self.session.commit() - else: - raise NotImplementedError('Missing either city_id or city_object_id') + with Session(self.engine) as session: + if city_id is not None: + session.query(Model).filter(Model.name == name, Model.city_id == city_id).delete() + session.commit() + elif city_object_id is not None: + session.query(Model).filter(Model.name == name, Model.city_object_id == city_object_id).delete() + session.commit() + else: + raise NotImplementedError('Missing either city_id or city_object_id') except SQLAlchemyError as err: logging.error('Error while deleting application: %s', err) raise SQLAlchemyError from err @@ -119,7 +123,8 @@ class SimulationResults(Repository): :return: [City] with the provided city_id """ try: - return self.session.execute(select(City).where(City.id == city_id)).first() + with Session(self.engine) as session: + return session.execute(select(City).where(City.id == city_id)).first() except SQLAlchemyError as err: logging.error('Error while fetching city by city_id: %s', err) raise SQLAlchemyError from err @@ -131,7 +136,8 @@ class SimulationResults(Repository): :return: [CityObject] with the provided city_object_id """ try: - return self.session.execute(select(CityObject).where(CityObject.id == city_object_id)).first() + with Session(self.engine) as session: + return session.execute(select(CityObject).where(CityObject.id == city_object_id)).first() except SQLAlchemyError as err: logging.error('Error while fetching city by city_id: %s', err) raise SQLAlchemyError from err @@ -145,18 +151,43 @@ class SimulationResults(Repository): :return: [SimulationResult] """ try: - result_set = self.session.execute(select(Model).where(or_( - Model.city_id == city_id, - Model.city_object_id == city_object_id - ))) - results = [r[0] for r in result_set] - if not result_names: - return results - filtered_results = [] - for result in results: - if result.name in result_names: - filtered_results.append(result) - return filtered_results + with Session(self.engine) as session: + result_set = session.execute(select(Model).where(or_( + Model.city_id == city_id, + Model.city_object_id == city_object_id + ))) + results = [r[0] for r in result_set] + if not result_names: + return results + filtered_results = [] + for result in results: + if result.name in result_names: + filtered_results.append(result) + return filtered_results except SQLAlchemyError as err: logging.error('Error while fetching city by city_id: %s', err) raise SQLAlchemyError from err + + def get_simulation_results_by_city_object_id_and_names(self, city_object_id, result_names=None) -> [Model]: + """ + Fetch the simulation results based in the city_object_id with the given names or all + :param city_object_id: the city object id + :param result_names: if given filter the results + :return: [SimulationResult] + """ + try: + with Session(self.engine) as session: + result_set = session.execute(select(Model).where( + Model.city_object_id == city_object_id + )) + results = [r[0] for r in result_set] + if not result_names: + return results + filtered_results = [] + for result in results: + if result.name in result_names: + filtered_results.append(result) + return filtered_results + except SQLAlchemyError as err: + logging.error('Error while fetching city by city_id: %s', err) + raise SQLAlchemyError from err \ No newline at end of file diff --git a/hub/persistence/repositories/user.py b/hub/persistence/repositories/user.py index 51101858..4467f7cb 100644 --- a/hub/persistence/repositories/user.py +++ b/hub/persistence/repositories/user.py @@ -9,6 +9,7 @@ import logging from sqlalchemy import select from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.orm import Session from hub.helpers.auth import Auth from hub.persistence.repository import Repository @@ -49,10 +50,11 @@ class User(Repository): pass try: user = Model(name=name, password=Auth.hash_password(password), role=role, application_id=application_id) - self.session.add(user) - self.session.flush() - self.session.commit() - self.session.refresh(user) + with Session(self.engine) as session: + session.add(user) + session.flush() + session.commit() + session.refresh(user) return user.id except SQLAlchemyError as err: logging.error('An error occurred while creating user %s', err) @@ -68,13 +70,14 @@ class User(Repository): :return: None """ try: - self.session.query(Model).filter(Model.id == user_id).update({ - 'name': name, - 'password': Auth.hash_password(password), - 'role': role, - 'updated': datetime.datetime.utcnow() - }) - self.session.commit() + with Session(self.engine) as session: + session.query(Model).filter(Model.id == user_id).update({ + 'name': name, + 'password': Auth.hash_password(password), + 'role': role, + 'updated': datetime.datetime.utcnow() + }) + session.commit() except SQLAlchemyError as err: logging.error('Error while updating user: %s', err) raise SQLAlchemyError from err @@ -86,8 +89,9 @@ class User(Repository): :return: None """ try: - self.session.query(Model).filter(Model.id == user_id).delete() - self.session.commit() + with Session(self.engine) as session: + session.query(Model).filter(Model.id == user_id).delete() + session.commit() except SQLAlchemyError as err: logging.error('Error while fetching user: %s', err) raise SQLAlchemyError from err @@ -100,10 +104,12 @@ class User(Repository): :return: User matching the search criteria or None """ try: - user = self.session.execute( - select(Model).where(Model.name == name, Model.application_id == application_id) - ).first() - return user[0] + with Session(self.engine) as session: + user = session.execute( + select(Model).where(Model.name == name, Model.application_id == application_id) + ).first() + session.commit() + return user[0] except SQLAlchemyError as err: logging.error('Error while fetching user by name and application: %s', err) raise SQLAlchemyError from err @@ -120,12 +126,13 @@ class User(Repository): :return: User """ try: - user = self.session.execute( - select(Model).where(Model.name == name, Model.application_id == application_id) - ).first() - if user: - if Auth.check_password(password, user[0].password): - return user[0] + with Session(self.engine) as session: + user = session.execute( + select(Model).where(Model.name == name, Model.application_id == application_id) + ).first() + if user: + if Auth.check_password(password, user[0].password): + return user[0] except SQLAlchemyError as err: logging.error('Error while fetching user by name: %s', err) raise SQLAlchemyError from err @@ -140,10 +147,11 @@ class User(Repository): :return: User """ try: - application = self.session.execute( - select(ApplicationModel).where(ApplicationModel.application_uuid == application_uuid) - ).first() - return self.get_by_name_application_id_and_password(name, password, application[0].id) + with Session(self.engine) as session: + application = session.execute( + select(ApplicationModel).where(ApplicationModel.application_uuid == application_uuid) + ).first() + return self.get_by_name_application_id_and_password(name, password, application[0].id) except SQLAlchemyError as err: logging.error('Error while fetching user by name: %s', err) raise SQLAlchemyError from err diff --git a/hub/persistence/repository.py b/hub/persistence/repository.py index 97ed9f91..5a3b4e26 100644 --- a/hub/persistence/repository.py +++ b/hub/persistence/repository.py @@ -6,7 +6,6 @@ Project Coder Peter Yefi peteryefi@gmail.com """ import logging from sqlalchemy import create_engine -from sqlalchemy.orm import Session from hub.persistence.configuration import Configuration @@ -19,6 +18,5 @@ class Repository: try: self.configuration = Configuration(db_name, dotenv_path, app_env) self.engine = create_engine(self.configuration.connection_string) - self.session = Session(self.engine) except ValueError as err: logging.error('Missing value for credentials: %s', err) diff --git a/hub/version.py b/hub/version.py index b04fde01..3021db4a 100644 --- a/hub/version.py +++ b/hub/version.py @@ -1,4 +1,4 @@ """ Hub version number """ -__version__ = '0.1.8.4' +__version__ = '0.1.8.34' diff --git a/setup.py b/setup.py index 3d0e70e2..c44df2fd 100644 --- a/setup.py +++ b/setup.py @@ -18,24 +18,22 @@ with open(version) as f: exec(f.read(), main_ns) - - setup( name='cerc-hub', version=main_ns['__version__'], - description="CERC Hub consist in a set of classes (Central data model), importers and exporters to help researchers " - "to create better and sustainable cities", - long_description="CERC Hub consist in a set of classes (Central data model), importers and exporters to help " - "researchers to create better and sustainable cities.\n\nDevelop at Concordia university in canada " - "as part of the research group from the next generation cities institute our aim among others it's " + description="CERC Hub consist of a set of classes (Central data model), importers and exporters to help researchers " + "to create better and more sustainable cities", + long_description="CERC Hub consist of a set of classes (Central data model), importers and exporters to help " + "researchers to create better and more sustainable cities.\n\nDeveloped at Concordia university in Canada " + "as part of the research group from the Next Generation Cities Institute, our aim among others is " "to provide a comprehensive set of tools to help researchers and urban developers to make decisions " "to improve the livability and efficiency of our cities", classifiers=[ - "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - ], - include_package_data=True, + "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + ], + include_package_data=True, packages=['hub', 'hub.catalog_factories', 'hub.catalog_factories.construction', @@ -88,7 +86,8 @@ setup( data_files=[ ('hub', glob.glob('requirements.txt')), ('hub/config', glob.glob('hub/config/*.ini')), - ('hub/catalog_factories/greenery/ecore_greenery', glob.glob('hub/catalog_factories/greenery/ecore_greenery/*.ecore')), + ('hub/catalog_factories/greenery/ecore_greenery', + glob.glob('hub/catalog_factories/greenery/ecore_greenery/*.ecore')), ('hub/data/construction', glob.glob('hub/data/construction/*')), ('hub/data/costs', glob.glob('hub/data/costs/montreal_costs.xml')), ('hub/data/customized_imports', glob.glob('hub/data/customized_imports/ashrae_archetypes.xml')), @@ -109,4 +108,4 @@ setup( ('hub/exports/building_energy/idf_files', glob.glob('hub/exports/building_energy/idf_files/*.idd')) ], -) +) \ No newline at end of file diff --git a/tests/test_db_factory.py b/tests/test_db_factory.py index 94b599a7..fe5b329e 100644 --- a/tests/test_db_factory.py +++ b/tests/test_db_factory.py @@ -103,16 +103,16 @@ class Control: app_env='TEST', dotenv_path=dotenv_path) - self._application_uuid = '60b7fc1b-f389-4254-9ffd-22a4cf32c7a3' + self._application_uuid = 'b9e0ce80-1218-410c-8a64-9d9b7026aad8' self._application_id = 1 self._user_id = 1 self._application_id = self._database.persist_application( - 'City_layers', - 'City layers test user', + 'test', + 'test', self.application_uuid ) - self._user_id = self._database.create_user('city_layers', self._application_id, 'city_layers', UserRoles.Admin) + self._user_id = self._database.create_user('test', self._application_id, 'test', UserRoles.Admin) self._pickle_path = Path('tests_data/pickle_path.bz2').resolve() @@ -248,36 +248,36 @@ TestDBFactory for x in building.onsite_electrical_production[cte.MONTH]] yearly_on_site_electrical_production = [x * cte.WATTS_HOUR_TO_JULES for x in building.onsite_electrical_production[cte.YEAR]] - results = json.dumps({cte.INSEL_MEB: [ - {'monthly_cooling_peak_load': monthly_cooling_peak_load}, - {'yearly_cooling_peak_load': yearly_cooling_peak_load}, - {'monthly_heating_peak_load': monthly_heating_peak_load}, - {'yearly_heating_peak_load': yearly_heating_peak_load}, - {'monthly_lighting_peak_load': monthly_lighting_peak_load}, - {'yearly_lighting_peak_load': yearly_lighting_peak_load}, - {'monthly_appliances_peak_load': monthly_appliances_peak_load}, - {'yearly_appliances_peak_load': yearly_appliances_peak_load}, - {'monthly_cooling_demand': monthly_cooling_demand}, - {'yearly_cooling_demand': yearly_cooling_demand}, - {'monthly_heating_demand': monthly_heating_demand}, - {'yearly_heating_demand': yearly_heating_demand}, - {'monthly_lighting_electrical_demand': monthly_lighting_electrical_demand}, - {'yearly_lighting_electrical_demand': yearly_lighting_electrical_demand}, - {'monthly_appliances_electrical_demand': monthly_appliances_electrical_demand}, - {'yearly_appliances_electrical_demand': yearly_appliances_electrical_demand}, - {'monthly_domestic_hot_water_heat_demand': monthly_domestic_hot_water_heat_demand}, - {'yearly_domestic_hot_water_heat_demand': yearly_domestic_hot_water_heat_demand}, - {'monthly_heating_consumption': monthly_heating_consumption}, - {'yearly_heating_consumption': yearly_heating_consumption}, - {'monthly_cooling_consumption': monthly_cooling_consumption}, - {'yearly_cooling_consumption': yearly_cooling_consumption}, - {'monthly_domestic_hot_water_consumption': monthly_domestic_hot_water_consumption}, - {'yearly_domestic_hot_water_consumption': yearly_domestic_hot_water_consumption}, - {'monthly_distribution_systems_electrical_consumption': monthly_distribution_systems_electrical_consumption}, - {'yearly_distribution_systems_electrical_consumption': yearly_distribution_systems_electrical_consumption}, - {'monthly_on_site_electrical_production': monthly_on_site_electrical_production}, - {'yearly_on_site_electrical_production': yearly_on_site_electrical_production} - ]}) + results = {cte.INSEL_MEB: { + 'monthly_cooling_peak_load': monthly_cooling_peak_load, + 'yearly_cooling_peak_load': yearly_cooling_peak_load, + 'monthly_heating_peak_load': monthly_heating_peak_load, + 'yearly_heating_peak_load': yearly_heating_peak_load, + 'monthly_lighting_peak_load': monthly_lighting_peak_load, + 'yearly_lighting_peak_load': yearly_lighting_peak_load, + 'monthly_appliances_peak_load': monthly_appliances_peak_load, + 'yearly_appliances_peak_load': yearly_appliances_peak_load, + 'monthly_cooling_demand': monthly_cooling_demand, + 'yearly_cooling_demand': yearly_cooling_demand, + 'monthly_heating_demand': monthly_heating_demand, + 'yearly_heating_demand': yearly_heating_demand, + 'monthly_lighting_electrical_demand': monthly_lighting_electrical_demand, + 'yearly_lighting_electrical_demand': yearly_lighting_electrical_demand, + 'monthly_appliances_electrical_demand': monthly_appliances_electrical_demand, + 'yearly_appliances_electrical_demand': yearly_appliances_electrical_demand, + 'monthly_domestic_hot_water_heat_demand': monthly_domestic_hot_water_heat_demand, + 'yearly_domestic_hot_water_heat_demand': yearly_domestic_hot_water_heat_demand, + 'monthly_heating_consumption': monthly_heating_consumption, + 'yearly_heating_consumption': yearly_heating_consumption, + 'monthly_cooling_consumption': monthly_cooling_consumption, + 'yearly_cooling_consumption': yearly_cooling_consumption, + 'monthly_domestic_hot_water_consumption': monthly_domestic_hot_water_consumption, + 'yearly_domestic_hot_water_consumption': yearly_domestic_hot_water_consumption, + 'monthly_distribution_systems_electrical_consumption': monthly_distribution_systems_electrical_consumption, + 'yearly_distribution_systems_electrical_consumption': yearly_distribution_systems_electrical_consumption, + 'monthly_on_site_electrical_production': monthly_on_site_electrical_production, + 'yearly_on_site_electrical_production': yearly_on_site_electrical_production + }} db_building_id = _building.id city_objects_id.append(db_building_id) diff --git a/tests/test_exports.py b/tests/test_exports.py index 3bb0f91f..e7197901 100644 --- a/tests/test_exports.py +++ b/tests/test_exports.py @@ -5,19 +5,20 @@ Copyright © 2022 Concordia CERC group Project Coder Guille Gutierrez guillermo.gutierrezmorote@concordia.ca Code contributors: Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ - -import logging.handlers +import json +import os from pathlib import Path from unittest import TestCase -from hub.imports.geometry_factory import GeometryFactory -from hub.helpers.dictionaries import Dictionaries -from hub.imports.construction_factory import ConstructionFactory -from hub.imports.usage_factory import UsageFactory -from hub.imports.weather_factory import WeatherFactory -from hub.exports.exports_factory import ExportsFactory -from hub.exports.energy_building_exports_factory import EnergyBuildingsExportsFactory + import hub.helpers.constants as cte from hub.city_model_structure.city import City +from hub.exports.energy_building_exports_factory import EnergyBuildingsExportsFactory +from hub.exports.exports_factory import ExportsFactory +from hub.helpers.dictionaries import Dictionaries +from hub.imports.construction_factory import ConstructionFactory +from hub.imports.geometry_factory import GeometryFactory +from hub.imports.usage_factory import UsageFactory +from hub.imports.weather_factory import WeatherFactory class TestExports(TestCase): @@ -66,12 +67,7 @@ class TestExports(TestCase): def _export(self, export_type, from_pickle=False): self._complete_city = self._get_complete_city(from_pickle) - try: - ExportsFactory(export_type, self._complete_city, self._output_path).export() - except ValueError as err: - if export_type != 'stl': - logging.warning('No backend export for STL test, skipped') - raise err + ExportsFactory(export_type, self._complete_city, self._output_path, base_uri='../glb').export() def _export_building_energy(self, export_type, from_pickle=False): self._complete_city = self._get_complete_city(from_pickle) @@ -83,11 +79,38 @@ class TestExports(TestCase): """ self._export('obj', False) - def test_stl_export(self): + def test_cesiumjs_tileset_export(self): """ - export to stl + export to cesiumjs tileset """ - self._export('stl', False) + self._export('cesiumjs_tileset', False) + tileset = Path(self._output_path / f'{self._city.name}.json') + self.assertTrue(tileset.exists()) + with open(tileset, 'r') as f: + json_tileset = json.load(f) + self.assertEqual(1, len(json_tileset['root']['children']), "Wrong number of children") + + def test_glb_export(self): + """ + export to glb format + """ + self._export('glb', False) + for building in self._city.buildings: + glb_file = Path(self._output_path / f'{building.name}.glb') + self.assertTrue(glb_file.exists(), f'{building.name} Building glb wasn\'t correctly generated') + + def test_geojson_export(self): + self._export('geojson', False) + geojson_file = Path(self._output_path / f'{self._city.name}.geojson') + self.assertTrue(geojson_file.exists(), f'{geojson_file} doesn\'t exists') + with open(geojson_file, 'r') as f: + geojson = json.load(f) + self.assertEqual(1, len(geojson['features']), 'Wrong number of buildings') + geometry = geojson['features'][0]['geometry'] + self.assertEqual('Polygon', geometry['type'], 'Wrong geometry type') + self.assertEqual(1, len(geometry['coordinates']), 'Wrong polygon structure') + self.assertEqual(11, len(geometry['coordinates'][0]), 'Wrong number of vertices') + os.unlink(geojson_file) # todo: this test need to cover a multipolygon example too def test_energy_ade_export(self): """ @@ -125,4 +148,3 @@ class TestExports(TestCase): EnergyBuildingsExportsFactory('idf', city, self._output_path).export() except Exception: self.fail("Idf ExportsFactory raised ExceptionType unexpectedly!") - diff --git a/tests/test_results_import.py b/tests/test_results_import.py index 1befac19..76be5876 100644 --- a/tests/test_results_import.py +++ b/tests/test_results_import.py @@ -40,7 +40,7 @@ class TestResultsImport(TestCase): function_to_hub=Dictionaries().montreal_function_to_hub_function).city ConstructionFactory('nrcan', self._city).enrich() - UsageFactory('nrcan', self._city).enrich() + UsageFactory('comnet', self._city).enrich() def test_sra_import(self): ExportsFactory('sra', self._city, self._output_path).export()