diff --git a/Cargo.lock b/Cargo.lock index 57d83d9..f299042 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -382,6 +382,15 @@ dependencies = [ "wee_alloc", ] +[[package]] +name = "lcax_calculation" +version = "2.2.4" +dependencies = [ + "lcax_core", + "lcax_models", + "serde_json", +] + [[package]] name = "lcax_convert" version = "2.2.4" @@ -389,6 +398,7 @@ dependencies = [ "bytes", "chrono", "field_access", + "lcax_calculation", "lcax_core", "lcax_models", "parquet", diff --git a/Cargo.toml b/Cargo.toml index 74a97ec..75a9286 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = [ +members = [ "modules/calculation", "modules/convert", "modules/core", "modules/lcax", diff --git a/lcax.schema.json b/lcax.schema.json index befcd65..dbb1703 100644 --- a/lcax.schema.json +++ b/lcax.schema.json @@ -17,7 +17,7 @@ "assemblies": { "type": "object", "additionalProperties": { - "$ref": "#/definitions/AssemblySource" + "$ref": "#/definitions/ReferenceSource_for_Assembly" } }, "classificationSystem": { @@ -145,288 +145,6 @@ } } }, - "Assembly": { - "type": "object", - "required": [ - "id", - "name", - "products", - "quantity", - "unit" - ], - "properties": { - "classification": { - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Classification" - } - }, - "comment": { - "type": [ - "string", - "null" - ] - }, - "description": { - "type": [ - "string", - "null" - ] - }, - "id": { - "type": "string" - }, - "metaData": { - "type": [ - "object", - "null" - ], - "additionalProperties": { - "type": "string" - } - }, - "name": { - "type": "string" - }, - "products": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/ProductSource" - } - }, - "quantity": { - "type": "number", - "format": "double" - }, - "results": { - "type": [ - "object", - "null" - ], - "additionalProperties": { - "type": "object", - "additionalProperties": { - "type": [ - "number", - "null" - ], - "format": "double" - } - } - }, - "unit": { - "$ref": "#/definitions/Unit" - } - } - }, - "AssemblySource": { - "oneOf": [ - { - "type": "object", - "required": [ - "assembly" - ], - "properties": { - "assembly": { - "$ref": "#/definitions/Assembly" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "reference" - ], - "properties": { - "reference": { - "$ref": "#/definitions/Reference" - } - }, - "additionalProperties": false - } - ] - }, - "BuildingInfo": { - "type": "object", - "required": [ - "buildingType", - "buildingTypology", - "floorsAboveGround", - "generalEnergyClass", - "roofType" - ], - "properties": { - "buildingCompletionYear": { - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "buildingFootprint": { - "anyOf": [ - { - "$ref": "#/definitions/ValueUnit" - }, - { - "type": "null" - } - ] - }, - "buildingHeight": { - "anyOf": [ - { - "$ref": "#/definitions/ValueUnit" - }, - { - "type": "null" - } - ] - }, - "buildingMass": { - "anyOf": [ - { - "$ref": "#/definitions/ValueUnit" - }, - { - "type": "null" - } - ] - }, - "buildingModelScope": { - "anyOf": [ - { - "$ref": "#/definitions/BuildingModelScope" - }, - { - "type": "null" - } - ] - }, - "buildingPermitYear": { - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "buildingType": { - "$ref": "#/definitions/BuildingType" - }, - "buildingTypology": { - "$ref": "#/definitions/BuildingTypology" - }, - "buildingUsers": { - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "certifications": { - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - } - }, - "energyDemandElectricity": { - "type": [ - "number", - "null" - ], - "format": "double" - }, - "energyDemandHeating": { - "type": [ - "number", - "null" - ], - "format": "double" - }, - "energySupplyElectricity": { - "type": [ - "number", - "null" - ], - "format": "double" - }, - "energySupplyHeating": { - "type": [ - "number", - "null" - ], - "format": "double" - }, - "exportedElectricity": { - "type": [ - "number", - "null" - ], - "format": "double" - }, - "floorsAboveGround": { - "type": "integer", - "format": "uint16", - "minimum": 0.0 - }, - "floorsBelowGround": { - "type": [ - "integer", - "null" - ], - "format": "uint16", - "minimum": 0.0 - }, - "frameType": { - "type": [ - "string", - "null" - ] - }, - "generalEnergyClass": { - "$ref": "#/definitions/GeneralEnergyClass" - }, - "grossFloorArea": { - "anyOf": [ - { - "$ref": "#/definitions/AreaType" - }, - { - "type": "null" - } - ] - }, - "heatedFloorArea": { - "anyOf": [ - { - "$ref": "#/definitions/AreaType" - }, - { - "type": "null" - } - ] - }, - "localEnergyClass": { - "type": [ - "string", - "null" - ] - }, - "roofType": { - "$ref": "#/definitions/RoofType" - } - } - }, "BuildingModelScope": { "type": "object", "required": [ @@ -943,42 +661,12 @@ ] }, "ImpactDataSource": { - "oneOf": [ - { - "type": "object", - "required": [ - "EPD" - ], - "properties": { - "EPD": { - "$ref": "#/definitions/EPD" - } - }, - "additionalProperties": false - }, + "anyOf": [ { - "type": "object", - "required": [ - "techFlow" - ], - "properties": { - "techFlow": { - "$ref": "#/definitions/TechFlow" - } - }, - "additionalProperties": false + "$ref": "#/definitions/EPD" }, { - "type": "object", - "required": [ - "reference" - ], - "properties": { - "reference": { - "$ref": "#/definitions/Reference" - } - }, - "additionalProperties": false + "$ref": "#/definitions/TechFlow" } ] }, @@ -1025,189 +713,531 @@ } } }, - "Product": { - "type": "object", - "required": [ - "id", - "impactData", - "name", - "quantity", - "referenceServiceLife", - "unit" - ], - "properties": { - "description": { - "type": [ - "string", - "null" - ] - }, - "id": { - "type": "string" - }, - "impactData": { - "$ref": "#/definitions/ImpactDataSource" - }, - "metaData": { - "type": [ - "object", - "null" - ], - "additionalProperties": { - "type": "string" - } - }, - "name": { - "type": "string" - }, - "quantity": { - "type": "number", - "format": "double" - }, - "referenceServiceLife": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - "results": { - "type": [ - "object", - "null" + "ProjectInfo": { + "oneOf": [ + { + "type": "object", + "required": [ + "buildingType", + "buildingTypology", + "floorsAboveGround", + "generalEnergyClass", + "roofType", + "type" ], - "additionalProperties": { - "type": "object", - "additionalProperties": { + "properties": { + "buildingCompletionYear": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "buildingFootprint": { + "anyOf": [ + { + "$ref": "#/definitions/ValueUnit" + }, + { + "type": "null" + } + ] + }, + "buildingHeight": { + "anyOf": [ + { + "$ref": "#/definitions/ValueUnit" + }, + { + "type": "null" + } + ] + }, + "buildingMass": { + "anyOf": [ + { + "$ref": "#/definitions/ValueUnit" + }, + { + "type": "null" + } + ] + }, + "buildingModelScope": { + "anyOf": [ + { + "$ref": "#/definitions/BuildingModelScope" + }, + { + "type": "null" + } + ] + }, + "buildingPermitYear": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "buildingType": { + "$ref": "#/definitions/BuildingType" + }, + "buildingTypology": { + "$ref": "#/definitions/BuildingTypology" + }, + "buildingUsers": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "certifications": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "energyDemandElectricity": { + "type": [ + "number", + "null" + ], + "format": "double" + }, + "energyDemandHeating": { + "type": [ + "number", + "null" + ], + "format": "double" + }, + "energySupplyElectricity": { "type": [ "number", "null" ], "format": "double" + }, + "energySupplyHeating": { + "type": [ + "number", + "null" + ], + "format": "double" + }, + "exportedElectricity": { + "type": [ + "number", + "null" + ], + "format": "double" + }, + "floorsAboveGround": { + "type": "integer", + "format": "uint16", + "minimum": 0.0 + }, + "floorsBelowGround": { + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0 + }, + "frameType": { + "type": [ + "string", + "null" + ] + }, + "generalEnergyClass": { + "$ref": "#/definitions/GeneralEnergyClass" + }, + "grossFloorArea": { + "anyOf": [ + { + "$ref": "#/definitions/AreaType" + }, + { + "type": "null" + } + ] + }, + "heatedFloorArea": { + "anyOf": [ + { + "$ref": "#/definitions/AreaType" + }, + { + "type": "null" + } + ] + }, + "localEnergyClass": { + "type": [ + "string", + "null" + ] + }, + "roofType": { + "$ref": "#/definitions/RoofType" + }, + "type": { + "type": "string", + "enum": [ + "buildingInfo" + ] } } }, - "transport": { - "type": [ - "array", - "null" + { + "type": "object", + "required": [ + "type" ], - "items": { - "$ref": "#/definitions/Transport" + "properties": { + "type": { + "type": "string", + "enum": [ + "infrastructureInfo" + ] + } } - }, - "unit": { - "$ref": "#/definitions/Unit" } - } + ] }, - "ProductSource": { + "ProjectPhase": { + "type": "string", + "enum": [ + "design", + "ongoing", + "built", + "other" + ] + }, + "ReferenceSource_for_Assembly": { "oneOf": [ { "type": "object", "required": [ - "product" + "id", + "name", + "products", + "quantity", + "type", + "unit" ], "properties": { - "product": { - "$ref": "#/definitions/Product" + "classification": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Classification" + } + }, + "comment": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "id": { + "type": "string" + }, + "metaData": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "products": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/ReferenceSource_for_Product" + } + }, + "quantity": { + "type": "number", + "format": "double" + }, + "results": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "object", + "additionalProperties": { + "type": [ + "number", + "null" + ], + "format": "double" + } + } + }, + "type": { + "type": "string", + "enum": [ + "actual" + ] + }, + "unit": { + "$ref": "#/definitions/Unit" } - }, - "additionalProperties": false + } }, { "type": "object", "required": [ - "reference" + "type", + "uri" ], "properties": { - "reference": { - "$ref": "#/definitions/Reference" + "format": { + "type": [ + "string", + "null" + ] + }, + "overrides": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "type": { + "type": "string", + "enum": [ + "reference" + ] + }, + "uri": { + "type": "string" + }, + "version": { + "type": [ + "string", + "null" + ] } - }, - "additionalProperties": false + } } ] }, - "ProjectInfo": { + "ReferenceSource_for_ImpactDataSource": { "oneOf": [ { "type": "object", + "anyOf": [ + { + "$ref": "#/definitions/EPD" + }, + { + "$ref": "#/definitions/TechFlow" + } + ], "required": [ - "buildingInfo" + "type" ], "properties": { - "buildingInfo": { - "$ref": "#/definitions/BuildingInfo" + "type": { + "type": "string", + "enum": [ + "actual" + ] } - }, - "additionalProperties": false + } }, { "type": "object", "required": [ - "infrastructureInfo" + "type", + "uri" ], "properties": { - "infrastructureInfo": { - "type": "object", + "format": { + "type": [ + "string", + "null" + ] + }, + "overrides": { + "type": [ + "object", + "null" + ], "additionalProperties": { "type": "string" } + }, + "type": { + "type": "string", + "enum": [ + "reference" + ] + }, + "uri": { + "type": "string" + }, + "version": { + "type": [ + "string", + "null" + ] } - }, - "additionalProperties": false + } } ] }, - "ProjectPhase": { - "type": "string", - "enum": [ - "design", - "ongoing", - "built", - "other" - ] - }, - "Reference": { - "type": "object", - "required": [ - "path", - "type" - ], - "properties": { - "format": { - "type": [ - "string", - "null" - ] - }, - "overrides": { - "type": [ - "object", - "null" + "ReferenceSource_for_Product": { + "oneOf": [ + { + "type": "object", + "required": [ + "id", + "impactData", + "name", + "quantity", + "referenceServiceLife", + "type", + "unit" ], - "additionalProperties": { - "type": "string" + "properties": { + "description": { + "type": [ + "string", + "null" + ] + }, + "id": { + "type": "string" + }, + "impactData": { + "$ref": "#/definitions/ReferenceSource_for_ImpactDataSource" + }, + "metaData": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "quantity": { + "type": "number", + "format": "double" + }, + "referenceServiceLife": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "results": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "object", + "additionalProperties": { + "type": [ + "number", + "null" + ], + "format": "double" + } + } + }, + "transport": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Transport" + } + }, + "type": { + "type": "string", + "enum": [ + "actual" + ] + }, + "unit": { + "$ref": "#/definitions/Unit" + } } }, - "path": { - "type": "string" - }, - "type": { - "$ref": "#/definitions/ReferenceType" - }, - "version": { - "type": [ - "string", - "null" - ] + { + "type": "object", + "required": [ + "type", + "uri" + ], + "properties": { + "format": { + "type": [ + "string", + "null" + ] + }, + "overrides": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "type": { + "type": "string", + "enum": [ + "reference" + ] + }, + "uri": { + "type": "string" + }, + "version": { + "type": [ + "string", + "null" + ] + } + } } - } - }, - "ReferenceType": { - "type": "string", - "enum": [ - "internal", - "external" ] }, "RoofType": { @@ -1358,9 +1388,9 @@ "distance", "distanceUnit", "id", - "impactCategories", - "name", - "transportEpd" + "impactData", + "lifeCycleStages", + "name" ], "properties": { "distance": { @@ -1373,17 +1403,17 @@ "id": { "type": "string" }, - "impactCategories": { + "impactData": { + "$ref": "#/definitions/ImpactDataSource" + }, + "lifeCycleStages": { "type": "array", "items": { - "$ref": "#/definitions/ImpactCategoryKey" + "$ref": "#/definitions/LifeCycleStage" } }, "name": { "type": "string" - }, - "transportEpd": { - "$ref": "#/definitions/ImpactDataSource" } } }, @@ -1396,10 +1426,12 @@ "kg", "tones", "pcs", + "kwh", "l", "m2r1", "km", "tones_km", + "kgm3", "unknown" ] }, diff --git a/modules/calculation/Cargo.toml b/modules/calculation/Cargo.toml new file mode 100644 index 0000000..d975d93 --- /dev/null +++ b/modules/calculation/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "lcax_calculation" +description.workspace = true +version.workspace = true +authors.workspace = true +edition.workspace = true +readme.workspace = true +license-file.workspace = true +homepage.workspace = true +documentation.workspace = true +repository.workspace = true + +[lib] +name = "lcax_calculation" +crate-type = ["cdylib", "rlib"] + +[dependencies] +lcax_core = { path = "../core", version = ">2.0.0" } +lcax_models = { path = "../models", version = ">2.0.0" } + +[dev-dependencies] +serde_json = "1.0.116" \ No newline at end of file diff --git a/modules/calculation/src/calculate.rs b/modules/calculation/src/calculate.rs new file mode 100644 index 0000000..57bbe67 --- /dev/null +++ b/modules/calculation/src/calculate.rs @@ -0,0 +1,158 @@ +use lcax_models::assembly::Assembly as LCAxAssembly; +use lcax_models::life_cycle_base::NewResults; +use lcax_models::life_cycle_base::{ + ImpactCategory, ImpactCategoryKey, LifeCycleStage, Results as LCAxResults, +}; +use lcax_models::product::{ImpactDataSource, Product as LCAxProduct}; +use lcax_models::project::Project as LCAxProject; +use lcax_models::shared::ReferenceSource; + +pub struct CalculationOptions { + pub reference_study_period: Option, + pub life_cycle_stages: Vec, + pub impact_categories: Vec, +} + +pub fn calculate_project( + project: &mut LCAxProject, + options: Option, +) -> Result<&mut LCAxProject, String> { + let _options = match options { + Some(options) => options, + None => CalculationOptions { + reference_study_period: project.reference_study_period.clone(), + life_cycle_stages: project.life_cycle_stages.clone(), + impact_categories: project.impact_categories.clone(), + }, + }; + + let mut project_results = + LCAxResults::new_results(&_options.impact_categories, &_options.life_cycle_stages); + project.assemblies.iter_mut().for_each(|(_, assembly)| { + let results = + calculate_assembly(resolve_reference_mut(assembly).unwrap(), &_options).unwrap(); + add_results(&mut project_results, &results) + }); + project.results = Some(project_results.clone()); + Ok(project) +} + +pub fn calculate_assembly( + assembly: &mut LCAxAssembly, + options: &CalculationOptions, +) -> Result { + let mut assembly_results = + LCAxResults::new_results(&options.impact_categories, &options.life_cycle_stages); + assembly.products.iter_mut().for_each(|(_, product)| { + let results = calculate_product(resolve_reference_mut(product).unwrap(), options).unwrap(); + add_results(&mut assembly_results, &results) + }); + + options + .impact_categories + .iter() + .for_each(|impact_category_key| { + options + .life_cycle_stages + .iter() + .for_each(|life_cycle_stage| { + let value = assembly_results + .get(impact_category_key) + .unwrap() + .get(life_cycle_stage) + .unwrap() + .unwrap(); + + *assembly_results + .get_mut(impact_category_key) + .unwrap() + .get_mut(life_cycle_stage) + .unwrap() = Some(value * assembly.quantity); + }); + }); + assembly.results = Some(assembly_results.clone()); + Ok(assembly_results) +} + +pub fn calculate_product( + product: &mut LCAxProduct, + options: &CalculationOptions, +) -> Result { + let mut product_results = LCAxResults::new(); + + options + .impact_categories + .iter() + .for_each(|impact_category_key| { + let mut impact_category = ImpactCategory::new(); + options + .life_cycle_stages + .iter() + .for_each( + |life_cycle_stage| match resolve_reference(&product.impact_data) { + Ok(ImpactDataSource::EPD(epd)) => { + impact_category.insert( + life_cycle_stage.clone(), + Some(match epd.impacts.get(impact_category_key) { + Some(impact) => match impact.get(life_cycle_stage) { + Some(value) => value.unwrap() * product.quantity, + None => 0.0, + }, + None => 0.0, + }), + ); + } + Ok(ImpactDataSource::TechFlow(techflow)) => { + impact_category.insert( + life_cycle_stage.clone(), + Some(match techflow.impacts.get(impact_category_key) { + Some(impact) => match impact.get(life_cycle_stage) { + Some(value) => value.unwrap() * product.quantity, + None => 0.0, + }, + None => 0.0, + }), + ); + } + Err(_) => panic!("Handling reference not implemented yet!"), + }, + ); + product_results.insert(impact_category_key.clone(), impact_category); + }); + + product.results = Some(product_results.clone()); + Ok(product_results) +} + +fn resolve_reference(reference: &ReferenceSource) -> Result<&T, String> { + match reference { + ReferenceSource::Actual(reference) => Ok(reference), + ReferenceSource::Reference(_) => panic!("Handling reference not implemented yet!"), + } +} + +fn resolve_reference_mut(reference: &mut ReferenceSource) -> Result<&mut T, String> { + match reference { + ReferenceSource::Actual(reference) => Ok(reference), + ReferenceSource::Reference(_) => panic!("Handling reference not implemented yet!"), + } +} + +fn add_results(existing_results: &mut LCAxResults, new_results: &LCAxResults) { + new_results + .iter() + .for_each(|(impact_category_key, impact_category)| { + impact_category + .iter() + .for_each(|(life_cycle_stage, value)| { + *existing_results + .get_mut(impact_category_key) + .unwrap() + .get_mut(life_cycle_stage) + .unwrap() = Some( + existing_results[impact_category_key][life_cycle_stage].unwrap() + + value.unwrap(), + ); + }); + }); +} diff --git a/modules/calculation/src/lib.rs b/modules/calculation/src/lib.rs new file mode 100644 index 0000000..ed2498d --- /dev/null +++ b/modules/calculation/src/lib.rs @@ -0,0 +1 @@ +pub mod calculate; diff --git a/modules/calculation/tests/datafixtures/project.json b/modules/calculation/tests/datafixtures/project.json new file mode 100644 index 0000000..b1207df --- /dev/null +++ b/modules/calculation/tests/datafixtures/project.json @@ -0,0 +1,165 @@ +{ + "id": "2f95c41e-0cc4-4b6e-90ac-ffa796aecd6d", + "name": "Test eksempel", + "description": "", + "comment": null, + "location": { + "country": "dnk", + "city": "", + "address": "Testvej 1, 1111 Testbyen" + }, + "owner": "Test", + "formatVersion": "2.2.4", + "lciaMethod": null, + "classificationSystem": "LCAByg", + "referenceStudyPeriod": 50, + "lifeCycleStages": [ + "a1a3", + "c3", + "c4", + "d" + ], + "impactCategories": [ + "ap", + "adpe", + "adpf", + "ep", + "pocp", + "odp", + "gwp", + "penre", + "pere" + ], + "assemblies": { + "d57fbbc1-2f57-47bc-b7de-8f1b2ee4da87": { + "type": "actual", + "id": "d57fbbc1-2f57-47bc-b7de-8f1b2ee4da87", + "name": "Test element", + "description": "", + "comment": "", + "quantity": 50.0, + "unit": "m2", + "classification": [ + { + "system": "LCAByg", + "code": "59ab59a5-2482-45ae-85f1-d0e39e640712", + "name": "Bærende indervægge" + } + ], + "products": { + "ee60d276-eb44-41a9-a36d-6ded36315c47": { + "type": "actual", + "id": "ee60d276-eb44-41a9-a36d-6ded36315c47", + "name": "Test produkt", + "description": "", + "referenceServiceLife": 80, + "impactData": { + "type": "actual", + "id": "d61fc8da-1a0d-4baa-a0fa-194c1f8a5218", + "name": "Test fase (A1-A3)", + "declaredUnit": "m3", + "version": "00.02.000", + "publishedDate": "1970-01-01", + "validUntil": "1970-01-01", + "formatVersion": "2.2.4", + "source": { + "name": "Ökobau.dat", + "url": "https://oekobaudat.de/OEKOBAU.DAT/datasetdetail/process.xhtml?uuid=5aa09d72-e200-40dc-b8da-959a72e32bc3&version=00.02.000&stock=OBD_2021_II&lang=en" + }, + "referenceServiceLife": null, + "standard": "en15804a1", + "comment": "", + "location": "dnk", + "subtype": "specific", + "conversions": [ + { + "value": 7850.0, + "to": "kg", + "metaData": "" + } + ], + "impacts": { + "pert": { + "a1a3": 512.5 + }, + "ep": { + "a1a3": 0.1557 + }, + "adpe": { + "a1a3": 0.00008897 + }, + "gwp": { + "a1a3": 818.0 + }, + "ap": { + "a1a3": 1.65 + }, + "pocp": { + "a1a3": 0.1334 + }, + "penrt": { + "a1a3": 9067.0 + }, + "adpf": { + "a1a3": 8430.0 + }, + "odp": { + "a1a3": 2.148e-12 + } + }, + "metaData": null + }, + "quantity": 0.5, + "unit": "kg", + "transport": null, + "results": null, + "metaData": null + } + }, + "results": null, + "metaData": null + } + }, + "results": null, + "projectInfo": { + "type": "buildingInfo", + "buildingType": "new", + "buildingTypology": "office", + "certifications": null, + "buildingMass": null, + "buildingHeight": null, + "grossFloorArea": { + "value": 100.0, + "unit": "m2", + "definition": "" + }, + "heatedFloorArea": { + "value": 100.0, + "unit": "m2", + "definition": "" + }, + "buildingFootprint": null, + "floorsAboveGround": 0, + "floorsBelowGround": 0, + "roofType": "other", + "frameType": "", + "buildingCompletionYear": 0, + "buildingPermitYear": null, + "energyDemandHeating": 0.0, + "energySupplyHeating": 0.0, + "energyDemandElectricity": 0.0, + "energySupplyElectricity": 0.0, + "exportedElectricity": 0.0, + "generalEnergyClass": "advanced", + "localEnergyClass": null, + "buildingUsers": null, + "buildingModelScope": null + }, + "projectPhase": "other", + "softwareInfo": { + "goalAndScopeDefinition": null, + "lcaSoftware": "LCAByg", + "calculationType": "BR2023" + }, + "metaData": null +} \ No newline at end of file diff --git a/modules/calculation/tests/test_calculate.rs b/modules/calculation/tests/test_calculate.rs new file mode 100644 index 0000000..ad4f997 --- /dev/null +++ b/modules/calculation/tests/test_calculate.rs @@ -0,0 +1,100 @@ +use std::collections::HashMap; +use std::fs; +use std::path::Path; + +use lcax_calculation::calculate::{ + calculate_assembly, calculate_product, calculate_project, CalculationOptions, +}; +use lcax_models::epd::{Standard, SubType, EPD}; +use lcax_models::life_cycle_base::{ImpactCategory, ImpactCategoryKey, LifeCycleStage}; +use lcax_models::product::{ImpactDataSource, Product}; +use lcax_models::project::Project; +use lcax_models::shared::{ReferenceSource, Unit}; + +#[test] +fn test_calculate_project() -> Result<(), String> { + let root_dir = Path::new(env!("CARGO_MANIFEST_DIR")); + + let file_path = root_dir.join("tests/datafixtures/project.json"); + let contents = fs::read_to_string(file_path).expect("Should have been able to read the file"); + let mut project = serde_json::from_str::(&contents).unwrap(); + + calculate_project(&mut project, None).unwrap(); + assert!(project.results.is_some()); + Ok(()) +} + +#[test] +fn test_calculate_assembly() -> Result<(), String> { + let root_dir = Path::new(env!("CARGO_MANIFEST_DIR")); + + let file_path = root_dir.join("tests/datafixtures/project.json"); + let contents = fs::read_to_string(file_path).expect("Should have been able to read the file"); + let mut project = serde_json::from_str::(&contents).unwrap(); + + let assembly = match project + .assemblies + .get_mut("d57fbbc1-2f57-47bc-b7de-8f1b2ee4da87") + .unwrap() + { + ReferenceSource::Actual(actual) => actual, + ReferenceSource::Reference(_) => panic!("Expected actual assembly"), + }; + let options = CalculationOptions { + reference_study_period: project.reference_study_period.clone(), + life_cycle_stages: project.life_cycle_stages.clone(), + impact_categories: project.impact_categories.clone(), + }; + + calculate_assembly(assembly, &options).unwrap(); + assert!(assembly.results.is_some()); + Ok(()) +} + +#[test] +fn test_calculate_product() -> Result<(), String> { + let mut product = Product { + id: "1".to_string(), + name: "Product 1".to_string(), + description: None, + reference_service_life: 20, + impact_data: ReferenceSource::Actual(ImpactDataSource::EPD(EPD { + id: "1".to_string(), + name: "EPD 1".to_string(), + declared_unit: Unit::M, + version: "".to_string(), + published_date: Default::default(), + valid_until: Default::default(), + format_version: "".to_string(), + source: None, + reference_service_life: None, + standard: Standard::EN15804A1, + comment: None, + location: Default::default(), + subtype: SubType::GENERIC, + conversions: None, + impacts: HashMap::from([( + ImpactCategoryKey::GWP, + ImpactCategory::from([(LifeCycleStage::A1A3, Some(3.0))]), + )]), + meta_data: None, + })), + quantity: 5.0, + unit: Unit::M, + transport: None, + results: None, + meta_data: None, + }; + + let options = lcax_calculation::calculate::CalculationOptions { + reference_study_period: None, + life_cycle_stages: vec![LifeCycleStage::A1A3], + impact_categories: vec![ImpactCategoryKey::GWP], + }; + let result = calculate_product(&mut product, &options).unwrap(); + assert_eq!( + result[&ImpactCategoryKey::GWP][&LifeCycleStage::A1A3], + Some(15.0) + ); + Ok(()) +} diff --git a/modules/convert/Cargo.toml b/modules/convert/Cargo.toml index 92cebdf..098cf09 100644 --- a/modules/convert/Cargo.toml +++ b/modules/convert/Cargo.toml @@ -24,5 +24,6 @@ parquet = { version = "51.0.0", default-features = false, features = ["flate2", uuid = { version = "1.8.0", features = ["v4", "v5"] } lcax_core = { path = "../core", version = ">2.0.0" } lcax_models = { path = "../models", version = ">2.0.0" } +lcax_calculation = { path = "../calculation", version = ">2.0.0" } chrono = "0.4.38" bytes = "1.6.0" diff --git a/modules/convert/src/lcabyg/parse.rs b/modules/convert/src/lcabyg/parse.rs index 09d5369..3b6e7e8 100644 --- a/modules/convert/src/lcabyg/parse.rs +++ b/modules/convert/src/lcabyg/parse.rs @@ -6,15 +6,14 @@ use serde_json::Error; use lcax_core::country::Country; use lcax_core::utils::get_version; -use lcax_models::assembly::AssemblySource; use lcax_models::assembly::{Assembly, Classification}; use lcax_models::life_cycle_base::{ImpactCategoryKey, LifeCycleStage}; -use lcax_models::product::{ImpactDataSource, Product as LCAxProduct, ProductSource}; +use lcax_models::product::{ImpactDataSource, Product as LCAxProduct}; use lcax_models::project::{ AreaType, BuildingInfo, BuildingType, BuildingTypology, GeneralEnergyClass, Location, Project as LCAxProject, ProjectInfo, RoofType, SoftwareInfo, }; -use lcax_models::shared::Unit; +use lcax_models::shared::{ReferenceSource, Unit}; use crate::lcabyg::edges::EdgeType; use crate::lcabyg::nodes::{epd_from_lcabyg_stages, Node}; @@ -120,7 +119,7 @@ fn add_result_from_lcabyg( ); for (assembly_id, _assembly) in &mut lcax_project.assemblies { match _assembly { - AssemblySource::Assembly(assembly) => { + ReferenceSource::Actual(assembly) => { assembly.results = collect_lcabyg_object_results( &get_result_id(assembly_id, results), results, @@ -309,7 +308,7 @@ fn add_element_data( } project.assemblies.insert( assembly.id.clone(), - AssemblySource::Assembly(assembly.clone()), + ReferenceSource::Actual(assembly.clone()), ); } @@ -348,7 +347,7 @@ fn add_construction_data( let product = add_construction_to_product_data(child_id, &construction_edge, nodes); assembly .products - .insert(product.id.clone(), ProductSource::Product(product.clone())); + .insert(product.id.clone(), ReferenceSource::Actual(product.clone())); break; } _ => continue, @@ -389,7 +388,7 @@ fn add_construction_to_product_data( let epd_data = epd_from_lcabyg_stages(&stages); - product.impact_data = ImpactDataSource::EPD(epd_data); + product.impact_data = ReferenceSource::Actual(ImpactDataSource::EPD(epd_data)); product } diff --git a/modules/convert/src/slice/model.rs b/modules/convert/src/slice/model.rs index 97fa639..fe38e75 100644 --- a/modules/convert/src/slice/model.rs +++ b/modules/convert/src/slice/model.rs @@ -7,11 +7,11 @@ use uuid; use lcax_core::country::Country; use lcax_core::utils::get_version; -use lcax_models::assembly::{Assembly, AssemblySource, Classification}; +use lcax_models::assembly::{Assembly, Classification}; use lcax_models::life_cycle_base::{ImpactCategory, ImpactCategoryKey, LifeCycleStage}; -use lcax_models::product::{ImpactDataSource, Product, ProductSource}; +use lcax_models::product::{ImpactDataSource, Product}; use lcax_models::project::{Location, Project as LCAxProject, SoftwareInfo}; -use lcax_models::shared::Unit; +use lcax_models::shared::{ReferenceSource, Unit}; use lcax_models::techflow::TechFlow; #[derive(Default, FieldAccess)] @@ -129,7 +129,7 @@ pub fn add_project_data(project: &mut LCAxProject, element: &SLiCEElement) { project.description = Some(format!("{stock_region_name}-{building_use_subtype_name}-{stock_activity_type_name}-{building_energy_performance_name}")); project.location = Location { - country: get_country_from_region(&element.stock_region_code), + country: get_country_from_region(&stock_region_name), city: None, address: None, }; @@ -188,22 +188,24 @@ pub fn add_slice_element(project: &mut LCAxProject, element: &SLiCEElement) { let product = product_from_slice(product_uuid.as_str(), element); assembly .products - .insert(product_uuid.clone(), ProductSource::Product(product)); + .insert(product_uuid.clone(), ReferenceSource::Actual(product)); project .assemblies - .insert(assembly_uuid.clone(), AssemblySource::Assembly(assembly)); + .insert(assembly_uuid.clone(), ReferenceSource::Actual(assembly)); } else { match project.assemblies.get_mut(&assembly_uuid).unwrap() { - AssemblySource::Assembly(ref mut assembly) => { + ReferenceSource::Actual(ref mut assembly) => { if !assembly.products.contains_key(&product_uuid) { let product = product_from_slice(product_uuid.as_str(), element); assembly .products - .insert(product_uuid.clone(), ProductSource::Product(product)); + .insert(product_uuid.clone(), ReferenceSource::Actual(product)); } else { match assembly.products.get_mut(&product_uuid).unwrap() { - ProductSource::Product(ref mut product) => match product.impact_data { - ImpactDataSource::TechFlow(ref mut tech_flow) => { + ReferenceSource::Actual(ref mut product) => match product.impact_data { + ReferenceSource::Actual(ImpactDataSource::TechFlow( + ref mut tech_flow, + )) => { add_impact_data(tech_flow, element); } _ => {} @@ -247,7 +249,7 @@ fn product_from_slice(uid: &str, element: &SLiCEElement) -> Product { name: element.worksection_class_sfb.clone(), description: Some("".to_string()), reference_service_life: 50, - impact_data: ImpactDataSource::TechFlow(create_tech_flow(element)), + impact_data: ReferenceSource::Actual(ImpactDataSource::TechFlow(create_tech_flow(element))), quantity: element.amount_material_kg_per_building, unit: Unit::KG, transport: None, diff --git a/modules/convert/src/slice/parse.rs b/modules/convert/src/slice/parse.rs index 38afa8f..62e557b 100644 --- a/modules/convert/src/slice/parse.rs +++ b/modules/convert/src/slice/parse.rs @@ -2,12 +2,12 @@ use std::collections::{HashMap, HashSet}; use std::sync::Arc; use bytes::Bytes; +use lcax_calculation::calculate::calculate_project; +use lcax_models::project::Project as LCAxProject; use parquet::file::reader::{FileReader, SerializedFileReader}; use parquet::record::RowAccessor; use parquet::schema::types::Type; -use lcax_models::project::Project as LCAxProject; - use crate::slice::model::{add_project_data, add_slice_element, SLiCEElement}; /// Parse an SLiCE parquet file into LCAx. @@ -166,6 +166,9 @@ fn get_projects_by_archetypes( } projects.retain(|_, project| project.id.is_empty() != true); + for (_, project) in projects.iter_mut() { + calculate_project(project, None).expect("Could not calculate results"); + } projects.values().cloned().collect() } diff --git a/modules/convert/tests/lcabyg/test_parse_lcabyg_project.rs b/modules/convert/tests/lcabyg/test_parse_lcabyg_project.rs index 26ca64d..d22605a 100644 --- a/modules/convert/tests/lcabyg/test_parse_lcabyg_project.rs +++ b/modules/convert/tests/lcabyg/test_parse_lcabyg_project.rs @@ -2,8 +2,7 @@ use std::fs; use std::path::Path; use lcax_convert::lcabyg; -use lcax_models::assembly::AssemblySource; -use lcax_models::product::ProductSource; +use lcax_models::shared::ReferenceSource; #[test] fn test_parse_lcabyg_project() -> Result<(), String> { @@ -21,24 +20,24 @@ fn test_parse_lcabyg_project() -> Result<(), String> { assert!(!lca.assemblies.is_empty()); for (_, assembly) in &lca.assemblies { match assembly { - AssemblySource::Assembly(assembly) => { + ReferenceSource::Actual(assembly) => { assert!(!assembly.name.is_empty()); assert!(!assembly.products.is_empty()); for (_, product) in &assembly.products { // Assert Product Info match product { - ProductSource::Product(product) => { + ReferenceSource::Actual(product) => { assert!(!product.name.is_empty()); assert!(!product.quantity.is_nan()); } - ProductSource::Reference(_) => { + ReferenceSource::Reference(_) => { assert!(false); } } } } - AssemblySource::Reference(_) => { + ReferenceSource::Reference(_) => { assert!(false); } } @@ -70,10 +69,10 @@ fn test_parse_lcabyg_example() -> Result<(), String> { assert!(!lca.assemblies.is_empty()); for (_, assembly) in &lca.assemblies { match assembly { - AssemblySource::Assembly(assembly) => { + ReferenceSource::Actual(assembly) => { assert!(assembly.results.is_some()); } - AssemblySource::Reference(_) => { + ReferenceSource::Reference(_) => { assert!(false); } } diff --git a/modules/convert/tests/slice/test_parse_slice.rs b/modules/convert/tests/slice/test_parse_slice.rs index 7e6f4eb..fcc2351 100644 --- a/modules/convert/tests/slice/test_parse_slice.rs +++ b/modules/convert/tests/slice/test_parse_slice.rs @@ -2,8 +2,7 @@ use std::fs; use std::path::Path; use lcax_convert::slice::parse::parse_slice; -use lcax_models::assembly::AssemblySource; -use lcax_models::product::ProductSource; +use lcax_models::shared::ReferenceSource; #[test] fn test_parse_slice() -> Result<(), String> { @@ -16,28 +15,29 @@ fn test_parse_slice() -> Result<(), String> { for project in projects { assert!(!project.id.is_empty()); assert!(!project.name.is_empty()); + assert!(project.results.is_some()); // Assert Assembly Info assert!(!project.assemblies.is_empty()); for (_, assembly) in &project.assemblies { match assembly { - AssemblySource::Assembly(assembly) => { + ReferenceSource::Actual(assembly) => { assert!(!assembly.name.is_empty()); assert!(!assembly.products.is_empty()); for (_, product) in &assembly.products { // Assert Product Info match product { - ProductSource::Product(product) => { + ReferenceSource::Actual(product) => { assert!(!product.quantity.is_nan()); } - ProductSource::Reference(_) => { + ReferenceSource::Reference(_) => { assert!(false); } } } } - AssemblySource::Reference(_) => { + ReferenceSource::Reference(_) => { assert!(false); } } diff --git a/modules/models/src/assembly.rs b/modules/models/src/assembly.rs index ef05f20..b2c36b3 100644 --- a/modules/models/src/assembly.rs +++ b/modules/models/src/assembly.rs @@ -1,21 +1,13 @@ -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; #[cfg(feature = "jsbindings")] use tsify::Tsify; use crate::life_cycle_base::Results; -use crate::product::ProductSource; -use crate::shared::{Reference, Unit}; - -#[derive(Deserialize, Serialize, JsonSchema, Clone)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "jsbindings", derive(Tsify))] -pub enum AssemblySource { - Assembly(Assembly), - Reference(Reference), -} +use crate::product::Product; +use crate::shared::{ReferenceSource, Unit}; #[derive(Deserialize, Serialize, JsonSchema, Clone)] #[serde(rename_all = "camelCase")] @@ -28,8 +20,8 @@ pub struct Assembly { pub quantity: f64, pub unit: Unit, pub classification: Option>, - pub products: HashMap, - pub results: Results, + pub products: HashMap>, + pub results: Option, pub meta_data: Option>, } diff --git a/modules/models/src/life_cycle_base.rs b/modules/models/src/life_cycle_base.rs index 6ae578b..518723d 100644 --- a/modules/models/src/life_cycle_base.rs +++ b/modules/models/src/life_cycle_base.rs @@ -1,8 +1,8 @@ -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fmt; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; #[cfg(feature = "jsbindings")] use tsify::Tsify; @@ -176,4 +176,27 @@ impl fmt::Display for ImpactCategoryKey { pub type ImpactCategory = HashMap>; -pub type Results = Option>; +pub type Results = HashMap; + +pub trait NewResults { + fn new_results( + impact_categories: &Vec, + life_cycle_stage: &Vec, + ) -> Self; +} +impl NewResults for Results { + fn new_results( + impact_categories: &Vec, + life_cycle_stage: &Vec, + ) -> Self { + let mut results = HashMap::new(); + impact_categories.iter().for_each(|impact_category_key| { + let mut impact_category = HashMap::new(); + life_cycle_stage.iter().for_each(|life_cycle_stage| { + impact_category.insert(life_cycle_stage.clone(), Some(0.0)); + }); + results.insert(impact_category_key.clone(), impact_category); + }); + results + } +} diff --git a/modules/models/src/product.rs b/modules/models/src/product.rs index 2c6738f..9c08236 100644 --- a/modules/models/src/product.rs +++ b/modules/models/src/product.rs @@ -6,18 +6,10 @@ use std::collections::HashMap; use tsify::Tsify; use crate::epd::EPD; -use crate::life_cycle_base::{ImpactCategoryKey, LifeCycleStage, Results}; -use crate::shared::{Reference, ReferenceType, Unit}; +use crate::life_cycle_base::{LifeCycleStage, Results}; +use crate::shared::{Reference, ReferenceSource, Unit}; use crate::techflow::TechFlow; -#[derive(Deserialize, Serialize, JsonSchema, Clone)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "jsbindings", derive(Tsify))] -pub enum ProductSource { - Product(Product), - Reference(Reference), -} - #[derive(Deserialize, Serialize, JsonSchema, Clone)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "jsbindings", derive(Tsify))] @@ -26,11 +18,11 @@ pub struct Product { pub name: String, pub description: Option, pub reference_service_life: u32, - pub impact_data: ImpactDataSource, + pub impact_data: ReferenceSource, pub quantity: f64, pub unit: Unit, pub transport: Option>, - pub results: Results, + pub results: Option, pub meta_data: Option>, } @@ -48,19 +40,18 @@ pub struct Transport { #[derive(Deserialize, Serialize, JsonSchema, Clone)] #[serde(rename_all = "camelCase")] +#[serde(untagged)] #[cfg_attr(feature = "jsbindings", derive(Tsify))] pub enum ImpactDataSource { #[serde(rename = "EPD")] EPD(EPD), TechFlow(TechFlow), - Reference(Reference), } -impl Default for ImpactDataSource { - fn default() -> ImpactDataSource { - ImpactDataSource::Reference(Reference { - _type: ReferenceType::EXTERNAL, - path: "".to_string(), +impl Default for ReferenceSource { + fn default() -> ReferenceSource { + ReferenceSource::Reference(Reference { + uri: "".to_string(), format: None, version: None, overrides: None, diff --git a/modules/models/src/project.rs b/modules/models/src/project.rs index e3df644..9b846e9 100644 --- a/modules/models/src/project.rs +++ b/modules/models/src/project.rs @@ -6,9 +6,9 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "jsbindings")] use tsify::Tsify; -use crate::assembly::AssemblySource; +use crate::assembly::Assembly; use crate::life_cycle_base::{ImpactCategoryKey, LifeCycleStage, Results}; -use crate::shared::Unit; +use crate::shared::{ReferenceSource, Unit}; #[derive(Deserialize, Serialize, JsonSchema, Default, Clone)] #[serde(rename_all = "camelCase")] @@ -30,8 +30,8 @@ pub struct Project { pub reference_study_period: Option, pub life_cycle_stages: Vec, pub impact_categories: Vec, - pub assemblies: HashMap, - pub results: Results, + pub assemblies: HashMap>, + pub results: Option, pub project_info: Option, pub project_phase: ProjectPhase, pub software_info: SoftwareInfo, @@ -58,7 +58,7 @@ impl Project { life_cycle_stages: vec![], impact_categories: vec![], assemblies: HashMap::new(), - results: Results::default(), + results: None, project_info: None, project_phase: ProjectPhase::DESIGN, software_info: SoftwareInfo { @@ -102,6 +102,7 @@ pub struct Location { #[derive(Deserialize, Serialize, JsonSchema, Clone)] #[serde(rename_all = "camelCase")] +#[serde(tag = "type")] #[cfg_attr(feature = "jsbindings", derive(Tsify))] pub enum ProjectInfo { BuildingInfo(BuildingInfo), diff --git a/modules/models/src/shared.rs b/modules/models/src/shared.rs index b450499..e964143 100644 --- a/modules/models/src/shared.rs +++ b/modules/models/src/shared.rs @@ -21,6 +21,7 @@ pub enum Unit { KM, #[allow(non_camel_case_types)] TONES_KM, + KGM3, UNKNOWN, } @@ -38,6 +39,7 @@ impl From<&String> for Unit { "kwh" => Unit::KWH, "m2r1" => Unit::M2R1, "tones*km" => Unit::TONES_KM, + "kgm3" => Unit::KGM3, _ => Unit::UNKNOWN, } } @@ -61,20 +63,19 @@ pub struct Source { } #[derive(Deserialize, Serialize, JsonSchema, Clone)] +#[serde(rename_all = "camelCase")] +#[serde(tag = "type")] #[cfg_attr(feature = "jsbindings", derive(Tsify))] -pub struct Reference { - #[serde(rename = "type")] - pub _type: ReferenceType, - pub path: String, - pub format: Option, - pub version: Option, - pub overrides: Option>, +pub enum ReferenceSource { + Actual(T), + Reference(Reference), } #[derive(Deserialize, Serialize, JsonSchema, Clone)] -#[serde(rename_all = "lowercase")] #[cfg_attr(feature = "jsbindings", derive(Tsify))] -pub enum ReferenceType { - INTERNAL, - EXTERNAL, +pub struct Reference { + pub uri: String, + pub format: Option, + pub version: Option, + pub overrides: Option>, } diff --git a/packages/javascript/src/lcax.d.ts b/packages/javascript/src/lcax.d.ts index 635fafb..87b12b1 100644 --- a/packages/javascript/src/lcax.d.ts +++ b/packages/javascript/src/lcax.d.ts @@ -75,7 +75,7 @@ export interface BuildingInfo { buildingModelScope: BuildingModelScope | null; } -export type ProjectInfo = { buildingInfo: BuildingInfo } | { infrastructureInfo: Record }; +export type ProjectInfo = ({ type: "buildingInfo" } & BuildingInfo) | ({ type: "infrastructureInfo" } & Record); export interface Location { country: Country; @@ -104,8 +104,8 @@ export interface Project { referenceStudyPeriod: number | null; lifeCycleStages: LifeCycleStage[]; impactCategories: ImpactCategoryKey[]; - assemblies: Record; - results: Results; + assemblies: Record>; + results: Results | null; projectInfo: ProjectInfo | null; projectPhase: ProjectPhase; softwareInfo: SoftwareInfo; @@ -135,52 +135,15 @@ export interface EPD { metaData: Record | null; } -export interface InternalImpactData { - path: string; -} - -export interface ExternalImpactData { - url: string; - format: string; - version: string | null; -} - -export type ImpactDataSource = { EPD: EPD } | { techFlow: TechFlow } | { reference: Reference }; - -export interface Transport { - id: string; - name: string; - impactCategories: ImpactCategoryKey[]; - distance: number; - distanceUnit: Unit; - transportEpd: ImpactDataSource; -} - -export interface Product { - id: string; - name: string; - description: string | null; - referenceServiceLife: number; - impactData: ImpactDataSource; - quantity: number; - unit: Unit; - transport: Transport[] | null; - results: Results; - metaData: Record | null; -} - -export type ProductSource = { product: Product } | { reference: Reference }; - -export type ReferenceType = "internal" | "external"; - export interface Reference { - type: ReferenceType; - path: string; + uri: string; format: string | null; version: string | null; overrides: Record | null; } +export type ReferenceSource = ({ type: "actual" } & T) | ({ type: "reference" } & Reference); + export interface Source { name: string; url: string | null; @@ -192,11 +155,7 @@ export interface Conversion { metaData: string; } -export type Unit = "m" | "m2" | "m3" | "kg" | "tones" | "pcs" | "l" | "m2r1" | "km" | "tones_km" | "unknown"; - -export type ImpactCategoryKey = "gwp" | "gwp_fos" | "gwp_bio" | "gwp_lul" | "odp" | "ap" | "ep" | "ep_fw" | "ep_mar" | "ep_ter" | "pocp" | "adpe" | "adpf" | "penre" | "pere" | "perm" | "pert" | "penrt" | "penrm" | "sm" | "pm" | "wdp" | "irp" | "etp_fw" | "htp_c" | "htp_nc" | "sqp" | "rsf" | "nrsf" | "fw" | "hwd" | "nhwd" | "rwd" | "cru" | "mrf" | "mer" | "eee" | "eet"; - -export type LifeCycleStage = "a1a3" | "a4" | "a5" | "b1" | "b2" | "b3" | "b4" | "b5" | "b6" | "b7" | "c1" | "c2" | "c3" | "c4" | "d"; +export type Unit = "m" | "m2" | "m3" | "kg" | "tones" | "pcs" | "kwh" | "l" | "m2r1" | "km" | "tones_km" | "kgm3" | "unknown"; export interface TechFlow { id: string; @@ -225,10 +184,46 @@ export interface Assembly { quantity: number; unit: Unit; classification: Classification[] | null; - products: Record; - results: Results; + products: Record>; + results: Results | null; + metaData: Record | null; +} + +export interface InternalImpactData { + path: string; +} + +export interface ExternalImpactData { + url: string; + format: string; + version: string | null; +} + +export type ImpactDataSource = EPD | TechFlow; + +export interface Transport { + id: string; + name: string; + lifeCycleStages: LifeCycleStage[]; + distance: number; + distanceUnit: Unit; + impactData: ImpactDataSource; +} + +export interface Product { + id: string; + name: string; + description: string | null; + referenceServiceLife: number; + impactData: ReferenceSource; + quantity: number; + unit: Unit; + transport: Transport[] | null; + results: Results | null; metaData: Record | null; } -export type AssemblySource = { assembly: Assembly } | { reference: Reference }; +export type ImpactCategoryKey = "gwp" | "gwp_fos" | "gwp_bio" | "gwp_lul" | "odp" | "ap" | "ep" | "ep_fw" | "ep_mar" | "ep_ter" | "pocp" | "adpe" | "adpf" | "penre" | "pere" | "perm" | "pert" | "penrt" | "penrm" | "sm" | "pm" | "wdp" | "irp" | "etp_fw" | "htp_c" | "htp_nc" | "sqp" | "rsf" | "nrsf" | "fw" | "hwd" | "nhwd" | "rwd" | "cru" | "mrf" | "mer" | "eee" | "eet"; + +export type LifeCycleStage = "a1a3" | "a4" | "a5" | "b1" | "b2" | "b3" | "b4" | "b5" | "b6" | "b7" | "c1" | "c2" | "c3" | "c4" | "d"; diff --git a/packages/javascript/src/lcax_bg.wasm b/packages/javascript/src/lcax_bg.wasm index 6c64643..e16439d 100644 Binary files a/packages/javascript/src/lcax_bg.wasm and b/packages/javascript/src/lcax_bg.wasm differ diff --git a/packages/python/src/lcax/__init__.py b/packages/python/src/lcax/__init__.py index 0f8b228..e9387c1 100644 --- a/packages/python/src/lcax/__init__.py +++ b/packages/python/src/lcax/__init__.py @@ -1,18 +1,25 @@ import json from pathlib import Path -from typing import Type, TypeVar +from typing import Type as PyType, TypeVar -from .lcax import * +from .lcax import _convert_lcabyg, _convert_ilcd, _convert_slice +import lcax as lcax_binary from .pydantic import * +from .pydantic import ReferenceSourceForImpactDataSource1 as EPD +from .pydantic import ReferenceSourceForImpactDataSource2 as TechFlow +from .pydantic import ReferenceSourceForAssembly1 as Assembly +from .pydantic import ReferenceSourceForProduct1 as Product +from .pydantic import ProjectInfo1 as BuildingInfo -__doc__ = lcax.__doc__ -if hasattr(lcax, "__all__"): - __all__ = lcax.__all__ + +__doc__ = lcax_binary.__doc__ +if hasattr(lcax_binary, "__all__"): + __all__ = lcax_binary.__all__ Project_Type = TypeVar("Project_Type", str, dict, Project) -def convert_lcabyg(data: str | dict, result_data: str | dict | None = None, *, as_type: Type[Project_Type] = dict) -> Project_Type: +def convert_lcabyg(data: str | dict, result_data: str | dict | None = None, *, as_type: PyType[Project_Type] = dict) -> Project_Type: """ Converts json formatted LCAbyg data into a LCAx project @@ -26,7 +33,7 @@ def convert_lcabyg(data: str | dict, result_data: str | dict | None = None, *, a result_data = json.dumps(result_data) try: - _project = lcax._convert_lcabyg(data, result_data) + _project = _convert_lcabyg(data, result_data) except Exception as err: raise ParsingException(err) @@ -37,13 +44,13 @@ def convert_lcabyg(data: str | dict, result_data: str | dict | None = None, *, a elif as_type == Project: return Project(**json.loads(_project)) else: - raise NotImplementedError("Currently only 'dict', 'str' and 'lcax.LCAxProject' is implemented as_type.") + raise NotImplementedError("Currently only 'dict', 'str' and 'lcax.Project' is implemented as_type.") EPD_Type = TypeVar("EPD_Type", str, dict, EPD) -def convert_ilcd(data: str | dict, *, as_type: Type[EPD_Type] = dict) -> EPD_Type: +def convert_ilcd(data: str | dict, *, as_type: PyType[EPD_Type] = dict) -> EPD_Type: """ Converts a json formatted ILCD+EPD data into EPDx @@ -54,7 +61,7 @@ def convert_ilcd(data: str | dict, *, as_type: Type[EPD_Type] = dict) -> EPD_Typ data = json.dumps(data) try: - _epd = lcax._convert_ilcd(data) + _epd = _convert_ilcd(data) except Exception as err: raise ParsingException(err) @@ -63,12 +70,12 @@ def convert_ilcd(data: str | dict, *, as_type: Type[EPD_Type] = dict) -> EPD_Typ elif as_type == dict: return json.loads(_epd) elif as_type == EPD: - return EPD(**json.loads(_epd)) + return EPD(**json.loads(_epd), type='actual') else: raise NotImplementedError("Currently only 'dict', 'str' and 'lcax.EPD' is implemented as_type.") -def convert_slice(path: str | Path, *, as_type: Type[Project_Type] = dict) -> list[Project_Type]: +def convert_slice(path: str | Path, *, as_type: PyType[Project_Type] = dict) -> list[Project_Type]: """ Converts a SLiCE .parquet file into a list of LCAx projects @@ -82,7 +89,7 @@ def convert_slice(path: str | Path, *, as_type: Type[Project_Type] = dict) -> li raise FileNotFoundError(f"File not found: {path}") try: - projects = lcax._convert_slice(str(path)) + projects = _convert_slice(str(path)) except Exception as err: raise ParsingException(err) diff --git a/packages/python/src/lcax/lcax.abi3.so b/packages/python/src/lcax/lcax.abi3.so index c673cc5..0c9cc77 100755 Binary files a/packages/python/src/lcax/lcax.abi3.so and b/packages/python/src/lcax/lcax.abi3.so differ diff --git a/packages/python/src/lcax/lcax.pyi b/packages/python/src/lcax/lcax.pyi index c4cba67..a000b6a 100644 --- a/packages/python/src/lcax/lcax.pyi +++ b/packages/python/src/lcax/lcax.pyi @@ -6,4 +6,4 @@ def _convert_ilcd(data: str) -> str: """Converts a json formatted ILCD+EPD data string into an EPDx formatted json string""" def _convert_slice(path: str) -> list[str]: - """Converts a SLiCE .parquet file to a list of json formatted LCAx projects""" \ No newline at end of file + """Converts a SLiCE .parquet file to a list of json formatted LCAx projects""" diff --git a/packages/python/src/lcax/pydantic.py b/packages/python/src/lcax/pydantic.py index 9f2ac3c..396450e 100644 --- a/packages/python/src/lcax/pydantic.py +++ b/packages/python/src/lcax/pydantic.py @@ -7,7 +7,7 @@ from enum import Enum from typing import Any, Dict, List, Optional, Union -from pydantic import BaseModel, ConfigDict, Field, RootModel +from pydantic import BaseModel, ConfigDict, Field class BuildingModelScope(BaseModel): @@ -379,12 +379,19 @@ class Location(BaseModel): country: Country +class Type(Enum): + BUILDING_INFO = 'buildingInfo' + + +class Type1(Enum): + INFRASTRUCTURE_INFO = 'infrastructureInfo' + + class ProjectInfo2(BaseModel): model_config = ConfigDict( - extra='forbid', populate_by_name=True, ) - infrastructure_info: Dict[str, str] = Field(..., alias='infrastructureInfo') + type: Type1 class ProjectPhase(Enum): @@ -394,9 +401,61 @@ class ProjectPhase(Enum): OTHER = 'other' -class ReferenceType(Enum): - INTERNAL = 'internal' - EXTERNAL = 'external' +class Type2(Enum): + ACTUAL = 'actual' + + +class Type3(Enum): + REFERENCE = 'reference' + + +class ReferenceSourceForAssembly2(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + format: Optional[str] = None + overrides: Optional[Dict[str, Any]] = None + type: Type3 + uri: str + version: Optional[str] = None + + +class Type4(Enum): + ACTUAL = 'actual' + + +class Type6(Enum): + REFERENCE = 'reference' + + +class ReferenceSourceForImpactDataSource3(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + format: Optional[str] = None + overrides: Optional[Dict[str, Any]] = None + type: Type6 + uri: str + version: Optional[str] = None + + +class Type7(Enum): + ACTUAL = 'actual' + + +class Type8(Enum): + REFERENCE = 'reference' + + +class ReferenceSourceForProduct2(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + format: Optional[str] = None + overrides: Optional[Dict[str, Any]] = None + type: Type8 + uri: str + version: Optional[str] = None class RoofType(Enum): @@ -451,6 +510,7 @@ class Unit(Enum): M2R1 = 'm2r1' KM = 'km' TONES_KM = 'tones_km' + KGM3 = 'kgm3' UNKNOWN = 'unknown' @@ -471,43 +531,6 @@ class AreaType(BaseModel): value: float -class BuildingInfo(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - building_completion_year: Optional[int] = Field( - None, alias='buildingCompletionYear', ge=0 - ) - building_footprint: Optional[ValueUnit] = Field(None, alias='buildingFootprint') - building_height: Optional[ValueUnit] = Field(None, alias='buildingHeight') - building_mass: Optional[ValueUnit] = Field(None, alias='buildingMass') - building_model_scope: Optional[BuildingModelScope] = Field( - None, alias='buildingModelScope' - ) - building_permit_year: Optional[int] = Field(None, alias='buildingPermitYear', ge=0) - building_type: BuildingType = Field(..., alias='buildingType') - building_typology: BuildingTypology = Field(..., alias='buildingTypology') - building_users: Optional[int] = Field(None, alias='buildingUsers', ge=0) - certifications: Optional[List[str]] = None - energy_demand_electricity: Optional[float] = Field( - None, alias='energyDemandElectricity' - ) - energy_demand_heating: Optional[float] = Field(None, alias='energyDemandHeating') - energy_supply_electricity: Optional[float] = Field( - None, alias='energySupplyElectricity' - ) - energy_supply_heating: Optional[float] = Field(None, alias='energySupplyHeating') - exported_electricity: Optional[float] = Field(None, alias='exportedElectricity') - floors_above_ground: int = Field(..., alias='floorsAboveGround', ge=0) - floors_below_ground: Optional[int] = Field(None, alias='floorsBelowGround', ge=0) - frame_type: Optional[str] = Field(None, alias='frameType') - general_energy_class: GeneralEnergyClass = Field(..., alias='generalEnergyClass') - gross_floor_area: Optional[AreaType] = Field(None, alias='grossFloorArea') - heated_floor_area: Optional[AreaType] = Field(None, alias='heatedFloorArea') - local_energy_class: Optional[str] = Field(None, alias='localEnergyClass') - roof_type: RoofType = Field(..., alias='roofType') - - class Conversion(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -541,31 +564,49 @@ class EPD(BaseModel): version: str -class ImpactDataSource1(BaseModel): - model_config = ConfigDict( - extra='forbid', - populate_by_name=True, - ) - epd: EPD = Field(..., alias='EPD') - - class ProjectInfo1(BaseModel): model_config = ConfigDict( - extra='forbid', populate_by_name=True, ) - building_info: BuildingInfo = Field(..., alias='buildingInfo') + building_completion_year: Optional[int] = Field( + None, alias='buildingCompletionYear', ge=0 + ) + building_footprint: Optional[ValueUnit] = Field(None, alias='buildingFootprint') + building_height: Optional[ValueUnit] = Field(None, alias='buildingHeight') + building_mass: Optional[ValueUnit] = Field(None, alias='buildingMass') + building_model_scope: Optional[BuildingModelScope] = Field( + None, alias='buildingModelScope' + ) + building_permit_year: Optional[int] = Field(None, alias='buildingPermitYear', ge=0) + building_type: BuildingType = Field(..., alias='buildingType') + building_typology: BuildingTypology = Field(..., alias='buildingTypology') + building_users: Optional[int] = Field(None, alias='buildingUsers', ge=0) + certifications: Optional[List[str]] = None + energy_demand_electricity: Optional[float] = Field( + None, alias='energyDemandElectricity' + ) + energy_demand_heating: Optional[float] = Field(None, alias='energyDemandHeating') + energy_supply_electricity: Optional[float] = Field( + None, alias='energySupplyElectricity' + ) + energy_supply_heating: Optional[float] = Field(None, alias='energySupplyHeating') + exported_electricity: Optional[float] = Field(None, alias='exportedElectricity') + floors_above_ground: int = Field(..., alias='floorsAboveGround', ge=0) + floors_below_ground: Optional[int] = Field(None, alias='floorsBelowGround', ge=0) + frame_type: Optional[str] = Field(None, alias='frameType') + general_energy_class: GeneralEnergyClass = Field(..., alias='generalEnergyClass') + gross_floor_area: Optional[AreaType] = Field(None, alias='grossFloorArea') + heated_floor_area: Optional[AreaType] = Field(None, alias='heatedFloorArea') + local_energy_class: Optional[str] = Field(None, alias='localEnergyClass') + roof_type: RoofType = Field(..., alias='roofType') + type: Type -class Reference(BaseModel): +class ReferenceSourceForImpactDataSource1(EPD): model_config = ConfigDict( populate_by_name=True, ) - format: Optional[str] = None - overrides: Optional[Dict[str, Any]] = None - path: str - type: ReferenceType - version: Optional[str] = None + type: Type4 class TechFlow(BaseModel): @@ -584,36 +625,11 @@ class TechFlow(BaseModel): source: Optional[Source] = None -class AssemblySource2(BaseModel): - model_config = ConfigDict( - extra='forbid', - populate_by_name=True, - ) - reference: Reference - - -class ImpactDataSource2(BaseModel): - model_config = ConfigDict( - extra='forbid', - populate_by_name=True, - ) - tech_flow: TechFlow = Field(..., alias='techFlow') - - -class ImpactDataSource3(BaseModel): +class ReferenceSourceForImpactDataSource2(TechFlow): model_config = ConfigDict( - extra='forbid', populate_by_name=True, ) - reference: Reference - - -class ProductSource2(BaseModel): - model_config = ConfigDict( - extra='forbid', - populate_by_name=True, - ) - reference: Reference + type: Type4 class Transport(BaseModel): @@ -623,40 +639,32 @@ class Transport(BaseModel): distance: float distance_unit: Unit = Field(..., alias='distanceUnit') id: str - impact_data: Union[ImpactDataSource1, ImpactDataSource2, ImpactDataSource3] = Field( - ..., alias='impactData' - ) + impact_data: Union[EPD, TechFlow] = Field(..., alias='impactData') life_cycle_stages: List[LifeCycleStage] = Field(..., alias='lifeCycleStages') name: str -class Product(BaseModel): +class ReferenceSourceForProduct1(BaseModel): model_config = ConfigDict( populate_by_name=True, ) description: Optional[str] = None id: str - impact_data: Union[ImpactDataSource1, ImpactDataSource2, ImpactDataSource3] = Field( - ..., alias='impactData' - ) + impact_data: Union[ + Union[ReferenceSourceForImpactDataSource1, ReferenceSourceForImpactDataSource2], + ReferenceSourceForImpactDataSource3, + ] = Field(..., alias='impactData') meta_data: Optional[Dict[str, Any]] = Field(None, alias='metaData') name: str quantity: float reference_service_life: int = Field(..., alias='referenceServiceLife', ge=0) results: Optional[Dict[str, Any]] = None transport: Optional[List[Transport]] = None + type: Type7 unit: Unit -class ProductSource1(BaseModel): - model_config = ConfigDict( - extra='forbid', - populate_by_name=True, - ) - product: Product - - -class Assembly(BaseModel): +class ReferenceSourceForAssembly1(BaseModel): model_config = ConfigDict( populate_by_name=True, ) @@ -666,25 +674,20 @@ class Assembly(BaseModel): id: str meta_data: Optional[Dict[str, Any]] = Field(None, alias='metaData') name: str - products: Dict[str, Union[ProductSource1, ProductSource2]] + products: Dict[str, Union[ReferenceSourceForProduct1, ReferenceSourceForProduct2]] quantity: float results: Optional[Dict[str, Any]] = None + type: Type2 unit: Unit -class AssemblySource1(BaseModel): - model_config = ConfigDict( - extra='forbid', - populate_by_name=True, - ) - assembly: Assembly - - class Project(BaseModel): model_config = ConfigDict( populate_by_name=True, ) - assemblies: Dict[str, Union[AssemblySource1, AssemblySource2]] + assemblies: Dict[ + str, Union[ReferenceSourceForAssembly1, ReferenceSourceForAssembly2] + ] classification_system: Optional[str] = Field(None, alias='classificationSystem') comment: Optional[str] = None description: Optional[str] = None