diff --git a/catalog_factories/usage/comnet_catalog.py b/catalog_factories/usage/comnet_catalog.py index 0667d94d..a68ea03c 100644 --- a/catalog_factories/usage/comnet_catalog.py +++ b/catalog_factories/usage/comnet_catalog.py @@ -193,7 +193,6 @@ class ComnetCatalog(Catalog): process_data[usage_type] = usage_parameters[24:26].values.tolist() schedules_key[usage_type] = usage_parameters[27:28].item() - return {'lighting': lighting_data, 'plug loads': plug_loads_data, 'occupancy': occupancy_data, diff --git a/catalog_factories/usage/nrcan_catalog.py b/catalog_factories/usage/nrcan_catalog.py new file mode 100644 index 00000000..949cb3d2 --- /dev/null +++ b/catalog_factories/usage/nrcan_catalog.py @@ -0,0 +1,170 @@ +""" +NRCAN usage catalog +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2022 Concordia CERC group +Project Coder Guille Gutierrez guillermo.gutierrezmorote@concordia.ca +""" + +import json +import urllib.request +import xmltodict + +import helpers.constants as cte +from catalog_factories.catalog import Catalog +from catalog_factories.data_models.usages.appliances import Appliances +from catalog_factories.data_models.usages.content import Content +from catalog_factories.data_models.usages.lighting import Lighting +from catalog_factories.data_models.usages.ocupancy import Occupancy +from catalog_factories.data_models.usages.schedule import Schedule +from catalog_factories.data_models.usages.thermal_control import ThermalControl +from catalog_factories.data_models.usages.usage import Usage +from catalog_factories.usage.usage_helper import UsageHelper + + +class NrcanCatalog(Catalog): + def __init__(self, path): + path = str(path / 'nrcan.xml') + self._content = None + self._schedules = {} + with open(path) as xml: + self._metadata = xmltodict.parse(xml.read()) + self._base_url = self._metadata['nrcan']['@base_url'] + self._load_schedules() + self._content = Content(self._load_archetypes()) + + def _calculate_hours_day(self, function): + # todo: pilar need to check how to calculate this value + return 24 + + @staticmethod + def _extract_schedule(raw): + nrcan_schedule_type = raw['category'] + if 'Heating' in raw['name']: + nrcan_schedule_type = f'{nrcan_schedule_type} Heating' + elif 'Cooling' in raw['name']: + nrcan_schedule_type = f'{nrcan_schedule_type} Cooling' + if nrcan_schedule_type not in UsageHelper().nrcan_schedule_type_to_hub_schedule_type: + return None + hub_type = UsageHelper().nrcan_schedule_type_to_hub_schedule_type[nrcan_schedule_type] + data_type = UsageHelper().nrcan_data_type_to_hub_data_type[raw['units']] + time_step = UsageHelper().nrcan_time_to_hub_time[raw['type']] + # nrcan only uses yearly range for the schedules + time_range = cte.YEAR + day_types = UsageHelper().nrcan_day_type_to_hub_days[raw['day_types']] + return Schedule(hub_type, raw['values'], data_type, time_step, time_range, day_types) + + def _load_schedules(self): + usage = self._metadata['nrcan']['standards']['usage'] + url = f'{self._base_url}{usage["schedules_location"]}' + with urllib.request.urlopen(url) as json_file: + schedules_type = json.load(json_file) + for schedule_type in schedules_type['tables']['schedules']['table']: + schedule = NrcanCatalog._extract_schedule(schedule_type) + if schedule is not None: + self._schedules[schedule_type['name']] = schedule + + def _get_schedule(self, name): + if name in self._schedules: + return self._schedules[name] + + def _load_archetypes(self): + usages = [] + usage = self._metadata['nrcan']['standards']['usage'] + url = f'{self._base_url}{usage["space_types_location"]}' + with urllib.request.urlopen(url) as json_file: + space_types = json.load(json_file)['tables']['space_types']['table'] + space_types = [st for st in space_types if st['building_type'] == 'Space Function'] + for space_type in space_types: + usage_type = space_type['space_type'] + mechanical_air_change = space_type['ventilation_air_changes'] + ventilation_rate = space_type['ventilation_per_area'] + if ventilation_rate == 0: + ventilation_rate = space_type['ventilation_per_person'] + hours_day = self._calculate_hours_day(usage_type) + days_year = 365 + occupancy_schedule_name = space_type['occupancy_schedule'] + lighting_schedule_name = space_type['lighting_schedule'] + appliance_schedule_name = space_type['electric_equipment_schedule'] + # thermal control + heating_setpoint_schedule_name = space_type['heating_setpoint_schedule'] + cooling_setpoint_schedule_name = space_type['cooling_setpoint_schedule'] + occupancy_schedule = self._get_schedule(occupancy_schedule_name) + lighting_schedule = self._get_schedule(lighting_schedule_name) + appliance_schedule = self._get_schedule(appliance_schedule_name) + heating_schedule = self._get_schedule(heating_setpoint_schedule_name) + cooling_schedule = self._get_schedule(cooling_setpoint_schedule_name) + + occupancy_density = space_type['occupancy_per_area'] + lighting_density = space_type['lighting_per_area'] + lighting_radiative_fraction = space_type['lighting_fraction_radiant'] + if lighting_radiative_fraction is not None: + lighting_convective_fraction = 1 - lighting_radiative_fraction + lighting_latent_fraction = 0 + appliances_density = space_type['electric_equipment_per_area'] + appliances_radiative_fraction = space_type['electric_equipment_fraction_radiant'] + if appliances_radiative_fraction is not None: + appliances_convective_fraction = 1 - appliances_radiative_fraction + appliances_latent_fraction = space_type['electric_equipment_fraction_latent'] + + occupancy = Occupancy(occupancy_density, 0, 0, 0, occupancy_schedule) + lighting = Lighting(lighting_density, + lighting_convective_fraction, + lighting_radiative_fraction, + lighting_latent_fraction, + lighting_schedule) + appliances = Appliances(appliances_density, + appliances_convective_fraction, + appliances_radiative_fraction, + appliances_latent_fraction, + appliance_schedule) + if heating_schedule is not None: + thermal_control = ThermalControl(max(heating_schedule.values), + min(heating_schedule.values), + min(cooling_schedule.values), + None, + heating_schedule, + cooling_schedule) + else: + thermal_control = ThermalControl(None, + None, + None, + None, + None, + None) + usages.append(Usage(usage_type, + hours_day, + days_year, + mechanical_air_change, + ventilation_rate, + occupancy, + lighting, + appliances, + thermal_control)) + return usages + + def names(self, category=None): + """ + Get the catalog elements names + :parm: for usage catalog category filter does nothing as there is only one category (usages) + """ + _names = {'usages': []} + for usage in self._content.usages: + _names['usages'].append(usage.usage) + return _names + + def entries(self, category=None): + """ + Get the catalog elements + :parm: for usage catalog category filter does nothing as there is only one category (usages) + """ + return self._content + + def get_entry(self, name): + """ + Get one catalog element by names + :parm: entry name + """ + for usage in self._content.usages: + if usage.usage.lower() == name.lower(): + return usage + raise IndexError(f"{name} doesn't exists in the catalog") diff --git a/catalog_factories/usage/usage_helper.py b/catalog_factories/usage/usage_helper.py index 4a397566..93c6fa04 100644 --- a/catalog_factories/usage/usage_helper.py +++ b/catalog_factories/usage/usage_helper.py @@ -13,6 +13,51 @@ class UsageHelper: """ Usage helper class """ + _nrcan_schedule_type_to_hub_schedule_type = { + 'Lighting': cte.LIGHTING, + 'Occupancy': cte.OCCUPANCY, + 'Equipment': cte.APPLIANCES, + 'Thermostat Setpoint Cooling': cte.COOLING_SET_POINT, # Compose 'Thermostat Setpoint' + 'Cooling' + 'Thermostat Setpoint Heating': cte.HEATING_SET_POINT, # Compose 'Thermostat Setpoint' + 'Heating' + } + _nrcan_data_type_to_hub_data_type = { + 'FRACTION': cte.FRACTION, + 'ON_OFF': cte.ON_OFF, + 'TEMPERATURE': cte.ANY_NUMBER + } + + _nrcan_time_to_hub_time = { + 'Hourly': cte.HOUR, + 'Constant': cte.CONSTANT + } + + _nrcan_day_type_to_hub_days = { + 'Default|Wkdy': [cte.MONDAY, cte.TUESDAY, cte.WEDNESDAY, cte.THURSDAY, cte.FRIDAY], + 'Sun|Hol': [cte.SUNDAY, cte.HOLIDAY], + 'Sat': [cte.SATURDAY], + 'Default|WntrDsn|SmrDsn': [cte.MONDAY, + cte.TUESDAY, + cte.WEDNESDAY, + cte.THURSDAY, + cte.FRIDAY, + cte.SATURDAY, + cte.SUNDAY, + cte.HOLIDAY, + cte.WINTER_DESIGN_DAY, + cte.SUMMER_DESIGN_DAY], + 'Default': [cte.MONDAY, + cte.TUESDAY, + cte.WEDNESDAY, + cte.THURSDAY, + cte.FRIDAY, + cte.SATURDAY, + cte.SUNDAY, + cte.HOLIDAY, + cte.WINTER_DESIGN_DAY, + cte.SUMMER_DESIGN_DAY] + + } + _usage_to_hft = { cte.RESIDENTIAL: 'residential', cte.SINGLE_FAMILY_HOUSE: 'Single family house', @@ -71,7 +116,7 @@ class UsageHelper: cte.GREEN_HOUSE: cte.GREEN_HOUSE, cte.NON_HEATED: cte.NON_HEATED } - + _comnet_data_type_to_hub_data_type = { 'Fraction': cte.FRACTION, 'OnOff': cte.ON_OFF, @@ -93,6 +138,22 @@ class UsageHelper: 'C-14 Gymnasium': 'C-14 Gymnasium' } + @property + def nrcan_day_type_to_hub_days(self): + return self._nrcan_day_type_to_hub_days + + @property + def nrcan_schedule_type_to_hub_schedule_type(self): + return self._nrcan_schedule_type_to_hub_schedule_type + + @property + def nrcan_data_type_to_hub_data_type(self): + return self._nrcan_data_type_to_hub_data_type + + @property + def nrcan_time_to_hub_time(self): + return self._nrcan_time_to_hub_time + @property def comnet_data_type_to_hub_data_type(self): return self._comnet_data_type_to_hub_data_type diff --git a/catalog_factories/usage_catalog_factory.py b/catalog_factories/usage_catalog_factory.py index 7f56a333..f7c20265 100644 --- a/catalog_factories/usage_catalog_factory.py +++ b/catalog_factories/usage_catalog_factory.py @@ -8,6 +8,7 @@ Project Coder Guille Gutierrez guillermo.gutierrezmorote@concordia.ca from pathlib import Path from typing import TypeVar from catalog_factories.usage.comnet_catalog import ComnetCatalog +from catalog_factories.usage.nrcan_catalog import NrcanCatalog Catalog = TypeVar('Catalog') @@ -25,6 +26,14 @@ class UsageCatalogFactory: """ return ComnetCatalog(self._path) + @property + def _nrcan(self): + """ + Retrieve NRCAN catalog + """ + # nrcan retrieves the data directly from github + return NrcanCatalog(self._path) + @property def catalog(self) -> Catalog: """ diff --git a/data/usage/nrcan.xml b/data/usage/nrcan.xml new file mode 100644 index 00000000..5f160c9a --- /dev/null +++ b/data/usage/nrcan.xml @@ -0,0 +1,9 @@ + + + + + NECB2020/data/space_types.json + NECB2015/data/schedules.json + + + \ No newline at end of file diff --git a/unittests/test_usage_catalog.py b/unittests/test_usage_catalog.py index 171f0eef..2cc5dfe5 100644 --- a/unittests/test_usage_catalog.py +++ b/unittests/test_usage_catalog.py @@ -14,4 +14,10 @@ class TestConstructionCatalog(TestCase): catalog = UsageCatalogFactory('comnet').catalog self.assertIsNotNone(catalog, 'catalog is none') content = catalog.entries() - self.assertEqual(len(content.usages), 32, 'Wrong number of usages') + self.assertEqual(32, len(content.usages), 'Wrong number of usages') + + def test_nrcan_catalog(self): + catalog = UsageCatalogFactory('nrcan').catalog + self.assertIsNotNone(catalog, 'catalog is none') + content = catalog.entries() + self.assertEqual(274, len(content.usages), 'Wrong number of usages')