diff --git a/.gitignore b/.gitignore index 3062492e..348fdf75 100644 --- a/.gitignore +++ b/.gitignore @@ -10,5 +10,5 @@ **/__pycache__/ **/.idea/ cerc_hub.egg-info -/out_files +**/out_files/ /input_files/output_buildings.geojson \ No newline at end of file diff --git a/hub/catalog_factories/cost/montreal_complete_cost_catalog.py b/hub/catalog_factories/cost/montreal_complete_cost_catalog.py new file mode 100644 index 00000000..c66668ca --- /dev/null +++ b/hub/catalog_factories/cost/montreal_complete_cost_catalog.py @@ -0,0 +1,196 @@ +""" +Cost catalog +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2023 Concordia CERC group +Project Coder Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +""" + +import xmltodict +from hub.catalog_factories.catalog import Catalog +from hub.catalog_factories.data_models.cost.archetype import Archetype +from hub.catalog_factories.data_models.cost.content import Content +from hub.catalog_factories.data_models.cost.capital_cost import CapitalCost +from hub.catalog_factories.data_models.cost.chapter import Chapter +from hub.catalog_factories.data_models.cost.item_description import ItemDescription +from hub.catalog_factories.data_models.cost.operational_cost import OperationalCost +from hub.catalog_factories.data_models.cost.fuel import Fuel +from hub.catalog_factories.data_models.cost.income import Income + + +class MontrealNewCatalog(Catalog): + """ + Montreal custom catalog class + """ + + def __init__(self, path): + path = (path / 'montreal_costs_completed.xml').resolve() + with open(path, 'r', encoding='utf-8') as xml: + self._archetypes = xmltodict.parse(xml.read(), force_list='archetype') + + # store the full catalog data model in self._content + self._content = Content(self._load_archetypes()) + + def _load_archetypes(self): + _catalog_archetypes = [] + archetypes = self._archetypes['archetypes']['archetype'] + for archetype in archetypes: + lod = float(archetype['@lod']) + function = archetype['@function'] + municipality = archetype['@municipality'] + country = archetype['@country'] + currency = archetype['currency'] + capital_cost = self.load_capital_costs(archetype) + operational_cost = self._get_operational_costs(archetype['operational_cost']) + end_of_life_cost = float(archetype['end_of_life_cost']['#text']) + construction = float(archetype['incomes']['subsidies']['construction']['#text']) + hvac = float(archetype['incomes']['subsidies']['hvac']['#text']) + photovoltaic_system = float(archetype['incomes']['subsidies']['photovoltaic']['#text']) + electricity_exports = float(archetype['incomes']['electricity_export']['#text']) / 1000 / 3600 + reduction_tax = float(archetype['incomes']['tax_reduction']['#text']) / 100 + income = Income(construction_subsidy=construction, + hvac_subsidy=hvac, + photovoltaic_subsidy=photovoltaic_system, + electricity_export=electricity_exports, + reductions_tax=reduction_tax) + _catalog_archetypes.append(Archetype(lod, + function, + municipality, + country, + currency, + capital_cost, + operational_cost, + end_of_life_cost, + income)) + return _catalog_archetypes + + @staticmethod + def item_description(item_type, item): + if 'refurbishment_cost' in item.keys(): + _refurbishment = float(item['refurbishment_cost']['#text']) + _refurbishment_unit = item['refurbishment_cost']['@cost_unit'] + _item_description = ItemDescription(item_type, + initial_investment=None, + initial_investment_unit=None, + refurbishment=_refurbishment, + refurbishment_unit=_refurbishment_unit, + reposition=None, + reposition_unit=None, + lifetime=None) + else: + _reposition = float(item['reposition']['#text']) + _reposition_unit = item['reposition']['@cost_unit'] + _investment = float(item['investment_cost']['#text']) + _investment_unit = item['investment_cost']['@cost_unit'] + _lifetime = float(item['lifetime_equipment']['#text']) + _item_description = ItemDescription(item_type, + initial_investment=_investment, + initial_investment_unit=_investment_unit, + refurbishment=None, + refurbishment_unit=None, + reposition=_reposition, + reposition_unit=_reposition_unit, + lifetime=_lifetime) + + return _item_description + + def load_capital_costs(self, archetype): + archetype_capital_costs = archetype['capital_cost'] + design_allowance = float( + archetype_capital_costs['Z_allowances_overhead_profit']['Z10_design_allowance']['#text']) / 100 + overhead_and_profit = float( + archetype_capital_costs['Z_allowances_overhead_profit']['Z20_overhead_profit']['#text']) / 100 + general_chapters = [] + shell_items = [] + service_items = [] + for category in archetype_capital_costs: + if category == 'B_shell': + items = archetype_capital_costs[category] + for item in items: + components = items[item] + for component in components: + building_item = components[component] + shell_items.append(self.item_description(component, building_item)) + general_chapters.append(Chapter(chapter_type=category, items=shell_items)) + elif category == 'D_services': + services = archetype_capital_costs[category] + for service in services: + components = services[service] + if len(components.keys()) == 1: + for component in components: + service_item = components[component] + service_items.append(self.item_description(component, service_item)) + else: + for component in components: + items = components[component] + if 'investment_cost' in items.keys(): + service_item = components[component] + service_items.append(self.item_description(component, service_item)) + else: + for item in items: + service_item = items[item] + service_items.append(self.item_description(item, service_item)) + + general_chapters.append(Chapter(chapter_type=category, items=service_items)) + capital_costs = CapitalCost(general_chapters=general_chapters, + design_allowance=design_allowance, + overhead_and_profit=overhead_and_profit) + + return capital_costs + + @staticmethod + def _get_operational_costs(entry): + fuels = [] + for item in entry['fuels']['fuel']: + fuel_type = item['@fuel_type'] + fuel_variable = float(item['variable']['#text']) + fuel_variable_units = item['variable']['@cost_unit'] + fuel_fixed_monthly = None + fuel_fixed_peak = None + if fuel_type == 'electricity': + fuel_fixed_monthly = float(item['fixed_monthly']['#text']) + fuel_fixed_peak = float(item['fixed_power']['#text']) / 1000 + elif fuel_type == 'gas': + fuel_fixed_monthly = float(item['fixed_monthly']['#text']) + fuel = Fuel(fuel_type, + fixed_monthly=fuel_fixed_monthly, + fixed_power=fuel_fixed_peak, + variable=fuel_variable, + variable_units=fuel_variable_units) + fuels.append(fuel) + heating_equipment_maintenance = float(entry['maintenance']['heating_equipment']['#text']) / 1000 + cooling_equipment_maintenance = float(entry['maintenance']['cooling_equipment']['#text']) / 1000 + photovoltaic_system_maintenance = float(entry['maintenance']['photovoltaic_system']['#text']) + co2_emissions = float(entry['co2_cost']['#text']) + _operational_cost = OperationalCost(fuels, + heating_equipment_maintenance, + cooling_equipment_maintenance, + photovoltaic_system_maintenance, + co2_emissions) + return _operational_cost + + def names(self, category=None): + """ + Get the catalog elements names + :parm: for costs catalog category filter does nothing as there is only one category (archetypes) + """ + _names = {'archetypes': []} + for archetype in self._content.archetypes: + _names['archetypes'].append(archetype.name) + return _names + + def entries(self, category=None): + """ + Get the catalog elements + :parm: for costs catalog category filter does nothing as there is only one category (archetypes) + """ + return self._content + + def get_entry(self, name): + """ + Get one catalog element by names + :parm: entry name + """ + for entry in self._content.archetypes: + if entry.name.lower() == name.lower(): + return entry + raise IndexError(f"{name} doesn't exists in the catalog") diff --git a/hub/catalog_factories/costs_catalog_factory.py b/hub/catalog_factories/costs_catalog_factory.py index de341991..e037fb2b 100644 --- a/hub/catalog_factories/costs_catalog_factory.py +++ b/hub/catalog_factories/costs_catalog_factory.py @@ -9,6 +9,7 @@ Code contributors: Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concord from pathlib import Path from typing import TypeVar from hub.catalog_factories.cost.montreal_custom_catalog import MontrealCustomCatalog +from hub.catalog_factories.cost.montreal_complete_cost_catalog import MontrealNewCatalog Catalog = TypeVar('Catalog') @@ -30,6 +31,14 @@ class CostsCatalogFactory: """ return MontrealCustomCatalog(self._path) + @property + def _montreal_new(self): + """ + Retrieve Montreal Custom catalog + """ + return MontrealNewCatalog(self._path) + + @property def catalog(self) -> Catalog: """ @@ -37,3 +46,7 @@ class CostsCatalogFactory: :return: CostCatalog """ return getattr(self, self._catalog_type, lambda: None) + + @property + def catalog_debug(self): + return MontrealNewCatalog(self._path) diff --git a/hub/data/costs/montreal_costs_completed.xml b/hub/data/costs/montreal_costs_completed.xml new file mode 100644 index 00000000..64be368c --- /dev/null +++ b/hub/data/costs/montreal_costs_completed.xml @@ -0,0 +1,328 @@ + + + CAD + + + + + 0 + + + + + 304 + + + 857.14 + + + + + 118 + + + + + + + 800 + 800 + 25 + + + + + + 622.86 + 622.86 + 25 + + + 47.62 + 47.62 + 15 + + + 47.62 + 47.62 + 15 + + + 47.62 + 47.62 + 15 + + + 47.62 + 47.62 + 15 + + + 47.62 + 47.62 + 15 + + + 47.62 + 47.62 + 15 + + + 47.62 + 47.62 + 15 + + + 622.86 + 622.86 + 15 + + + + 0 + 0 + 15 + + + 47.62 + 47.62 + 15 + + + + 47.62 + 47.62 + 15 + + + 47.62 + 47.62 + 15 + + + + + + 47.62 + 47.62 + 15 + + + 47.62 + 47.62 + 15 + + + + + 139 + 139 + 20 + + + + + 2.5 + 14 + + + + + + 12.27 + 0 + 0.075 + + + 17.71 + 0.0640 + + + 1.2 + + + 0.04 + + + + 40 + 40 + 1 + + 30 + + 6.3 + + + 20 + 1.5 + 3.6 + + 0.07 + 5 + + + + CAD + + + + + 0 + + + + + 304 + + + 857.14 + + + + + 118 + + + + + + + 800 + 800 + 25 + + + + + + 622.86 + 622.86 + 25 + + + 47.62 + 47.62 + 15 + + + 47.62 + 47.62 + 15 + + + 47.62 + 47.62 + 15 + + + 47.62 + 47.62 + 15 + + + 47.62 + 47.62 + 15 + + + 47.62 + 47.62 + 15 + + + 47.62 + 47.62 + 15 + + + 622.86 + 622.86 + 15 + + + + 0 + 0 + 15 + + + 47.62 + 47.62 + 15 + + + + 47.62 + 47.62 + 15 + + + 47.62 + 47.62 + 15 + + + + + + 47.62 + 47.62 + 15 + + + 47.62 + 47.62 + 15 + + + + + 139 + 139 + 20 + + + + + 6 + 14 + + + + + + 12.27 + 0 + 0.075 + + + 17.71 + 0.0640 + + + 1.2 + + + 0.04 + + + + 40 + 40 + 1 + + 30 + + 6.3 + + + 20 + 1.5 + 3.6 + + 0.05 + 5 + + + \ No newline at end of file diff --git a/tests/test_costs_catalog.py b/tests/test_costs_catalog.py index 8f873850..fbbc07be 100644 --- a/tests/test_costs_catalog.py +++ b/tests/test_costs_catalog.py @@ -26,3 +26,20 @@ class TestCostsCatalog(TestCase): with self.assertRaises(IndexError): catalog.get_entry('unknown') + + def test_new_costs_catalog(self): + catalog = CostsCatalogFactory('montreal_complete').catalog_debug + catalog_categories = catalog.names() + self.assertIsNotNone(catalog, 'catalog is none') + content = catalog.entries() + self.assertTrue(len(content.archetypes) == 2) + + # retrieving all the entries should not raise any exceptions + for category in catalog_categories: + for value in catalog_categories[category]: + catalog.get_entry(value) + + with self.assertRaises(IndexError): + catalog.get_entry('unknown') + print(catalog.entries()) +