""" lca_carbon_workflow module Returns the summarize of envelope and energy systems SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2024 Concordia CERC group Project developers: Alireza Adli alireza.adli@concordia.ca Mohammad Reza Seyedabadi mohammad.seyedabadi@mail.concordia.ca """ from pathlib import Path from input_geojson_content import InputGeoJsonContent from hub.imports.geometry_factory import GeometryFactory from hub.imports.construction_factory import ConstructionFactory from hub.helpers.dictionaries import Dictionaries from city_model_structure.life_cycle_assessment.access_nrcan_catalogue \ import AccessNrcanCatalog from city_model_structure.life_cycle_assessment.opening_emission \ import OpeningEmission from city_model_structure.life_cycle_assessment.envelope_emission \ import EnvelopeEmission from city_model_structure.life_cycle_assessment.lca_end_of_life_carbon \ import EndOfLifeEmission class LCACarbonWorkflow: def __init__( self, city_path, archetypes_catalog_file_name, constructions_catalog_file, catalog='nrcan', building_parameters=('height', 'year_of_construction', 'function')): """ LCACarbonWorkflow takes a number of buildings and enrich the city object using cerc-hub GeometryFactory and ConstructionFactory. Then it calculates embodied and end of life carbon emission of each building. It puts out the results of opening and envelop emission for each mentioned cycle separately. Final results will be stored in the below attributes: building_envelope_emission: building_opening_emission: building_component_emission: building_envelope_end_of_life_emission: building_opening_end_of_life_emission: building_component_end_of_life_emission: :param city_path: Either a path to the buildings (GeoJson) file or the content of such a file. :param archetypes_catalog_file_name: Path to the buildings' archetypes (JSON). :param constructions_catalog_file: Path to the construction materials data. :param catalog: Type of the catalog (in this case 'nrcan', the default argument) :param building_parameters: Parameters used for using the catalog (in this case three default arguments) """ self.file_path = Path(__file__).parent / 'input_files' / InputGeoJsonContent( city_path).content self.catalogs_path = Path(__file__).parent / 'input_files' self.archetypes_catalog_file_name = archetypes_catalog_file_name self.constructions_catalog_file = constructions_catalog_file self.nrcan_catalogs = AccessNrcanCatalog( self.catalogs_path, archetypes=self.archetypes_catalog_file_name, constructions=self.constructions_catalog_file) self.out_path = (Path(__file__).parent / 'out_files') self.handler = catalog self.height, self.year_of_construction, self.function = \ building_parameters print('[simulation start]') self.city = GeometryFactory( 'geojson', path=self.file_path, height_field=self.height, year_of_construction_field=self.year_of_construction, function_field=self.function, function_to_hub=Dictionaries().montreal_function_to_hub_function).city print(f'city created from {self.file_path}') ConstructionFactory(self.handler, self.city).enrich() self.building_envelope_emission = [] self.building_opening_emission = [] self.building_component_emission = [] self.building_envelope_end_of_life_emission = [] self.building_opening_end_of_life_emission = [] self.building_component_end_of_life_emission = [] def calculate_building_component_emission(self, building): surface_envelope_emission = [] surface_opening_emission = [] surface_envelope_end_of_life_emission = [] surface_opening_end_of_life_emission = [] opaque_surface_code = self.nrcan_catalogs.find_opaque_surface( self.nrcan_catalogs.hub_to_nrcan_function(building.function), self.nrcan_catalogs.year_to_period_of_construction( building.year_of_construction), '6') for surface in building.surfaces: boundary_envelope_emission = [] boundary_opening_emission = [] boundary_envelope_end_of_life_emission = [] boundary_opening_end_of_life_emission = [] for boundary in surface.associated_thermal_boundaries: opening_emission = None opening_end_of_life_emission = None layer_emission, layer_end_of_life_emission = \ self._calculate_envelope_emission(boundary) boundary_envelope_emission += layer_emission boundary_envelope_end_of_life_emission += layer_end_of_life_emission if boundary.window_ratio: opening_emission, opening_end_of_life_emission = \ self._calculate_opening_emission( building, surface, boundary, opaque_surface_code) if opening_emission: boundary_opening_emission += opening_emission boundary_opening_end_of_life_emission += opening_end_of_life_emission if boundary_opening_emission: surface_opening_emission += boundary_opening_emission surface_opening_end_of_life_emission += \ boundary_opening_end_of_life_emission surface_envelope_emission += boundary_envelope_emission surface_envelope_end_of_life_emission += \ boundary_envelope_end_of_life_emission building_envelope_emission = sum(surface_envelope_emission) building_envelope_workload = sum(surface_envelope_end_of_life_emission) building_opening_emission = sum(surface_opening_emission) building_opening_workload = sum(surface_opening_end_of_life_emission) building_component_emission = \ building_envelope_emission + building_opening_emission building_component_workload = \ building_envelope_workload + building_opening_workload return building_envelope_emission, building_opening_emission, \ building_component_emission, building_envelope_workload, \ building_opening_workload, building_component_workload def _calculate_envelope_emission(self, boundary): """ The method calculates embodied and end of life emission of the building's envelope by iterating through each building boundary's layers. The argument corresponding to the boundary parameter comes from the calculate_building_component_emission() method. The output also is used in the calculate_building_component_emission() method. So the current method is hidden to the user. :param boundary: :return: """ layer_emission = [] layer_end_of_life_emission = [] for layer in boundary.layers: if not layer.no_mass: layer_material = \ self.nrcan_catalogs.search_material(layer.material_name) layer_emission.append(EnvelopeEmission( layer_material['embodied_carbon'], boundary.opaque_area, layer.thickness, layer.density).calculate_envelope_emission()) boundary_workload = \ boundary.opaque_area * \ layer.thickness * \ layer.density layer_end_of_life_emission.append(EndOfLifeEmission( layer_material['recycling_ratio'], layer_material['onsite_recycling_ratio'], layer_material['company_recycling_ratio'], layer_material['landfilling_ratio'], boundary_workload).calculate_end_of_life_emission()) return layer_emission, layer_end_of_life_emission def _calculate_opening_emission( self, building, surface, boundary, opaque_surface_code, density=2579): """ This calculates the opening emission by iterating through each thermal opening of each building's boundary. It is done based on the mentioned parameters. The arguments come from the calculate_building_component_emission() method and the output is used in the same method. So the current method is hidden to the user. Windows have the assumed density of 2579 kg/m3 Window's thickness assumed the same as wall's thickness These two values are being used to calculate window's workload for the End of Life emission evaluation. :param building: :param surface: :param boundary: :param opaque_surface_code: :param density: :return: """ opening_emission = [] opening_end_of_life_emission = [] for opening in boundary.thermal_openings: transparent_surface_type = 'Window' if building.year_of_construction >= 2020 and \ surface.type == 'Roof': transparent_surface_type = 'Skylight' opening_material = self.nrcan_catalogs.search_transparent_surfaces( transparent_surface_type, opaque_surface_code) opening_emission.append( OpeningEmission(opening_material['embodied_carbon'], opening.area).calculate_opening_emission()) window_workload = opening.area * boundary.thickness * density opening_end_of_life_emission.append(EndOfLifeEmission( opening_material['recycling_ratio'], opening_material['onsite_recycling_ratio'], opening_material['company_recycling_ratio'], opening_material['landfilling_ratio'], window_workload).calculate_end_of_life_emission()) return opening_emission, opening_end_of_life_emission def calculate_emission(self): """ It iterates through the city object and gives each building to the calculate_building_component_emission() method. Then it unpack the results of the mentioned method to the (currently six) attributes which hold the final results. These attributes are mentioned in the constructor method description. """ for building in self.city.buildings: envelope_emission, opening_emission, component_emission, \ envelope_end_of_life_emission, \ opening_end_of_life_emission, \ component_end_of_life_emission = \ self.calculate_building_component_emission(building) self.building_envelope_emission.append(envelope_emission) self.building_opening_emission.append(opening_emission) self.building_component_emission.append(component_emission) self.building_envelope_end_of_life_emission.append( envelope_end_of_life_emission) self.building_opening_end_of_life_emission.append( opening_end_of_life_emission) self.building_component_end_of_life_emission.append( component_end_of_life_emission)