From 60b725dad67087c8f8d52d21c5f0128e0351c592 Mon Sep 17 00:00:00 2001 From: guille Date: Tue, 15 Nov 2022 08:38:41 -0500 Subject: [PATCH 1/2] Partial nrcan usage implementation --- catalog_factories/usage/comnet_catalog.py | 1 - catalog_factories/usage/nrcan_catalog.py | 143 +++++++++++++++++++++ catalog_factories/usage/usage_helper.py | 62 ++++++++- catalog_factories/usage_catalog_factory.py | 9 ++ data/usage/nrcan.xml | 9 ++ unittests/test_usage_catalog.py | 4 + 6 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 catalog_factories/usage/nrcan_catalog.py create mode 100644 data/usage/nrcan.xml diff --git a/catalog_factories/usage/comnet_catalog.py b/catalog_factories/usage/comnet_catalog.py index e2fb294d..748d0b36 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..3ae172d1 --- /dev/null +++ b/catalog_factories/usage/nrcan_catalog.py @@ -0,0 +1,143 @@ +""" +NRCAN usage catalog +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2022 Concordia CERC group +Project Coder Guille Gutierrez guillermo.gutierrezmorote@concordia.ca +""" +from typing import Dict +import pandas as pd +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 +from helpers.configuration_helper import ConfigurationHelper as ch + + +class NrcanCatalog(Catalog): + def __init__(self, path): + path = str(path / 'nrcan.xml') + self._content = None + self._schedules = [] + self._lightings = [] + self._occupancies = [] + self._internal_gains = [] + self._appliances = [] + self._thermal_controls = [] + usages = [] + with open(path) as xml: + self._metadata = xmltodict.parse(xml.read()) + self._base_url = self._metadata['nrcan']['@base_url'] + self._load_schedules() + self._load_archetypes() + + def _calculate_hours_day(self, function): + days = [cte.MONDAY, cte.TUESDAY, cte.WEDNESDAY, cte.THURSDAY, cte.FRIDAY, cte.SATURDAY, cte.SUNDAY, cte.HOLIDAY] + number_of_days_per_type = [51, 50, 50, 50, 50, 52, 52, 10] + total = 0 + for schedule in self._schedules[function]['HVAC Avail']: + yearly_days = number_of_days_per_type[days.index(schedule.day_types[0])] + for value in schedule.values: + total += value * yearly_days + return total / 365 + + @staticmethod + def _extract_schedule(raw): + nrcan_schedule_type = raw['category'] + print(raw) + 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.append({schedule_type['name'], schedule}) + + def _get_schedule(self, name): + if name in self._schedules: + print(f'dictionary found {name}: {self._schedule[name]}') + return self._schedule[name] + + def _load_archetypes(self): + 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 = 0 # self._calculate_hours_day(usage_type) + days_year = 365 + + occupancy_schedule_name = space_type['occupancy_schedule'] + self._load_schedule(occupancy_schedule_name) + lighting_schedule_name = space_type['lighting_schedule'] + appliances_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'] + print(usage_type, mechanical_air_change, ventilation_rate, hours_day, days_year, occupancy_schedule_name, lighting_schedule_name, appliances_schedule_name, heating_setpoint_schedule_name, cooling_setpoint_schedule_name) + + def _read_archetype_file(self) -> Dict: + """ + reads xml files containing metadata to access to the json file? + :return : Dict + """ + print(self._metadata) + return + + 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..f3c4bf2b 100644 --- a/catalog_factories/usage/usage_helper.py +++ b/catalog_factories/usage/usage_helper.py @@ -13,6 +13,50 @@ 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 + } + + _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 +115,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 +137,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..78d26f98 100644 --- a/unittests/test_usage_catalog.py +++ b/unittests/test_usage_catalog.py @@ -15,3 +15,7 @@ class TestConstructionCatalog(TestCase): self.assertIsNotNone(catalog, 'catalog is none') content = catalog.entries() self.assertEqual(len(content.usages), 32, 'Wrong number of usages') + + def test_nrcan_catalog(self): + catalog = UsageCatalogFactory('nrcan').catalog + self.assertIsNotNone(catalog, 'catalog is none') \ No newline at end of file From 1db5b02847588a939182153c9fcddf65a9f30c4d Mon Sep 17 00:00:00 2001 From: guille Date: Mon, 21 Nov 2022 12:46:39 -0500 Subject: [PATCH 2/2] partial completion of nrcan catalog --- catalog_factories/usage/nrcan_catalog.py | 97 +++++++++++++++--------- catalog_factories/usage/usage_helper.py | 5 +- unittests/test_usage_catalog.py | 6 +- 3 files changed, 69 insertions(+), 39 deletions(-) diff --git a/catalog_factories/usage/nrcan_catalog.py b/catalog_factories/usage/nrcan_catalog.py index 3ae172d1..949cb3d2 100644 --- a/catalog_factories/usage/nrcan_catalog.py +++ b/catalog_factories/usage/nrcan_catalog.py @@ -4,8 +4,7 @@ SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2022 Concordia CERC group Project Coder Guille Gutierrez guillermo.gutierrezmorote@concordia.ca """ -from typing import Dict -import pandas as pd + import json import urllib.request import xmltodict @@ -20,40 +19,26 @@ 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 -from helpers.configuration_helper import ConfigurationHelper as ch class NrcanCatalog(Catalog): def __init__(self, path): path = str(path / 'nrcan.xml') self._content = None - self._schedules = [] - self._lightings = [] - self._occupancies = [] - self._internal_gains = [] - self._appliances = [] - self._thermal_controls = [] - usages = [] + 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._load_archetypes() + self._content = Content(self._load_archetypes()) def _calculate_hours_day(self, function): - days = [cte.MONDAY, cte.TUESDAY, cte.WEDNESDAY, cte.THURSDAY, cte.FRIDAY, cte.SATURDAY, cte.SUNDAY, cte.HOLIDAY] - number_of_days_per_type = [51, 50, 50, 50, 50, 52, 52, 10] - total = 0 - for schedule in self._schedules[function]['HVAC Avail']: - yearly_days = number_of_days_per_type[days.index(schedule.day_types[0])] - for value in schedule.values: - total += value * yearly_days - return total / 365 + # todo: pilar need to check how to calculate this value + return 24 @staticmethod def _extract_schedule(raw): nrcan_schedule_type = raw['category'] - print(raw) if 'Heating' in raw['name']: nrcan_schedule_type = f'{nrcan_schedule_type} Heating' elif 'Cooling' in raw['name']: @@ -76,14 +61,14 @@ class NrcanCatalog(Catalog): for schedule_type in schedules_type['tables']['schedules']['table']: schedule = NrcanCatalog._extract_schedule(schedule_type) if schedule is not None: - self._schedules.append({schedule_type['name'], schedule}) + self._schedules[schedule_type['name']] = schedule def _get_schedule(self, name): if name in self._schedules: - print(f'dictionary found {name}: {self._schedule[name]}') - return self._schedule[name] + 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: @@ -95,25 +80,67 @@ class NrcanCatalog(Catalog): ventilation_rate = space_type['ventilation_per_area'] if ventilation_rate == 0: ventilation_rate = space_type['ventilation_per_person'] - hours_day = 0 # self._calculate_hours_day(usage_type) + hours_day = self._calculate_hours_day(usage_type) days_year = 365 - occupancy_schedule_name = space_type['occupancy_schedule'] - self._load_schedule(occupancy_schedule_name) lighting_schedule_name = space_type['lighting_schedule'] - appliances_schedule_name = space_type['electric_equipment_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'] - print(usage_type, mechanical_air_change, ventilation_rate, hours_day, days_year, occupancy_schedule_name, lighting_schedule_name, appliances_schedule_name, heating_setpoint_schedule_name, cooling_setpoint_schedule_name) + 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) - def _read_archetype_file(self) -> Dict: - """ - reads xml files containing metadata to access to the json file? - :return : Dict - """ - print(self._metadata) - return + 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): """ diff --git a/catalog_factories/usage/usage_helper.py b/catalog_factories/usage/usage_helper.py index f3c4bf2b..93c6fa04 100644 --- a/catalog_factories/usage/usage_helper.py +++ b/catalog_factories/usage/usage_helper.py @@ -21,13 +21,14 @@ class UsageHelper: 'Thermostat Setpoint Heating': cte.HEATING_SET_POINT, # Compose 'Thermostat Setpoint' + 'Heating' } _nrcan_data_type_to_hub_data_type = { - 'Fraction': cte.FRACTION, + 'FRACTION': cte.FRACTION, 'ON_OFF': cte.ON_OFF, 'TEMPERATURE': cte.ANY_NUMBER } _nrcan_time_to_hub_time = { - 'Hourly': cte.HOUR + 'Hourly': cte.HOUR, + 'Constant': cte.CONSTANT } _nrcan_day_type_to_hub_days = { diff --git a/unittests/test_usage_catalog.py b/unittests/test_usage_catalog.py index 78d26f98..2cc5dfe5 100644 --- a/unittests/test_usage_catalog.py +++ b/unittests/test_usage_catalog.py @@ -14,8 +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') \ No newline at end of file + self.assertIsNotNone(catalog, 'catalog is none') + content = catalog.entries() + self.assertEqual(274, len(content.usages), 'Wrong number of usages')