From 59f155c0529ea1366810132024dc66be6d445392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isak=20N=C3=A4slund?= Date: Thu, 3 Sep 2026 16:02:23 +0200 Subject: [PATCH 01/25] Add tests for resulting module values --- .../LifeCycleAssessment_Engine_Tests.csproj | 1 + .../ResultingModuleValues.cs | 121 ++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 .ci/unit-tests/LifeCycleAssessment_Engine_Tests/ResultingModuleValues.cs diff --git a/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/LifeCycleAssessment_Engine_Tests.csproj b/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/LifeCycleAssessment_Engine_Tests.csproj index cd717c1..ab23949 100644 --- a/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/LifeCycleAssessment_Engine_Tests.csproj +++ b/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/LifeCycleAssessment_Engine_Tests.csproj @@ -18,6 +18,7 @@ + diff --git a/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/ResultingModuleValues.cs b/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/ResultingModuleValues.cs new file mode 100644 index 0000000..14b167c --- /dev/null +++ b/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/ResultingModuleValues.cs @@ -0,0 +1,121 @@ +/* + * This file is part of the Buildings and Habitats object Model (BHoM) + * Copyright (c) 2015 - 2025, the respective contributors. All rights reserved. + * + * Each contributor holds copyright over their respective contributions. + * The project versioning (Git) records all such contribution source information. + * + * + * The BHoM is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, or + * (at your option) any later version. + * + * The BHoM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this code. If not, see . + */ + +using AutoBogus; +using BH.Engine.LifeCycleAssessment; +using BH.oM.LifeCycleAssessment; +using BH.oM.LifeCycleAssessment.Interfaces; +using BH.oM.LifeCycleAssessment.MaterialFragments; +using BH.oM.LifeCycleAssessment.MaterialFragments.Construction; +using BH.oM.LifeCycleAssessment.Results; +using BH.oM.Physical.Materials; +using BH.oM.Test.NUnit; +using FluentAssertions; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BH.Tests.Engine.LifeCycleAssessment +{ + public class ResultingModuleValues : NUnitTest + { + /***************************************************/ + /**** Public Methods ****/ + /***************************************************/ + + + [TestCaseSource(nameof(PartCases))] + [Description("Tests that resulting module values do not contain both combination modules and their constituent parts after evaluation resulting module values, in particular when pre-computed values have been provided. This is critical to ensure that their is no misalignment in the data for when precomputed values are provided.")] + public void ResultingModuleValuesDoesNotContainCombinationOrParts(IEnvironmentalMetric metric, Dictionary precomputedValues, List expectedModules) + { + + var resultingValues = Query.IResultingModuleValues(metric, 1, null, precomputedValues, null); + Assert.That(resultingValues.Keys, Is.EquivalentTo(expectedModules), "Resulting module values should contain either combination modules or their parts, but not both."); + + + } + + /***************************************************/ + + private static IEnumerable PartCases() + { + yield return new object[] + { + new ClimateChangeTotalMetric() + { + Indicators = new Dictionary { { Module.A1toA3, 1 }, { Module.A5, 2 } } + }, + new Dictionary() + { + { Module.A5_1, new PrecomputedModuleValues { ModuleValues = new Dictionary { { MetricType.ClimateChangeTotal, 1 } } } } + }, + new List { Module.A1toA3, Module.A5_1 } + }; + + yield return new object[] + { + new ClimateChangeTotalMetric() + { + Indicators = new Dictionary { { Module.A1toA3, 1 }, { Module.A5_1, 2 } } + }, + new Dictionary() + { + { Module.A5, new PrecomputedModuleValues { ModuleValues = new Dictionary { { MetricType.ClimateChangeTotal, 1 } } } } + }, + new List { Module.A1toA3, Module.A5 } + }; + + yield return new object[] + { + new ClimateChangeTotalMetric() + { + Indicators = new Dictionary { { Module.A1toA3, 1 }, { Module.B1toB7, 2 } } + }, + new Dictionary() + { + { Module.B1_1, new PrecomputedModuleValues { ModuleValues = new Dictionary { { MetricType.ClimateChangeTotal, 1 } } } } + }, + new List { Module.A1toA3, Module.B1_1 } + }; + + yield return new object[] + { + new ClimateChangeTotalMetric() + { + Indicators = new Dictionary { { Module.A1toA3, 1 }, { Module.B1_1, 2 }, { Module.B1_2, 2 }, { Module.B3, 2 } } + }, + new Dictionary() + { + { Module.B1toB7, new PrecomputedModuleValues { ModuleValues = new Dictionary { { MetricType.ClimateChangeTotal, 1 } } } } + }, + new List { Module.A1toA3, Module.B1toB7 } + }; + } + + + } +} + + From 190a0747a9a996e168bee4304d72e8bdf8ac00fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isak=20N=C3=A4slund?= Date: Thu, 3 Sep 2026 16:03:09 +0200 Subject: [PATCH 02/25] Add method for extracting the value of a particular module, or its parts for the case of a combination module --- .../Query/ModuleValue.cs | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 LifeCycleAssessment_Engine/Query/ModuleValue.cs diff --git a/LifeCycleAssessment_Engine/Query/ModuleValue.cs b/LifeCycleAssessment_Engine/Query/ModuleValue.cs new file mode 100644 index 0000000..0ff47f4 --- /dev/null +++ b/LifeCycleAssessment_Engine/Query/ModuleValue.cs @@ -0,0 +1,158 @@ +/* + * This file is part of the Buildings and Habitats object Model (BHoM) + * Copyright (c) 2015 - 2026, the respective contributors. All rights reserved. + * + * Each contributor holds copyright over their respective contributions. + * The project versioning (Git) records all such contribution source information. + * + * + * The BHoM is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, or + * (at your option) any later version. + * + * The BHoM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this code. If not, see . + */ + +using BH.Engine.Base; +using BH.Engine.Matter; +using BH.oM.Base; +using BH.oM.Base.Attributes; +using BH.oM.Dimensional; +using BH.oM.LifeCycleAssessment; +using BH.oM.LifeCycleAssessment.Configs; +using BH.oM.LifeCycleAssessment.Fragments; +using BH.oM.LifeCycleAssessment.Interfaces; +using BH.oM.LifeCycleAssessment.MaterialFragments; +using BH.oM.LifeCycleAssessment.MaterialFragments.Transport; +using BH.oM.LifeCycleAssessment.Results; +using BH.oM.Physical.Materials; +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; + +namespace BH.Engine.LifeCycleAssessment +{ + public static partial class Query + { + + /***************************************************/ + /**** Public Methods ****/ + /***************************************************/ + + [Description("Gets the value of a module from the module data, taking into account any combination modules and their parts.")] + [Input("moduleData", "The module data to get the value from.")] + [Input("module", "The module to get the value for.")] + [Input("requireAllRequiredSubParts", "If true, all required sub-parts must be present in the module data for the value to be calculated. If false, the value will be calculated from the available sub-parts, requiring at least one subpart to be present.")] + [Output("value", "The value of the module.")] + public static double ModuleValue(this ILifeCycleAssessmentModuleData> moduleData, Module module, bool requireAllRequiredSubParts = true) + { + if (moduleData == null) + return double.NaN; + + return moduleData.Indicators.ModuleValue(module, requireAllRequiredSubParts); + } + + /***************************************************/ + + [Description("Gets the value of a module from the module data, taking into account any combination modules and their parts.")] + [Input("moduleData", "The module data to get the value from.")] + [Input("module", "The module to get the value for.")] + [Input("requireAllRequiredSubParts", "If true, all required sub-parts must be present in the module data for the value to be calculated. If false, the value will be calculated from the available sub-parts, requiring at least one subpart to be present.")] + [Output("value", "The value of the module.")] + public static double ModuleValue(this IDictionary moduleData, Module module, bool requireAllRequiredSubParts = true) + { + if (moduleData == null) + return double.NaN; + + if (moduleData.TryGetModuleValue(module, out double value, requireAllRequiredSubParts)) + return value; + + return double.NaN; + } + + /***************************************************/ + /**** Private Methods ****/ + /***************************************************/ + + [Description("Tries to get the value of a module from the module data, taking into account any combination modules and their parts.")] + [Input("moduleData", "The module data to get the value from.")] + [Input("module", "The module to get the value for.")] + [Input("requireAllRequiredSubParts", "If true, all required sub-parts must be present in the module data for the value to be calculated. If false, the value will be calculated from the available sub-parts, requiring at least one subpart to be present.")] + [Output("value", "The value of the module.")] + private static bool TryGetModuleValue(this IDictionary moduleData, Module module, out double value, bool requireAllRequiredSubParts = true) + { + if (moduleData == null) + { + value = double.NaN; + return false; + } + + if (moduleData.TryGetValue(module, out value)) + return true; + + if (Query.CombinationModules().TryGetValue(module, out IReadOnlyList<(Module, bool)> parts)) + { + if (requireAllRequiredSubParts) + { + // If all required sub-parts are needed, check if all are present + value = 0; + foreach (var part in parts) + { + if (part.Item2) // If the part is required + { + if (!moduleData.TryGetModuleValue(part.Item1, out double subValue)) + { + value = double.NaN; + return false; // Required part is missing + } + value += subValue; + } + else + { + // If the part is not required, add its value if present + if (moduleData.TryGetModuleValue(part.Item1, out double subValue)) + value += subValue; + } + } + return true; + } + else + { + // If not all are required, return the sum of available parts + //Check that at least one part is present + value = 0; + bool atLeastOnePartPresent = false; + foreach (var part in parts) + { + + // If the part is not required, add its value if present + if (moduleData.TryGetModuleValue(part.Item1, out double subValue)) + { + value += subValue; + atLeastOnePartPresent = true; + } + } + + return atLeastOnePartPresent; + } + } + + value = double.NaN; + return false; + } + + /***************************************************/ + } +} + + + From f258af40f3ec932a4429062d116bc2e39721c916 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isak=20N=C3=A4slund?= Date: Thu, 3 Sep 2026 16:03:33 +0200 Subject: [PATCH 03/25] Add method for computing the wastefactor based on a wasterate --- .../Query/WasteFactor.cs | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 LifeCycleAssessment_Engine/Query/WasteFactor.cs diff --git a/LifeCycleAssessment_Engine/Query/WasteFactor.cs b/LifeCycleAssessment_Engine/Query/WasteFactor.cs new file mode 100644 index 0000000..8ce482d --- /dev/null +++ b/LifeCycleAssessment_Engine/Query/WasteFactor.cs @@ -0,0 +1,88 @@ +/* + * This file is part of the Buildings and Habitats object Model (BHoM) + * Copyright (c) 2015 - 2026, the respective contributors. All rights reserved. + * + * Each contributor holds copyright over their respective contributions. + * The project versioning (Git) records all such contribution source information. + * + * + * The BHoM is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, or + * (at your option) any later version. + * + * The BHoM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this code. If not, see . + */ + + +using BH.oM.Base.Attributes; +using BH.oM.Geometry; +using BH.oM.LifeCycleAssessment; +using BH.oM.LifeCycleAssessment.MaterialFragments; +using BH.oM.LifeCycleAssessment.MaterialFragments.Construction; +using BH.oM.LifeCycleAssessment.MaterialFragments.Transport; +using BH.oM.LifeCycleAssessment.Results; +using BH.oM.Physical.Materials; +using BH.oM.Quantities; +using BH.oM.Quantities.Attributes; +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; + +namespace BH.Engine.LifeCycleAssessment +{ + public static partial class Query + { + /***************************************************/ + /**** Public Methods ****/ + /***************************************************/ + + [Description("The waste factor WFi is calculated by converting the waste rate WRi (a percentage of the quantity of materials brought to the site that are wasted)" + + " to the quantity of materials wasted on site as a percentage of the material quantities used in the final asset (Eqn. 2.5 from IStructE guidance).")] + [Input("constructionWasteEmissions", "The construction waste emissions parameters including the waste rate to be converted into a waste factor.")] + [Output("wasteFactor", "Percentage of the quantity of materials wasted on site as a percentage of the material quantities used in the final asset.")] + public static double WasteFactor(this ConstructionWasteEmissions constructionWasteEmissions) + { + return (constructionWasteEmissions?.WasteRate).WasteFactor(); + } + + /***************************************************/ + + [Description("The waste factor WFi is calculated by converting the waste rate WRi (a percentage of the quantity of materials brought to the site that are wasted)" + + " to the quantity of materials wasted on site as a percentage of the material quantities used in the final asset (Eqn. 2.5 from IStructE guidance).")] + [Input("wasteRate", "The WasteRate object storing the waste rate - a percentage of the quantity of materials brought to the site that are wasted. Should be a value between 0 and 1.")] + [Output("wasteFactor", "Percentage of the quantity of materials wasted on site as a percentage of the material quantities used in the final asset.")] + public static double WasteFactor(this WasteRate wasteRate) + { + if(wasteRate == null) + return double.NaN; + + return WasteFactor(wasteRate.Rate); + + } + + /***************************************************/ + + [Description("The waste factor WFi is calculated by converting the waste rate WRi (a percentage of the quantity of materials brought to the site that are wasted)" + + " to the quantity of materials wasted on site as a percentage of the material quantities used in the final asset (Eqn. 2.5 from IStructE guidance).")] + [Input("wasteRate", "The waste rate WRi - a percentage of the quantity of materials brought to the site that are wasted. Should be a value between 0 and 1.")] + [Output("wasteFactor", "Percentage of the quantity of materials wasted on site as a percentage of the material quantities used in the final asset.")] + public static double WasteFactor(double wasteRate) + { + return (1 / (1 - wasteRate)) - 1; + } + + /***************************************************/ + } +} + + + From 06b84c6f682d88f1a68b0ca0467f8b97e56d199d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isak=20N=C3=A4slund?= Date: Thu, 3 Sep 2026 16:05:04 +0200 Subject: [PATCH 04/25] Refactor handling of construction waste - adding a query method to compute the value, and calling it from the modify method Atempting to make the code a bit cleaner --- .../Modify/AddConstructionWasteEmissions.cs | 69 +----------- .../Query/ConstructionWasteEmissions.cs | 106 ++++++++++++++++++ 2 files changed, 109 insertions(+), 66 deletions(-) create mode 100644 LifeCycleAssessment_Engine/Query/ConstructionWasteEmissions.cs diff --git a/LifeCycleAssessment_Engine/Modify/AddConstructionWasteEmissions.cs b/LifeCycleAssessment_Engine/Modify/AddConstructionWasteEmissions.cs index 916ca15..2e47d02 100644 --- a/LifeCycleAssessment_Engine/Modify/AddConstructionWasteEmissions.cs +++ b/LifeCycleAssessment_Engine/Modify/AddConstructionWasteEmissions.cs @@ -52,74 +52,11 @@ public static partial class Modify [Output("resultingValues", "The input dictionary is modified in place with the A5_3 module added, representing construction waste emissions calculated as: (cradle-to-gate + transport + disposal) × waste factor.")] public static void AddConstructionWasteEmissions(this Dictionary resultingValues, ConstructionWasteEmissions constructionEmissions, MetricType metricType) { - List requiredWasteModules = new List() { Module.A1toA3, Module.A4 }; - - - List disposalModules = new List() { Module.C3toC4, Module.C3, Module.C4 }; - - double total = 0; - List missingModules = new List(); - - //Get out the cradle to gate metrics - if (resultingValues.TryGetValue(Module.A1toA3, out double a1a3)) - total += a1a3; - else if(resultingValues.TryGetValue(Module.A1, out double a1) && resultingValues.TryGetValue(Module.A2, out double a2) && resultingValues.TryGetValue(Module.A3, out double a3)) - total += a1 + a2 + a3; - else - missingModules.Add(Module.A1toA3); - - if(resultingValues.TryGetValue(Module.A4, out double a4)) - total += a4; - else - missingModules.Add(Module.A4); - - if (!constructionEmissions.ResuedOnSite) - { - if (resultingValues.TryGetValue(Module.C2, out double c2)) - total += c2; //Add C2 if not re-used on site - else - missingModules.Add(Module.C2); - } - - if (resultingValues.TryGetValue(Module.C3toC4, out double c3c4)) - total += c3c4; - else - { - //For some EPDs it might be that only one of C3 or C4 is reported, so check for both and add whichever is present - //Quite common for EPDs to only report C3 or C4 - //However if both are present, then both should be used. - bool hasAtLeastOne = false; - if (resultingValues.TryGetValue(Module.C3, out double c3)) - { - hasAtLeastOne = true; - total += c3; - } - - if (resultingValues.TryGetValue(Module.C4, out double c4)) - { - hasAtLeastOne = true; - total += c4; - } - - if(!hasAtLeastOne) - missingModules.Add(Module.C3toC4); - } - - if (missingModules.Any()) - { - string modulesMissingNames = string.Join(", ", missingModules); - - string message = $"Missing modules for waste computation of waste factors (A5.3) for metric of type {metricType}: {string.Join(", ", missingModules)} (or their subparts)."; - BH.Engine.Base.Compute.RecordError(message); + double a5_3 = resultingValues.ConstructionWasteEmissions(constructionEmissions, metricType); + if (double.IsNaN(a5_3)) return; - } - //From IStructE guidance - //The waste factor WFi is calculated by converting the waste rate WRi (a percentage of the quantity of materials brought - //to the site that are wasted) to the quantity of materials wasted on site as a percentage of the material quantities used - //in the final asset(Eqn. 2.5): - double wasteFactor = (1 / (1 - constructionEmissions.WasteRate.Rate)) - 1; - resultingValues[Module.A5_3] = total * wasteFactor; + resultingValues[Module.A5_3] = a5_3; if(Query.PartOfCombinationModules().TryGetValue(Module.A5_3, out var parentCombinations)) { diff --git a/LifeCycleAssessment_Engine/Query/ConstructionWasteEmissions.cs b/LifeCycleAssessment_Engine/Query/ConstructionWasteEmissions.cs new file mode 100644 index 0000000..df40488 --- /dev/null +++ b/LifeCycleAssessment_Engine/Query/ConstructionWasteEmissions.cs @@ -0,0 +1,106 @@ +/* + * This file is part of the Buildings and Habitats object Model (BHoM) + * Copyright (c) 2015 - 2026, the respective contributors. All rights reserved. + * + * Each contributor holds copyright over their respective contributions. + * The project versioning (Git) records all such contribution source information. + * + * + * The BHoM is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, or + * (at your option) any later version. + * + * The BHoM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this code. If not, see . + */ + + +using BH.oM.Base.Attributes; +using BH.oM.Geometry; +using BH.oM.LifeCycleAssessment; +using BH.oM.LifeCycleAssessment.MaterialFragments; +using BH.oM.LifeCycleAssessment.MaterialFragments.Construction; +using BH.oM.LifeCycleAssessment.MaterialFragments.Transport; +using BH.oM.LifeCycleAssessment.Results; +using BH.oM.Physical.Materials; +using BH.oM.Quantities; +using BH.oM.Quantities.Attributes; +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; + +namespace BH.Engine.LifeCycleAssessment +{ + public static partial class Query + { + /***************************************************/ + /**** Public Methods ****/ + /***************************************************/ + + [Description("Calculates construction waste emissions (A5_3 module) based on construction emissions parameters. The method computes waste factors according to IStructE guidance and returns the A5_3 module representing construction waste impacts.")] + [Input("resultingValues", "Dictionary of module values to which the construction emissions will be added. Must contain required modules (A1toA3 or A1+A2+A3, A4, C3toC4 or C3 or C4, and optionally C2 if not reused on site).")] + [Input("constructionEmissions", "Construction emissions parameters including waste rate and whether materials are reused on site.")] + [Input("metricType", "The metric type being evaluated, used for error reporting when required modules are missing.")] + [Output("constructionWasteEmissions", "The calculated construction waste emissions as a double value.")] + public static double ConstructionWasteEmissions(this Dictionary resultingValues, ConstructionWasteEmissions constructionEmissions, MetricType metricType) + { + + if(resultingValues == null || constructionEmissions?.WasteRate == null) + return double.NaN; + + double total = 0; + List missingModules = new List(); + + //Get out the cradle to gate metrics + if (resultingValues.TryGetModuleValue(Module.A1toA3, out double a1a3)) + total += a1a3; + else + missingModules.Add(Module.A1toA3); + + //Get out the transport metrics + if(resultingValues.TryGetModuleValue(Module.A4, out double a4)) + total += a4; + else + missingModules.Add(Module.A4); + + //Get out the disposal metrics + if (!constructionEmissions.ResuedOnSite) + { + if (resultingValues.TryGetModuleValue(Module.C2, out double c2)) + total += c2; //Add C2 if not re-used on site + else + missingModules.Add(Module.C2); + } + + //Get out the disposal metrics + if (resultingValues.TryGetModuleValue(Module.C3toC4, out double c3c4, false)) + total += c3c4; + else + missingModules.Add(Module.C3toC4); + + if (missingModules.Any()) + { + string message = $"Missing modules for waste computation of waste factors (A5.3) for metric of type {metricType}: {string.Join(", ", missingModules)} (or their subparts)."; + BH.Engine.Base.Compute.RecordError(message); + return double.NaN; + } + + return total * constructionEmissions.WasteFactor(); + + } + + /***************************************************/ + + } +} + + + From 4f54e6290f75e991e79d4d0bbdd7c98a08c364b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isak=20N=C3=A4slund?= Date: Thu, 3 Sep 2026 16:55:31 +0200 Subject: [PATCH 05/25] Refactor out the handling of the EvaluationConfigData Handling the computation of the actual values to be evaluated outside the ResultingModuleValue method. Simplifies the code a bit, and makes it possible to re-use the existing mechanism for ensuring there are not miss matches in the modules. --- .../LifeCycleAssessment_Engine_Tests.csproj | 2 +- .../ResultingModuleValues.cs | 2 +- .../Query/EnvironmentalResults.cs | 105 +++-------- .../Query/EvaluationConfigData.cs | 166 +++++++++++++++++ .../Query/ResultingModuleValues.cs | 168 +----------------- 5 files changed, 201 insertions(+), 242 deletions(-) create mode 100644 LifeCycleAssessment_Engine/Query/EvaluationConfigData.cs diff --git a/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/LifeCycleAssessment_Engine_Tests.csproj b/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/LifeCycleAssessment_Engine_Tests.csproj index ab23949..1662d3d 100644 --- a/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/LifeCycleAssessment_Engine_Tests.csproj +++ b/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/LifeCycleAssessment_Engine_Tests.csproj @@ -1,4 +1,4 @@ - + net6.0 diff --git a/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/ResultingModuleValues.cs b/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/ResultingModuleValues.cs index 14b167c..8fca0e4 100644 --- a/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/ResultingModuleValues.cs +++ b/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/ResultingModuleValues.cs @@ -51,7 +51,7 @@ public class ResultingModuleValues : NUnitTest public void ResultingModuleValuesDoesNotContainCombinationOrParts(IEnvironmentalMetric metric, Dictionary precomputedValues, List expectedModules) { - var resultingValues = Query.IResultingModuleValues(metric, 1, null, precomputedValues, null); + var resultingValues = Query.IResultingModuleValues(metric, 1, precomputedValues); Assert.That(resultingValues.Keys, Is.EquivalentTo(expectedModules), "Resulting module values should contain either combination modules or their parts, but not both."); diff --git a/LifeCycleAssessment_Engine/Query/EnvironmentalResults.cs b/LifeCycleAssessment_Engine/Query/EnvironmentalResults.cs index eef9cd9..caaf064 100644 --- a/LifeCycleAssessment_Engine/Query/EnvironmentalResults.cs +++ b/LifeCycleAssessment_Engine/Query/EnvironmentalResults.cs @@ -33,6 +33,7 @@ using BH.oM.LifeCycleAssessment.MaterialFragments.Transport; using BH.oM.LifeCycleAssessment.Results; using BH.oM.Physical.Materials; +using BH.oM.Quantities.Attributes; using System; using System.Collections; using System.Collections.Generic; @@ -186,6 +187,7 @@ public static List EnvironmentalResults(this TakeoffItem takeoff /***************************************************/ + [PreviousVersion("9.3", "BH.Engine.LifeCycleAssessment.Query.EnvironmentalResults(BH.oM.LifeCycleAssessment.MaterialFragments.EnvironmentalProductDeclaration, System.Double, System.String, System.Collections.Generic.List, BH.oM.LifeCycleAssessment.Configs.IEvaluationConfig, System.Object)")] [Description("Evaluates all or selected metrics stored on the EnvironmentalProductDeclaration (EPD) and returns a result per metric.\n" + "Each metric is evaluated by multiplying the values for each module by the provided quantityValue.\n" + "Please be mindful that the unit of the quantityValue should match the QuantityType on the EnvironmentalProductDeclaration.")] @@ -194,10 +196,10 @@ public static List EnvironmentalResults(this TakeoffItem takeoff [Input("materialName", "The name of the Material that owns the EnvironmentalProductDeclaration. Stored as an identifier on the returned result classes.")] [Input("metricFilter", "Optional filter for the provided EnvironmentalProductDeclaration for selecting one or more of the provided metrics for calculation. This method also accepts multiple metric types simultaneously. If nothing is provided then no filtering is assumed, i.e. all metrics on the found EPDS are evaluated.")] [Input("evaluationConfig", "Config controlling how the metrics should be evaluated, may contain additional parameters for the evaluation. If no config is provided the default evaluation mechanism is used which computes resulting module values as metric value times applicable quantity.")] - [Input("configData", "Additional data required for evaluation with the provided config. If no config is provided, this input can be left empty. Type of data expected depends on the config. For the IStructEEvaluationConfig the mass should be provided here.")] + [Input("mass", "Additional mass data required for evaluation with the provided config. If no config is provided, this input can be left empty.", typeof(Mass))] [Output("results", "List of MaterialResults corresponding to the evaluated metrics on the EPD.")] [PreviousInputNames("quantityValue", "referenceValue")] - public static List EnvironmentalResults(this EnvironmentalProductDeclaration epd, double quantityValue, string materialName = "", List metricFilter = null, IEvaluationConfig evaluationConfig = null, object configData = null) + public static List EnvironmentalResults(this EnvironmentalProductDeclaration epd, double quantityValue, string materialName = "", List metricFilter = null, IEvaluationConfig evaluationConfig = null, double mass = double.NaN) { if (epd == null) { @@ -209,7 +211,7 @@ public static List EnvironmentalResults(this EnvironmentalProduc foreach (IEnvironmentalMetric metric in epd.FilteredFactors(metricFilter)) { - results.Add(EnvironmentalResults(metric, epd.Name, materialName, quantityValue, evaluationConfig, configData)); + results.Add(EnvironmentalResults(metric, epd.Name, materialName, quantityValue, evaluationConfig, mass)); } return results; @@ -217,12 +219,13 @@ public static List EnvironmentalResults(this EnvironmentalProduc /***************************************************/ + [PreviousVersion("9.3", "BH.Engine.LifeCycleAssessment.Query.EnvironmentalResults(BH.oM.LifeCycleAssessment.MaterialFragments.CombinedLifeCycleAssessmentFactors, System.Double, System.Double, System.String, System.Collections.Generic.List, BH.oM.LifeCycleAssessment.Configs.IEvaluationConfig, System.Object)")] [Description("Evaluates all or selected metrics stored on the EnvironmentalProductDeclaration (EPD) and returns a result per metric.\n" + "Each metric is evaluated by multiplying the values for each module by the provided quantityValue.\n" + "Please be mindful that the unit of the quantityValue should match the QuantityType on the EnvironmentalProductDeclaration.")] [Input("factorsProvider", "The CombinedLifeCycleAssessmentFactors to evaluate. Returned results will correspond to all, or selected, metrics stored on this object.")] [Input("quantityValue", "The quantity value to evaluate all metrics by. All metric properties will be multiplied by this value. Quantity should correspond to the QuantityType on the EPD.")] - [Input("mass", "The mass value to transport scenarios by. Can for the case of quantity type of the APD being mass, this should be equal to the quantityValue.")] + [Input("mass", "The mass value to transport scenarios by. Can for the case of quantity type of the EPD being mass, this should be equal to the quantityValue.", typeof(Mass))] [Input("materialName", "The name of the Material that owns the EnvironmentalProductDeclaration. Stored as an identifier on the returned result classes.")] [Input("metricFilter", "Optional filter for the provided EnvironmentalProductDeclaration for selecting one or more of the provided metrics for calculation. This method also accepts multiple metric types simultaneously. If nothing is provided then no filtering is assumed, i.e. all metrics on the found EPDS are evaluated.")] [Input("evaluationConfig", "Config controlling how the metrics should be evaluated, may contain additional parameters for the evaluation. If no config is provided the default evaluation mechanism is used which computes resulting module values as metric value times applicable quantity.")] @@ -237,11 +240,14 @@ public static List EnvironmentalResults(this CombinedLifeCycleAs return null; } + //Setup precomputed values based on the config + Dictionary precomputedModules = evaluationConfig.IEvaluationConfigData(quantityValue, mass); + Dictionary a4TransportResults = factorsProvider.A4TransportFactors?.ITransportResults(mass, metricFilter) ?? new Dictionary(); Dictionary c2TransportResults = factorsProvider.C2TransportFactors?.ITransportResults(mass, metricFilter) ?? new Dictionary(); Dictionary c3c4Emissions = factorsProvider.C3C4WasteAndDisposalFactors?.WasteAndDisposalResults(mass, quantityValue, factorsProvider.EnvironmentalProductDeclaration.EnvironmentalMetrics) ?? new Dictionary(); - Dictionary precomputedModules = new Dictionary(); + if (a4TransportResults.Any()) precomputedModules[Module.A4] = new PrecomputedModuleValues() { ModuleValues = a4TransportResults, OverwriteExistingValues = true }; @@ -257,7 +263,7 @@ public static List EnvironmentalResults(this CombinedLifeCycleAs foreach (IEnvironmentalMetric metric in factorsProvider.EnvironmentalProductDeclaration.FilteredFactors(metricFilter)) { MetricType type = metric.IMetricType(); - Dictionary resultingValues = metric.IResultingModuleValues(quantityValue, evaluationConfig, precomputedModules, configData); + Dictionary resultingValues = metric.IResultingModuleValues(quantityValue, precomputedModules); //Check if C2 and A4 results are defined explicitly, and override them if they are a4TransportResults.Remove(type); //Remove as used up @@ -302,6 +308,7 @@ public static List EnvironmentalResults(this CombinedLifeCycleAs /***************************************************/ + [PreviousVersion("9.3", "BH.Engine.LifeCycleAssessment.Query.EnvironmentalResults(BH.oM.LifeCycleAssessment.MaterialFragments.IEnvironmentalMetric, System.String, System.String, System.Double, BH.oM.LifeCycleAssessment.Configs.IEvaluationConfig, System.Object)")] [Description("Evaluates the EnvironmentalMetric and returns a MaterialResult of a type corresponding to the metric. The evaluation is done by multiplying all module data on the metric by the provided quantityValue.\n" + "Please be mindful that the unit of the quantityValue should match the QuantityType on the EnvironmentalProductDeclaration to which the metric belongs.")] [Input("metric", "The EnvironmentalMetric to evaluate. Returned result will be a MaterialResult of a type corresponding to the metric.")] @@ -309,9 +316,9 @@ public static List EnvironmentalResults(this CombinedLifeCycleAs [Input("materialName", "The name of the Material that owns the EnvironmentalProductDeclaration. Stored as an identifier on the returned result class.")] [Input("quantityValue", "The quantity value to evaluate all metrics by. All metric properties will be multiplied by this value. Quantity should correspond to the QuantityType on the EPD.")] [Input("evaluationConfig", "Config controlling how the metrics should be evaluated, may contain additional parameters for the evaluation. If no config is provided the default evaluation mechanism is used which computes resulting module values as metric value times quantity.")] - [Input("configData", "Additional data required for evaluation with the provided config. If no config is provided, this input can be left empty. Type of data expected depends on the config. For the IStructEEvaluationConfig the mass should be provided here.")] + [Input("mass", "Additional mass data required for evaluation with the provided config. If no config is provided, this input can be left empty.", typeof(Mass))] [Output("result", "A MaterialResult of a type corresponding to the evaluated metric with module data calculated as data on metric multiplied by the provided quantity value.")] - public static MaterialResult EnvironmentalResults(this IEnvironmentalMetric metric, string epdName, string materialName, double quantityValue, IEvaluationConfig evaluationConfig = null, object configData = null) + public static MaterialResult EnvironmentalResults(this IEnvironmentalMetric metric, string epdName, string materialName, double quantityValue, IEvaluationConfig evaluationConfig = null, double mass = double.NaN) { if (metric == null) { @@ -319,7 +326,12 @@ public static MaterialResult EnvironmentalResults(this IEnvironmentalMetric metr return null; } - IDictionary resultingValues = metric.IResultingModuleValues(quantityValue, evaluationConfig, null, configData); + Dictionary precomputedModules = null; + + if (evaluationConfig != null && !double.IsNaN(mass)) + precomputedModules = evaluationConfig.IEvaluationConfigData(quantityValue, mass); + + IDictionary resultingValues = metric.IResultingModuleValues(quantityValue, precomputedModules); return Create.MaterialResult(materialName, epdName, metric.IMetricType(), resultingValues as dynamic); } @@ -330,95 +342,36 @@ public static MaterialResult EnvironmentalResults(this IEnvironmentalMetric metr private static List IEnvironmentalResults(this IEnvironmentalFactorsProvider factorsProvider, TakeoffItem takeoffItem, List metricFilter, IEvaluationConfig evaluationConfig) { - if (ITryGetConfigData(evaluationConfig, takeoffItem, out object configData)) - { - return EnvironmentalResults(factorsProvider as dynamic, takeoffItem, metricFilter, evaluationConfig, configData); - } - return new List(); + + return EnvironmentalResults(factorsProvider as dynamic, takeoffItem, metricFilter, evaluationConfig); + } /***************************************************/ - private static List EnvironmentalResults(this EnvironmentalProductDeclaration factorsProvider, TakeoffItem takeoffItem, List metricFilter, IEvaluationConfig evaluationConfig, object configData) + private static List EnvironmentalResults(this EnvironmentalProductDeclaration factorsProvider, TakeoffItem takeoffItem, List metricFilter, IEvaluationConfig evaluationConfig) { - return EnvironmentalResults(factorsProvider, takeoffItem.QuantityValue(factorsProvider.QuantityType), takeoffItem.Material.Name, metricFilter, evaluationConfig, configData); + return EnvironmentalResults(factorsProvider, takeoffItem.QuantityValue(factorsProvider.QuantityType), takeoffItem.Material.Name, metricFilter, evaluationConfig, takeoffItem.QuantityValue(QuantityType.Mass)); } /***************************************************/ - private static List EnvironmentalResults(this CombinedLifeCycleAssessmentFactors factorsProvider, TakeoffItem takeoffItem, List metricFilter, IEvaluationConfig evaluationConfig, object configData) + private static List EnvironmentalResults(this CombinedLifeCycleAssessmentFactors factorsProvider, TakeoffItem takeoffItem, List metricFilter, IEvaluationConfig evaluationConfig) { double quantityValue = takeoffItem.QuantityValue(factorsProvider.EnvironmentalProductDeclaration.QuantityType); double mass = takeoffItem.QuantityValue(QuantityType.Mass); - return EnvironmentalResults(factorsProvider, quantityValue, mass, takeoffItem.Material.Name, metricFilter, evaluationConfig, configData); + return EnvironmentalResults(factorsProvider, quantityValue, mass, takeoffItem.Material.Name, metricFilter, evaluationConfig); } /***************************************************/ - private static List EnvironmentalResults(this IEnvironmentalFactorsProvider factorsProvider, TakeoffItem takeoffItem, List metricFilter, IEvaluationConfig evaluationConfig, object configData) + private static List EnvironmentalResults(this IEnvironmentalFactorsProvider factorsProvider, TakeoffItem takeoffItem, List metricFilter, IEvaluationConfig evaluationConfig) { BH.Engine.Base.Compute.RecordWarning($"No evaluation method implemented for metric providers of type {factorsProvider.GetType().Name}"); return new List(); } - /***************************************************/ - /**** Private Methods - Extract config data ****/ - /***************************************************/ - - private static bool ITryGetConfigData(this IEvaluationConfig evaluationConfig, TakeoffItem takeoffItem, out object configData) - { - if (evaluationConfig == null) - { - configData = null; - return true; - } - - return TryGetConfigData(evaluationConfig as dynamic, takeoffItem, out configData); - } - - /***************************************************/ - - private static bool TryGetConfigData(this IStructEEvaluationConfig evaluationConfig, TakeoffItem takeoffItem, out object configData) - { - double mass = takeoffItem.QuantityValue(QuantityType.Mass); - - configData = mass; - if (double.IsNaN(mass) || mass == 0) - { - BH.Engine.Base.Compute.RecordError($"Unable to extract required mass required to evaluate with the {nameof(IStructEEvaluationConfig)}."); - return false; - } - - return true; - } - - /***************************************************/ - - private static bool TryGetConfigData(this GlobalEmissionFactors evaluationConfig, TakeoffItem takeoffItem, out object configData) - { - double mass = takeoffItem.QuantityValue(QuantityType.Mass); - - configData = mass; - if (double.IsNaN(mass) || mass == 0) - { - BH.Engine.Base.Compute.RecordError($"Unable to extract required mass required to evaluate with the {nameof(IStructEEvaluationConfig)}."); - return false; - } - - return true; - } - - /***************************************************/ - - private static bool TryGetConfigData(this IEvaluationConfig evaluationConfig, TakeoffItem takeoffItem, out object configData) - { - //Fallback method - BH.Engine.Base.Compute.RecordWarning($"No method exists to extract config data for config of type {evaluationConfig.GetType().Name}."); - configData = null; - return true; //Return true to continue evaluation - } - /***************************************************/ } } diff --git a/LifeCycleAssessment_Engine/Query/EvaluationConfigData.cs b/LifeCycleAssessment_Engine/Query/EvaluationConfigData.cs new file mode 100644 index 0000000..29842d9 --- /dev/null +++ b/LifeCycleAssessment_Engine/Query/EvaluationConfigData.cs @@ -0,0 +1,166 @@ +/* + * This file is part of the Buildings and Habitats object Model (BHoM) + * Copyright (c) 2015 - 2026, the respective contributors. All rights reserved. + * + * Each contributor holds copyright over their respective contributions. + * The project versioning (Git) records all such contribution source information. + * + * + * The BHoM is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, or + * (at your option) any later version. + * + * The BHoM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this code. If not, see . + */ + +using BH.oM.Base; +using BH.oM.Base.Attributes; +using BH.oM.LifeCycleAssessment; +using BH.oM.LifeCycleAssessment.Configs; +using BH.oM.LifeCycleAssessment.Interfaces; +using BH.oM.LifeCycleAssessment.MaterialFragments; +using BH.oM.LifeCycleAssessment.Results; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; + +namespace BH.Engine.LifeCycleAssessment +{ + public static partial class Query + { + /***************************************************/ + /**** Public Methods ****/ + /***************************************************/ + + [Description("Provides pre-computed module values for the provided evaluation config. This is used to provide additional module values for modules that are evaluated based on project level values, such as A5_2 and C1.")] + [Input("evaluationConfig", "The evaluation config to get the pre-computed module values for.")] + [Input("quantityValue", "The quantity value of the element to be evaluated.")] + [Input("mass", "The mass of the element to be evaluated. This is used to calculate the weight factor for the pre-computed module values.")] + [Output("preComputedModuleValues", "The pre-computed module values for the provided evaluation config.")] + public static Dictionary IEvaluationConfigData(this IEvaluationConfig evaluationConfig, double quantityValue, double mass) + { + if (evaluationConfig == null) + return new Dictionary(); + + return EvaluationConfigData(evaluationConfig as dynamic, quantityValue, mass); + } + + /***************************************************/ + + [Description("Provides pre-computed module values for the provided IStructEEvaluationConfig. This is used to provide additional module values for modules that are evaluated based on project level values, such as A5_2 and C1.")] + [Input("evaluationConfig", "The IStructEEvaluationConfig to get the pre-computed module values for.")] + [Input("quantityValue", "The quantity value of the element to be evaluated.")] + [Input("mass", "The mass of the element to be evaluated. This is used to calculate the weight factor for the pre-computed module values.")] + [Output("preComputedModuleValues", "The pre-computed module values for the provided IStructEEvaluationConfig.")] + public static Dictionary EvaluationConfigData(this IStructEEvaluationConfig evaluationConfig, double quantityValue, double mass) + { + if(evaluationConfig == null) + return new Dictionary(); + + double weightFactor; + + if (evaluationConfig.TotalWeight == 0 || evaluationConfig.TotalWeight < mass) + { + BH.Engine.Base.Compute.RecordWarning($"The total weight is 0 or smaller than the mass of the element. The weightfactor has been set to 0. This has an influence on the {nameof(Module.A5_2)} and {nameof(Module.C1)} modules, which will be given 0 value results"); + weightFactor = 0; + } + else + weightFactor = mass / evaluationConfig.TotalWeight; + + List applicableTypes = new List { MetricType.ClimateChangeTotal, MetricType.ClimateChangeTotalNoBiogenic, MetricType.ClimateChangeFossil }; + + + //Special handling of A5_2 for site activities module with additional project factor + double a5_2Value = evaluationConfig.ProjectCost * evaluationConfig.A5CarbonFactor * weightFactor; //Set as portion of total project value + PrecomputedModuleValues a5_2Values = new PrecomputedModuleValues() + { + OverwriteExistingValues = true, + ModuleValues = applicableTypes.ToDictionary(x => x, x => a5_2Value) + }; + + //C1 evaluated based on project level values + double c1Value = evaluationConfig.FloorArea * evaluationConfig.C1CarbonFactor * weightFactor; //Set as portion of total project value + PrecomputedModuleValues c1Values = new PrecomputedModuleValues() + { + OverwriteExistingValues = true, + ModuleValues = applicableTypes.ToDictionary(x => x, x => c1Value) + }; + + return new Dictionary + { + { Module.A5_2, a5_2Values }, + { Module.C1, c1Values } + }; + } + + /***************************************************/ + + [Description("Provides pre-computed module values for the provided GlobalEmissionFactors. This is used to provide additional module values for modules that are evaluated based on project level values, such as A5_1 and A5_2.")] + [Input("evaluationConfig", "The GlobalEmissionFactors to get the pre-computed module values for.")] + [Input("quantityValue", "The quantity value of the element to be evaluated.")] + [Input("mass", "The mass of the element to be evaluated. This is used to calculate the weight factor for the pre-computed module values.")] + [Output("preComputedModuleValues", "The pre-computed module values for the provided GlobalEmissionFactors.")] + public static Dictionary EvaluationConfigData(this GlobalEmissionFactors evaluationConfig, double quantityValue, double mass) + { + if(evaluationConfig == null) + return new Dictionary(); + + double weightFactor; + + if (evaluationConfig.TotalBuildingMass == 0 || evaluationConfig.TotalBuildingMass < mass) + { + BH.Engine.Base.Compute.RecordWarning($"The total weight is 0 or smaller than the mass of the element. The weightfactor has been set to 0. This has an influence on the {nameof(Module.A5_1)} and {nameof(Module.A5_2)} modules, which will be given 0 value results"); + weightFactor = 0; + } + else + weightFactor = mass / evaluationConfig.TotalBuildingMass; + + if (evaluationConfig.StructuresOnlyMass) + weightFactor /= 2; //Divide weight factor by 2 if only structures mass is considered as the total building mass is for the whole building + + + //Special handling of A5_1 for pre construction demolition module + PrecomputedModuleValues a5_1Values = new PrecomputedModuleValues() + { + OverwriteExistingValues = true, + ModuleValues = evaluationConfig.PreConstructionDemolition.EnvironmentalFactors.ToDictionary(x => x.IMetricType(), x => x.Value * evaluationConfig.PreConstructionDemolition.DemolishedFloorArea * weightFactor) //Set as portion of total project value + }; + + //Special handling of A5_2 for site activities module with additional project factor + PrecomputedModuleValues a5_2Values = new PrecomputedModuleValues() + { + OverwriteExistingValues = true, + ModuleValues = evaluationConfig.ConstructionActivities.EnvironmentalFactors.ToDictionary(x => x.IMetricType(), x => x.Value * evaluationConfig.ConstructionActivities.ConstructedFloorArea * weightFactor) //Set as portion of total project value + }; + + return new Dictionary + { + { Module.A5_1, a5_1Values }, + { Module.A5_2, a5_2Values } + }; + } + + /***************************************************/ + /**** Private Method - fallback ****/ + /***************************************************/ + + public static Dictionary EvaluationConfigData(this IEvaluationConfig evaluationConfig, double quantityValue, double mass) + { + BH.Engine.Base.Compute.RecordWarning($"The provided evaluation config of type {evaluationConfig.GetType().Name} is not supported for pre-computation of module values. No pre-computed values will be provided for the modules."); + return null; + } + + /***************************************************/ + } +} + + + diff --git a/LifeCycleAssessment_Engine/Query/ResultingModuleValues.cs b/LifeCycleAssessment_Engine/Query/ResultingModuleValues.cs index 1dda5a2..be6300e 100644 --- a/LifeCycleAssessment_Engine/Query/ResultingModuleValues.cs +++ b/LifeCycleAssessment_Engine/Query/ResultingModuleValues.cs @@ -40,15 +40,15 @@ public static partial class Query /**** Public Methods ****/ /***************************************************/ + [PreviousVersion("9.3", "BH.Engine.LifeCycleAssessment.Query.IResultingModuleValues(BH.oM.LifeCycleAssessment.MaterialFragments.IEnvironmentalMetric, System.Double, BH.oM.LifeCycleAssessment.Configs.IEvaluationConfig, System.Collections.Generic.Dictionary, System.Object)")] [Description("Gets the resulting values for each module of the provided EnvironmentalMetric given the provided quantityValue.\n" + "The resulting values are computed based on provided config, defaulting to the values on the metric for each module multiplied by the quantity value.\n" + "Please be mindful that the unit of the quantityValue should match the QuantityType on the EnvironmentalProductDeclaration to which the metric belongs.")] [Input("metric", "The EnvironmentalMetric to get resulting values for. All module values on the metric will be extracted and multiplied by the qunatityValue.")] [Input("quantityValue", "The quantity value to evaluate all metrics by. All metric properties will be multiplied by this value. Quantity should correspond to the QuantityType on the EPD.")] - [Input("evaluationConfig", "Config controlling how the metrics should be evaluated, may contain additional parameters for the evaluation. If no config is provided the default evaluation mechanism is used which computes resulting module values as metric value times applicable quantity.")] - [Input("configData", "Additional data required for evaluation with the provided config. If no config is provided, this input can be left empty. Type of data expected depends on the config. For the IStructEEvaluationConfig the mass should be provided here.")] + [Input("precomputedValues", "Precomputed values for particular modules and metrics where the result values have been computed by other means than taking the quantityvalue times the metric factor. This can be user set, but is also used by the internal system for IEvaluationCOnfigs as well as for pre-computed values for things like transport and waste management on the CombinedLifecycleAssessmentFactors.")] [Output("resultValues", "The resulting values for each module.")] - public static Dictionary IResultingModuleValues(this IEnvironmentalMetric metric, double quantityValue, IEvaluationConfig evaluationConfig, Dictionary precomputedValues, object configData) + public static Dictionary IResultingModuleValues(this IEnvironmentalMetric metric, double quantityValue, Dictionary precomputedValues) { if (metric == null) { @@ -66,12 +66,8 @@ public static Dictionary IResultingModuleValues(this IEnvironmen if (metric.Indicators.Count == 0) return new Dictionary(); + return ResultingModuleValues(metric, quantityValue, precomputedValues); - - if (evaluationConfig == null) //For case of null config, use default evaluation methodology of module data value * quantity for each module - return ResultingModuleValues(metric, quantityValue, precomputedValues); - else - return ResultingModuleValues(metric, quantityValue, evaluationConfig as dynamic, precomputedValues, configData); } /***************************************************/ @@ -140,162 +136,6 @@ private static void RemoveCombinationParts(this Dictionary resul /***************************************************/ - [Description("IStructE Evaluation methodology for getting the resulting values for each module of the provided EnvironmentalMetric given the provided quantityValue.\n" + - "Evaluation method only applicable for the two CLimateChangeTotal metric types - all other metrics are evaluated using the default mechanism.\n" + - "Method works for most modules works the same as default evaluation mechanism, with exception for the C1 and A5 module where project totals are acounted for.")] - [Input("metric", "The EnvironmentalMetric to get resulting values for. All module values on the metric will be extracted and multiplied by the qunatityValue.")] - [Input("quantityValue", "The quantity value to evaluate all metrics by. All metric properties will be multiplied by this value. Quantity should correspond to the QuantityType on the EPD.")] - [Input("configData", "Additional data required for evaluation with the provided config. If no config is provided, this input can be left empty. Type of data expected depends on the config. For the IStructEEvaluationConfig the mass should be provided here.")] - [Output("resultValues", "The resulting values for each module.")] - private static Dictionary ResultingModuleValues(this IEnvironmentalMetric metric, double quantityValue, GlobalEmissionFactors evaluationConfig, Dictionary precomputedValues, object configData) - { - - //Specific evaluation method using the config only applicable for evaluatingResultingModuleValues(this Dictionary < Module, double > moduleFactors climate change totals - MetricType metricType = metric.IMetricType(); - - double mass = 0; - bool massProvided = false; - if (configData != null) - { - if (configData is double) - { - mass = (double)configData; - massProvided = true; - } - else if (double.TryParse(configData.ToString(), out mass)) - massProvided = true; - } - - if (!massProvided) - { - Engine.Base.Compute.RecordError($"Please provide the mass of the evaluated object in the configData when evaluating metrics with the {nameof(GlobalEmissionFactors)}."); - return new Dictionary(); - } - - double weightFactor; - - if (evaluationConfig.TotalBuildingMass == 0 || evaluationConfig.TotalBuildingMass < mass) - { - BH.Engine.Base.Compute.RecordWarning($"The total weight is 0 or smaller than the mass of the element. The weightfactor has been set to 0. This has an influence on the {nameof(Module.A5_1)} and {nameof(Module.A5_2)} modules, which will be given 0 value results"); - weightFactor = 0; - } - else - weightFactor = mass / evaluationConfig.TotalBuildingMass; - - if(evaluationConfig.StructuresOnlyMass) - weightFactor /= 2; //Divide weight factor by 2 if only structures mass is considered as the total building mass is for the whole building - - //Set up base line factors - Dictionary resultingValues = ResultingModuleValues(metric, quantityValue, precomputedValues); - - //Special handling of A5_1 for pre construction demolition module - IEnvironmentalFactor preConstructionFactor = evaluationConfig.PreConstructionDemolition?.EnvironmentalFactors?.FirstOrDefault(x => x.IMetricType() == metricType); - if(preConstructionFactor != null && evaluationConfig.PreConstructionDemolition.DemolishedFloorArea != 0) - resultingValues[Module.A5_1] = preConstructionFactor.Value * evaluationConfig.PreConstructionDemolition.DemolishedFloorArea * weightFactor; //Set as portion of total project value - - //Special handling of A5_2 for site activities module with additional project factor - IEnvironmentalFactor siteActivitiesFactor = evaluationConfig.ConstructionActivities?.EnvironmentalFactors?.FirstOrDefault(x => x.IMetricType() == metricType); - if(preConstructionFactor != null && evaluationConfig.ConstructionActivities.ConstructedFloorArea != 0) - resultingValues[Module.A5_2] = siteActivitiesFactor.Value * evaluationConfig.ConstructionActivities.ConstructedFloorArea * weightFactor; - - if(preConstructionFactor != null || siteActivitiesFactor != null) - resultingValues.Remove(Module.A5); //Remove existing A5 value as it will be different than the sum of its parts - - return resultingValues; - } - - /***************************************************/ - - [Description("IStructE Evaluation methodology for getting the resulting values for each module of the provided EnvironmentalMetric given the provided quantityValue.\n" + - "Evaluation method only applicable for the two CLimateChangeTotal metric types - all other metrics are evaluated using the default mechanism.\n" + - "Method works for most modules works the same as default evaluation mechanism, with exception for the C1 and A5 module where project totals are acounted for.")] - [Input("metric", "The EnvironmentalMetric to get resulting values for. All module values on the metric will be extracted and multiplied by the qunatityValue.")] - [Input("quantityValue", "The quantity value to evaluate all metrics by. All metric properties will be multiplied by this value. Quantity should correspond to the QuantityType on the EPD.")] - [Input("configData", "Additional data required for evaluation with the provided config. If no config is provided, this input can be left empty. Type of data expected depends on the config. For the IStructEEvaluationConfig the mass should be provided here.")] - [Output("resultValues", "The resulting values for each module.")] - private static Dictionary ResultingModuleValues(this IEnvironmentalMetric metric, double quantityValue, IStructEEvaluationConfig evaluationConfig, Dictionary precomputedValues, object configData) - { - - //Specific evaluation method using the config only applicable for evaluatingResultingModuleValues(this Dictionary < Module, double > moduleFactors climate change totals - MetricType metricType = metric.IMetricType(); - List applicableTypes = new List { MetricType.ClimateChangeTotal, MetricType.ClimateChangeTotalNoBiogenic, MetricType.ClimateChangeFossil }; - if (!applicableTypes.Any(x => x == metricType)) - { - Base.Compute.RecordNote($"The {nameof(IStructEEvaluationConfig)} evaluation is only applicable for evaluating metrics of type {string.Join(",", applicableTypes)}." + - $"All other metrics are evaluated based on standard evaluation procedure of module times quantity for all modules."); - return ResultingModuleValues(metric, quantityValue, precomputedValues); - } - - double mass = 0; - bool massProvided = false; - if (configData != null) - { - if (configData is double) - { - mass = (double)configData; - massProvided = true; - } - else if(double.TryParse(configData.ToString(), out mass)) - massProvided = true; - } - - if (!massProvided) - { - Engine.Base.Compute.RecordError($"Please provide the mass of the evaluated object in the configData when evaluating metrics with the {nameof(IStructEEvaluationConfig)}."); - return new Dictionary(); - } - - double weightFactor; - - if (evaluationConfig.TotalWeight == 0 || evaluationConfig.TotalWeight < mass) - { - BH.Engine.Base.Compute.RecordWarning($"The total weight is 0 or smaller than the mass of the element. The weightfactor has been set to 0. This has an influence on the {nameof(Module.A5_2)} and {nameof(Module.C1)} modules, which will be given 0 value results"); - weightFactor = 0; - } - else - weightFactor = mass/ evaluationConfig.TotalWeight; - - //Set up base line factors - Dictionary resultingValues = ResultingModuleValues(metric, quantityValue, precomputedValues); - - //Special handling of A5 for site activities module with additional project factor - resultingValues[Module.A5_2] = evaluationConfig.ProjectCost * evaluationConfig.A5CarbonFactor * weightFactor; - - //C1 evaluated based on project level values - resultingValues[Module.C1] = weightFactor * evaluationConfig.FloorArea * evaluationConfig.C1CarbonFactor; - - //Check if C1toC4 was computed and needs to be udpated given the explicit computation of C1 - if (resultingValues.ContainsKey(Module.C1toC4)) - { - //If contains all parts -> update the total - if (resultingValues.ContainsKey(Module.C2) && resultingValues.ContainsKey(Module.C3) && resultingValues.ContainsKey(Module.C4)) - { - resultingValues[Module.C1toC4] = resultingValues[Module.C1] + resultingValues[Module.C2] + resultingValues[Module.C3] + resultingValues[Module.C4]; - - } - else - resultingValues.Remove(Module.C1toC4); //If not, remove as total will be different than parts - } - - return resultingValues; - } - - /***************************************************/ - /**** Private Methods - Evaluation - Fallback ****/ - /***************************************************/ - - [Description("Fallback method for unkown config provided, raising warning and calling the defautl evaluation mechanism. Please note that this method is not triggered for null config, which also calls default mechism, but without warning.")] - [Input("metric", "The EnvironmentalMetric to get resulting values for. All module values on the metric will be extracted and multiplied by the qunatityValue.")] - [Input("quantityValue", "The quantity value to evaluate all metrics by. All metric properties will be multiplied by this value. Quantity should correspond to the QuantityType on the EPD.")] - [Output("resultValues", "The resulting values for each module.")] - private static Dictionary ResultingModuleValues(this IEnvironmentalMetric metric, double quantityValue, IEvaluationConfig evaluationConfig, Dictionary precomputedValues, object configData) - { - BH.Engine.Base.Compute.RecordWarning($"No evaluation method implemented for evaluation config of type {evaluationConfig}. Results returned are based on default evaluation method of module values times quantity."); - - return ResultingModuleValues(metric, quantityValue, precomputedValues); - } - - /***************************************************/ } } From 8afb63572dce400c4da7f7c5f92daafc22a7410b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isak=20N=C3=A4slund?= Date: Fri, 4 Sep 2026 11:32:40 +0200 Subject: [PATCH 06/25] Fix compliance issues --- .../ResultingModuleValues.cs | 2 +- LifeCycleAssessment_Engine/Query/EvaluationConfigData.cs | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/ResultingModuleValues.cs b/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/ResultingModuleValues.cs index 8fca0e4..4810cbf 100644 --- a/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/ResultingModuleValues.cs +++ b/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/ResultingModuleValues.cs @@ -1,6 +1,6 @@ /* * This file is part of the Buildings and Habitats object Model (BHoM) - * Copyright (c) 2015 - 2025, the respective contributors. All rights reserved. + * Copyright (c) 2015 - 2026, the respective contributors. All rights reserved. * * Each contributor holds copyright over their respective contributions. * The project versioning (Git) records all such contribution source information. diff --git a/LifeCycleAssessment_Engine/Query/EvaluationConfigData.cs b/LifeCycleAssessment_Engine/Query/EvaluationConfigData.cs index 29842d9..29129e5 100644 --- a/LifeCycleAssessment_Engine/Query/EvaluationConfigData.cs +++ b/LifeCycleAssessment_Engine/Query/EvaluationConfigData.cs @@ -152,10 +152,11 @@ public static Dictionary EvaluationConfigData(t /**** Private Method - fallback ****/ /***************************************************/ - public static Dictionary EvaluationConfigData(this IEvaluationConfig evaluationConfig, double quantityValue, double mass) + [Description("Fallback method for providing pre-computed module values for unsupported evaluation configs. This will log a warning and return an empty dictionary.")] + private static Dictionary EvaluationConfigData(this IEvaluationConfig evaluationConfig, double quantityValue, double mass) { BH.Engine.Base.Compute.RecordWarning($"The provided evaluation config of type {evaluationConfig.GetType().Name} is not supported for pre-computation of module values. No pre-computed values will be provided for the modules."); - return null; + return new Dictionary(); } /***************************************************/ From 293d52a703a7cad56e23d02a9c5fd164d2ac14ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isak=20N=C3=A4slund?= Date: Tue, 13 Oct 2026 13:53:41 +0200 Subject: [PATCH 07/25] Small fix for the ConstructionEMssions test - making sure it implements the NUnitTest and adding ClearCurrentEvents --- .../ConstructionEmssionsTests.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/ConstructionEmssionsTests.cs b/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/ConstructionEmssionsTests.cs index 2225ffc..22f8904 100644 --- a/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/ConstructionEmssionsTests.cs +++ b/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/ConstructionEmssionsTests.cs @@ -20,9 +20,10 @@ * along with this code. If not, see . */ +using BH.Engine.LifeCycleAssessment; using BH.oM.LifeCycleAssessment; using BH.oM.LifeCycleAssessment.MaterialFragments.Construction; -using BH.Engine.LifeCycleAssessment; +using BH.oM.Test.NUnit; using NUnit.Framework; using System; using System.Collections.Generic; @@ -32,13 +33,14 @@ namespace BH.Tests.Engine.LifeCycleAssessment { - public class ConstructionEmssionsTests + public class ConstructionEmssionsTests : NUnitTest { [TestCaseSource(nameof(AddConstructionWasteEmissionsTestCases))] [Description("Tests the AddConstructionWasteEmissions extension method by validating that construction waste emissions (A5_3 module) are correctly calculated based on waste rates and reuse scenarios. Tests both normal cases and edge cases where required modules are missing.")] public void TestAddConstructionWasteEmissions(Dictionary resultingValues, ConstructionWasteEmissions constructionEmissions, double assumedA5_3Value) - { + { + BH.Engine.Base.Compute.ClearCurrentEvents(); //Make sure events raised by previous tests can not satisfy the assertions below resultingValues.AddConstructionWasteEmissions(constructionEmissions, MetricType.ClimateChangeTotal); if(assumedA5_3Value == 0) { From 5734d68991ff256cd1052f830902df4d6e03e7ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isak=20N=C3=A4slund?= Date: Tue, 13 Oct 2026 13:54:45 +0200 Subject: [PATCH 08/25] Add proper tests for CombinationModules --- .../CombinationModulesTests.cs | 99 +++++++++++++++++-- 1 file changed, 92 insertions(+), 7 deletions(-) diff --git a/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/CombinationModulesTests.cs b/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/CombinationModulesTests.cs index 868f97a..4c53b23 100644 --- a/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/CombinationModulesTests.cs +++ b/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/CombinationModulesTests.cs @@ -33,7 +33,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; using System.Text; using System.Threading.Tasks; @@ -45,16 +44,102 @@ public class CombinationModulesTests : NUnitTest /**** Public Methods ****/ /***************************************************/ + [TestCase(Module.A1, new[] { Module.A1toA3 })] + [TestCase(Module.A3, new[] { Module.A1toA3 })] + [TestCase(Module.A5_1, new[] { Module.A5 })] + [TestCase(Module.A5_3, new[] { Module.A5 })] + [TestCase(Module.B1_1, new[] { Module.B1, Module.B1toB3, Module.B1toB5, Module.B1toB7 })] + [TestCase(Module.B1, new[] { Module.B1toB3, Module.B1toB5, Module.B1toB7 })] + [TestCase(Module.B2, new[] { Module.B1toB3, Module.B1toB5, Module.B1toB7 })] + [TestCase(Module.B4_2, new[] { Module.B4, Module.B4toB5, Module.B1toB5, Module.B1toB7 })] + [TestCase(Module.B5, new[] { Module.B4toB5, Module.B1toB5, Module.B1toB7 })] + [TestCase(Module.B1toB3, new[] { Module.B1toB5, Module.B1toB7 })] + [TestCase(Module.B1toB5, new[] { Module.B1toB7 })] + [TestCase(Module.B6, new[] { Module.B1toB7 })] + [TestCase(Module.B7_3, new[] { Module.B7, Module.B1toB7 })] + [TestCase(Module.B8_2, new[] { Module.B8 })] + [TestCase(Module.C1, new[] { Module.C1toC4 })] + [TestCase(Module.C2, new[] { Module.C1toC4 })] + [TestCase(Module.C3, new[] { Module.C3toC4, Module.C1toC4 })] + [TestCase(Module.C4, new[] { Module.C3toC4, Module.C1toC4 })] + [TestCase(Module.C3toC4, new[] { Module.C1toC4 })] + [TestCase(Module.D_1, new[] { Module.D })] + [Description("Tests that PartOfCombinationModules returns every combination a module is part of, including combinations that contain it only indirectly, e.g. B1_1 is part of B1, B1toB3, B1toB5 and B1toB7. Expected values are derived by hand from the module hierarchy.")] + public void PartOfCombinationModulesIncludesAllParents(Module part, Module[] expectedCombinations) + { + var partOfCombinations = Query.PartOfCombinationModules(); + + partOfCombinations.Should().ContainKey(part); + partOfCombinations[part].Should().BeEquivalentTo(expectedCombinations); + } + + /***************************************************/ + + [TestCase(Module.A4)] + [TestCase(Module.A1toA3)] + [TestCase(Module.A5)] + [TestCase(Module.B1toB7)] + [TestCase(Module.B8)] + [TestCase(Module.C1toC4)] + [TestCase(Module.D)] + [Description("Tests that modules that are not part of any combination, either stand-alone modules or the top level combinations, are not included in the PartOfCombinationModules result.")] + public void PartOfCombinationModulesExcludesModulesWithoutParents(Module module) + { + Query.PartOfCombinationModules().Should().NotContainKey(module); + } + + /***************************************************/ + [Test] - [Description("Tests the PartOfCombinationModules query method by displaying all parts of combination modules. This is a visual inspection test with no assertions to verify the method returns the expected module combinations.")] - public void TestCombinationModuleParts() + [Description("Tests that PartOfCombinationModules is consistent with CombinationModules: for every combination, each of its parts must list that combination as a parent, and the parents of the combination itself must also be parents of the part.")] + public void PartOfCombinationModulesConsistentWithCombinationModules() { - var partOFCombinationMOdules = Query.PartOfCombinationModules(); - foreach (var item in partOFCombinationMOdules) + var combinations = Query.CombinationModules(); + var partOfCombinations = Query.PartOfCombinationModules(); + + Assert.Multiple(() => + { + foreach (var combination in combinations) + { + foreach (var part in combination.Value.Select(x => x.Item1)) + { + Assert.That(partOfCombinations.ContainsKey(part), Is.True, $"{part} should be part of {combination.Key}"); + if (!partOfCombinations.ContainsKey(part)) + continue; + + Assert.That(partOfCombinations[part], Does.Contain(combination.Key), $"{part} should list {combination.Key} as a parent"); + + if (partOfCombinations.TryGetValue(combination.Key, out var grandParents)) + Assert.That(partOfCombinations[part], Is.SupersetOf(grandParents), $"{part} should also list the parents of {combination.Key}"); + } + } + }); + } + + /***************************************************/ + + [Test] + [Description("Tests that the module hierarchy is free of cycles: no module is a part of itself, directly or through other combinations, and no combination lists duplicate parts.")] + public void CombinationModulesHaveNoCyclesOrDuplicates() + { + var combinations = Query.CombinationModules(); + var partOfCombinations = Query.PartOfCombinationModules(); + + Assert.Multiple(() => { - Console.WriteLine($"{item.Key}: {string.Join(", ", item.Value)}"); - } + foreach (var combination in combinations) + { + List parts = combination.Value.Select(x => x.Item1).ToList(); + Assert.That(parts, Is.Unique, $"{combination.Key} has duplicate parts"); + Assert.That(parts, Does.Not.Contain(combination.Key), $"{combination.Key} is listed as a part of itself"); + } + foreach (var item in partOfCombinations) + { + Assert.That(item.Value, Does.Not.Contain(item.Key), $"{item.Key} is indirectly a part of itself"); + Assert.That(item.Value, Is.Unique, $"{item.Key} has duplicate parents"); + } + }); } /***************************************************/ From 09cc1d419751114c42d8047cf882f9f04e38158f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isak=20N=C3=A4slund?= Date: Tue, 13 Oct 2026 13:55:32 +0200 Subject: [PATCH 09/25] Uptick PreviousVersion attributes --- LifeCycleAssessment_Engine/Query/EnvironmentalResults.cs | 6 +++--- LifeCycleAssessment_Engine/Query/ResultingModuleValues.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/LifeCycleAssessment_Engine/Query/EnvironmentalResults.cs b/LifeCycleAssessment_Engine/Query/EnvironmentalResults.cs index caaf064..9b1459f 100644 --- a/LifeCycleAssessment_Engine/Query/EnvironmentalResults.cs +++ b/LifeCycleAssessment_Engine/Query/EnvironmentalResults.cs @@ -187,7 +187,7 @@ public static List EnvironmentalResults(this TakeoffItem takeoff /***************************************************/ - [PreviousVersion("9.3", "BH.Engine.LifeCycleAssessment.Query.EnvironmentalResults(BH.oM.LifeCycleAssessment.MaterialFragments.EnvironmentalProductDeclaration, System.Double, System.String, System.Collections.Generic.List, BH.oM.LifeCycleAssessment.Configs.IEvaluationConfig, System.Object)")] + [PreviousVersion("10.0", "BH.Engine.LifeCycleAssessment.Query.EnvironmentalResults(BH.oM.LifeCycleAssessment.MaterialFragments.EnvironmentalProductDeclaration, System.Double, System.String, System.Collections.Generic.List, BH.oM.LifeCycleAssessment.Configs.IEvaluationConfig, System.Object)")] [Description("Evaluates all or selected metrics stored on the EnvironmentalProductDeclaration (EPD) and returns a result per metric.\n" + "Each metric is evaluated by multiplying the values for each module by the provided quantityValue.\n" + "Please be mindful that the unit of the quantityValue should match the QuantityType on the EnvironmentalProductDeclaration.")] @@ -219,7 +219,7 @@ public static List EnvironmentalResults(this EnvironmentalProduc /***************************************************/ - [PreviousVersion("9.3", "BH.Engine.LifeCycleAssessment.Query.EnvironmentalResults(BH.oM.LifeCycleAssessment.MaterialFragments.CombinedLifeCycleAssessmentFactors, System.Double, System.Double, System.String, System.Collections.Generic.List, BH.oM.LifeCycleAssessment.Configs.IEvaluationConfig, System.Object)")] + [PreviousVersion("10.0", "BH.Engine.LifeCycleAssessment.Query.EnvironmentalResults(BH.oM.LifeCycleAssessment.MaterialFragments.CombinedLifeCycleAssessmentFactors, System.Double, System.Double, System.String, System.Collections.Generic.List, BH.oM.LifeCycleAssessment.Configs.IEvaluationConfig, System.Object)")] [Description("Evaluates all or selected metrics stored on the EnvironmentalProductDeclaration (EPD) and returns a result per metric.\n" + "Each metric is evaluated by multiplying the values for each module by the provided quantityValue.\n" + "Please be mindful that the unit of the quantityValue should match the QuantityType on the EnvironmentalProductDeclaration.")] @@ -308,7 +308,7 @@ public static List EnvironmentalResults(this CombinedLifeCycleAs /***************************************************/ - [PreviousVersion("9.3", "BH.Engine.LifeCycleAssessment.Query.EnvironmentalResults(BH.oM.LifeCycleAssessment.MaterialFragments.IEnvironmentalMetric, System.String, System.String, System.Double, BH.oM.LifeCycleAssessment.Configs.IEvaluationConfig, System.Object)")] + [PreviousVersion("10.0", "BH.Engine.LifeCycleAssessment.Query.EnvironmentalResults(BH.oM.LifeCycleAssessment.MaterialFragments.IEnvironmentalMetric, System.String, System.String, System.Double, BH.oM.LifeCycleAssessment.Configs.IEvaluationConfig, System.Object)")] [Description("Evaluates the EnvironmentalMetric and returns a MaterialResult of a type corresponding to the metric. The evaluation is done by multiplying all module data on the metric by the provided quantityValue.\n" + "Please be mindful that the unit of the quantityValue should match the QuantityType on the EnvironmentalProductDeclaration to which the metric belongs.")] [Input("metric", "The EnvironmentalMetric to evaluate. Returned result will be a MaterialResult of a type corresponding to the metric.")] diff --git a/LifeCycleAssessment_Engine/Query/ResultingModuleValues.cs b/LifeCycleAssessment_Engine/Query/ResultingModuleValues.cs index be6300e..b599a98 100644 --- a/LifeCycleAssessment_Engine/Query/ResultingModuleValues.cs +++ b/LifeCycleAssessment_Engine/Query/ResultingModuleValues.cs @@ -40,7 +40,7 @@ public static partial class Query /**** Public Methods ****/ /***************************************************/ - [PreviousVersion("9.3", "BH.Engine.LifeCycleAssessment.Query.IResultingModuleValues(BH.oM.LifeCycleAssessment.MaterialFragments.IEnvironmentalMetric, System.Double, BH.oM.LifeCycleAssessment.Configs.IEvaluationConfig, System.Collections.Generic.Dictionary, System.Object)")] + [PreviousVersion("10.0", "BH.Engine.LifeCycleAssessment.Query.IResultingModuleValues(BH.oM.LifeCycleAssessment.MaterialFragments.IEnvironmentalMetric, System.Double, BH.oM.LifeCycleAssessment.Configs.IEvaluationConfig, System.Collections.Generic.Dictionary, System.Object)")] [Description("Gets the resulting values for each module of the provided EnvironmentalMetric given the provided quantityValue.\n" + "The resulting values are computed based on provided config, defaulting to the values on the metric for each module multiplied by the quantity value.\n" + "Please be mindful that the unit of the quantityValue should match the QuantityType on the EnvironmentalProductDeclaration to which the metric belongs.")] From 0a60f22700ad5604c2c00c086d2c306ce470a3ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isak=20N=C3=A4slund?= Date: Tue, 13 Oct 2026 14:13:34 +0200 Subject: [PATCH 10/25] Fix incorrect comment --- LifeCycleAssessment_Engine/Query/CombinationModules.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/LifeCycleAssessment_Engine/Query/CombinationModules.cs b/LifeCycleAssessment_Engine/Query/CombinationModules.cs index 3e0a619..0181980 100644 --- a/LifeCycleAssessment_Engine/Query/CombinationModules.cs +++ b/LifeCycleAssessment_Engine/Query/CombinationModules.cs @@ -48,7 +48,8 @@ public static partial class Query if (m_CombinationModules == null) { // Define the combinations of modules and their parts. - // The bool indicates whether the module is a combination or not, i.e. true for combinations and false for parts. + // The bool indicates whether the part is required for the combination to be computed, i.e. true for required parts and false for optional parts. + // Optional parts are added to the combination if present, but their absence does not prevent the combination from being computed. // The order of the parts is important, as it defines the order in which they are combined and caluculated. Parts lower down the lift can be made up of parts from above in the list Dictionary> modules = new Dictionary>(); modules[Module.A1toA3] = new List<(Module, bool)>() { (Module.A1, true), (Module.A2,true), (Module.A3, true) }; From e1dc8b51061dc634ed38dba2446db2b07d9c50e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isak=20N=C3=A4slund?= Date: Tue, 13 Oct 2026 14:14:43 +0200 Subject: [PATCH 11/25] Add test method for MaterialResults --- .../MaterialResultTests.cs | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 .ci/unit-tests/LifeCycleAssessment_Engine_Tests/MaterialResultTests.cs diff --git a/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/MaterialResultTests.cs b/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/MaterialResultTests.cs new file mode 100644 index 0000000..a95c596 --- /dev/null +++ b/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/MaterialResultTests.cs @@ -0,0 +1,165 @@ +/* + * This file is part of the Buildings and Habitats object Model (BHoM) + * Copyright (c) 2015 - 2026, the respective contributors. All rights reserved. + * + * Each contributor holds copyright over their respective contributions. + * The project versioning (Git) records all such contribution source information. + * + * + * The BHoM is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, or + * (at your option) any later version. + * + * The BHoM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this code. If not, see . + */ + +using BH.Engine.LifeCycleAssessment; +using BH.oM.LifeCycleAssessment; +using BH.oM.LifeCycleAssessment.Results; +using BH.oM.Test.NUnit; +using FluentAssertions; +using NUnit.Framework; +using System.Collections.Generic; +using System.Linq; + +namespace BH.Tests.Engine.LifeCycleAssessment +{ + public class MaterialResultTests : NUnitTest + { + /***************************************************/ + /**** Public Methods ****/ + /***************************************************/ + + [TestCaseSource(nameof(CombinationCases))] + [Description("Tests that Create.MaterialResult adds combination modules as the sum of their parts, but only when all parts flagged as required in CombinationModules are present and at least one part exists. Combinations already provided must be left untouched and must not cause other combinations to be added, and the dictionary provided must not be modified.")] + public void MaterialResultAddsCombinationModules(Dictionary indicators, Dictionary expected) + { + Dictionary inputCopy = new Dictionary(indicators); + + MaterialResult result = Create.MaterialResult("Material", "EPD", MetricType.ClimateChangeTotal, indicators); + + result.Should().NotBeNull(); + result.Indicators.Should().BeEquivalentTo(expected); + indicators.Should().BeEquivalentTo(inputCopy, "the input should not be modified"); + } + + /***************************************************/ + + [Test] + [Description("Tests that Create.MaterialResult stores the names and returns a result of the type matching the provided MetricType.")] + public void MaterialResultIdentifiers() + { + MaterialResult result = Create.MaterialResult("Material", "EPD", MetricType.ClimateChangeTotal, new Dictionary { { Module.A1, 1 } }); + + result.MaterialName.Should().Be("Material"); + result.EnvironmentalProductDeclarationName.Should().Be("EPD"); + result.IMetricType().Should().Be(MetricType.ClimateChangeTotal); + } + + /***************************************************/ + /**** Private Methods ****/ + /***************************************************/ + + private static IEnumerable CombinationCases() + { + // A1toA3: all parts required + yield return Case("A1toA3 added from all parts", + new() { { Module.A1, 1 }, { Module.A2, 2 }, { Module.A3, 3 } }, + new() { { Module.A1, 1 }, { Module.A2, 2 }, { Module.A3, 3 }, { Module.A1toA3, 6 } }); + + yield return Case("A1toA3 not added when required part missing", + new() { { Module.A1, 1 }, { Module.A2, 2 } }, + new() { { Module.A1, 1 }, { Module.A2, 2 } }); + + yield return Case("A1toA3 already present is kept", + new() { { Module.A1toA3, 10 }, { Module.A1, 1 }, { Module.A2, 2 }, { Module.A3, 3 } }, + new() { { Module.A1toA3, 10 }, { Module.A1, 1 }, { Module.A2, 2 }, { Module.A3, 3 } }); + + yield return Case("Only A1toA3 provided adds nothing", + new() { { Module.A1toA3, 6 } }, + new() { { Module.A1toA3, 6 } }); + + // A5: A5_2 and A5_3 required, A5_1 and A5_4 optional + yield return Case("A5 added from required parts only", + new() { { Module.A5_2, 2 }, { Module.A5_3, 3 } }, + new() { { Module.A5_2, 2 }, { Module.A5_3, 3 }, { Module.A5, 5 } }); + + yield return Case("A5 added from all parts", + new() { { Module.A5_1, 1 }, { Module.A5_2, 2 }, { Module.A5_3, 3 }, { Module.A5_4, 4 } }, + new() { { Module.A5_1, 1 }, { Module.A5_2, 2 }, { Module.A5_3, 3 }, { Module.A5_4, 4 }, { Module.A5, 10 } }); + + yield return Case("A5 not added when required A5_3 missing", + new() { { Module.A5_1, 1 }, { Module.A5_2, 2 }, { Module.A5_4, 4 } }, + new() { { Module.A5_1, 1 }, { Module.A5_2, 2 }, { Module.A5_4, 4 } }); + + yield return Case("A5 not added when only optional parts present", + new() { { Module.A5_1, 1 }, { Module.A5_4, 4 } }, + new() { { Module.A5_1, 1 }, { Module.A5_4, 4 } }); + + // B8: all parts optional + yield return Case("B8 added from a single optional part", + new() { { Module.B8_2, 2 } }, + new() { { Module.B8_2, 2 }, { Module.B8, 2 } }); + + yield return Case("B8 added from all optional parts", + new() { { Module.B8_1, 1 }, { Module.B8_2, 2 }, { Module.B8_3, 3 } }, + new() { { Module.B8_1, 1 }, { Module.B8_2, 2 }, { Module.B8_3, 3 }, { Module.B8, 6 } }); + + // Nested combinations + yield return Case("B1 and B1toB3 added from sub parts", + new() { { Module.B1_1, 1 }, { Module.B1_2, 2 }, { Module.B2, 3 }, { Module.B3, 4 } }, + new() { { Module.B1_1, 1 }, { Module.B1_2, 2 }, { Module.B2, 3 }, { Module.B3, 4 }, { Module.B1, 3 }, { Module.B1toB3, 10 } }); + + yield return Case("B1toB3 not added when B1_2 missing", + new() { { Module.B1_1, 1 }, { Module.B2, 3 }, { Module.B3, 4 } }, + new() { { Module.B1_1, 1 }, { Module.B2, 3 }, { Module.B3, 4 } }); + + yield return Case("All B modules cascade up to B1toB7", + new() + { + { Module.B1_1, 1 }, { Module.B1_2, 1 }, { Module.B2, 1 }, { Module.B3, 1 }, + { Module.B4_1, 1 }, { Module.B4_2, 1 }, { Module.B5, 1 }, { Module.B6, 1 }, + { Module.B7_1, 1 }, { Module.B7_2, 1 }, { Module.B7_3, 1 } + }, + new() + { + { Module.B1_1, 1 }, { Module.B1_2, 1 }, { Module.B2, 1 }, { Module.B3, 1 }, + { Module.B4_1, 1 }, { Module.B4_2, 1 }, { Module.B5, 1 }, { Module.B6, 1 }, + { Module.B7_1, 1 }, { Module.B7_2, 1 }, { Module.B7_3, 1 }, + { Module.B1, 2 }, { Module.B1toB3, 4 }, { Module.B4, 2 }, { Module.B4toB5, 3 }, { Module.B1toB5, 7 }, { Module.B7, 3 }, { Module.B1toB7, 11 } + }); + + yield return Case("C1toC4 added from nested parts", + new() { { Module.C1, 1 }, { Module.C2, 2 }, { Module.C3, 3 }, { Module.C4, 4 } }, + new() { { Module.C1, 1 }, { Module.C2, 2 }, { Module.C3, 3 }, { Module.C4, 4 }, { Module.C3toC4, 7 }, { Module.C1toC4, 10 } }); + + yield return Case("C1toC4 added using provided C3toC4", + new() { { Module.C1, 1 }, { Module.C2, 2 }, { Module.C3toC4, 7 } }, + new() { { Module.C1, 1 }, { Module.C2, 2 }, { Module.C3toC4, 7 }, { Module.C1toC4, 10 } }); + + yield return Case("C3toC4 added, but not C1toC4, when C1 and C2 are missing", + new() { { Module.C3, 3 }, { Module.C4, 4 } }, + new() { { Module.C3, 3 }, { Module.C4, 4 }, { Module.C3toC4, 7 } }); + + yield return Case("Empty input gives empty result", + new(), + new()); + } + + /***************************************************/ + + private static TestCaseData Case(string name, Dictionary indicators, Dictionary expected) + { + return new TestCaseData(indicators, expected).SetName($"MaterialResult: {name}"); + } + + /***************************************************/ + } +} From 3e7a20a8e40f440186a4935b34b9268692cddc51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isak=20N=C3=A4slund?= Date: Tue, 13 Oct 2026 14:22:04 +0200 Subject: [PATCH 12/25] Bugfixes for ModuleValue --- LifeCycleAssessment_Engine/Query/ModuleValue.cs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/LifeCycleAssessment_Engine/Query/ModuleValue.cs b/LifeCycleAssessment_Engine/Query/ModuleValue.cs index 0ff47f4..7253453 100644 --- a/LifeCycleAssessment_Engine/Query/ModuleValue.cs +++ b/LifeCycleAssessment_Engine/Query/ModuleValue.cs @@ -105,6 +105,7 @@ private static bool TryGetModuleValue(this IDictionary moduleDat { // If all required sub-parts are needed, check if all are present value = 0; + bool atLeastOnePartPresent = false; foreach (var part in parts) { if (part.Item2) // If the part is required @@ -115,15 +116,24 @@ private static bool TryGetModuleValue(this IDictionary moduleDat return false; // Required part is missing } value += subValue; + atLeastOnePartPresent = true; } else { // If the part is not required, add its value if present if (moduleData.TryGetModuleValue(part.Item1, out double subValue)) + { value += subValue; + atLeastOnePartPresent = true; + } } } - return true; + + // Combinations with only optional parts have no required part to guarantee a value, so make sure at least one part is present + if (!atLeastOnePartPresent) + value = double.NaN; + + return atLeastOnePartPresent; } else { @@ -134,7 +144,7 @@ private static bool TryGetModuleValue(this IDictionary moduleDat foreach (var part in parts) { - // If the part is not required, add its value if present + // Add the value of the part if present. Pass on the setting to make sure nested combinations are also computed from available parts if (moduleData.TryGetModuleValue(part.Item1, out double subValue)) { value += subValue; @@ -142,6 +152,9 @@ private static bool TryGetModuleValue(this IDictionary moduleDat } } + if (!atLeastOnePartPresent) + value = double.NaN; + return atLeastOnePartPresent; } } From 6c4e7a4c3a33d848bfaf4fa20675e9fcee44baa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isak=20N=C3=A4slund?= Date: Tue, 13 Oct 2026 14:22:13 +0200 Subject: [PATCH 13/25] Add tests for ModuleValue method --- .../ModuleValueTests.cs | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 .ci/unit-tests/LifeCycleAssessment_Engine_Tests/ModuleValueTests.cs diff --git a/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/ModuleValueTests.cs b/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/ModuleValueTests.cs new file mode 100644 index 0000000..57c067c --- /dev/null +++ b/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/ModuleValueTests.cs @@ -0,0 +1,133 @@ +/* + * This file is part of the Buildings and Habitats object Model (BHoM) + * Copyright (c) 2015 - 2026, the respective contributors. All rights reserved. + * + * Each contributor holds copyright over their respective contributions. + * The project versioning (Git) records all such contribution source information. + * + * + * The BHoM is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, or + * (at your option) any later version. + * + * The BHoM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this code. If not, see . + */ + +using BH.Engine.LifeCycleAssessment; +using BH.oM.LifeCycleAssessment; +using BH.oM.Test.NUnit; +using NUnit.Framework; +using System.Collections.Generic; + +namespace BH.Tests.Engine.LifeCycleAssessment +{ + public class ModuleValueTests : NUnitTest + { + /***************************************************/ + /**** Public Methods ****/ + /***************************************************/ + + [TestCaseSource(nameof(ModuleValueCases))] + [Description("Tests the ModuleValue extension method for module dictionaries, checking how combination modules are computed from their parts. Parts flagged as required in CombinationModules must be present when requireAllRequiredSubParts is true, while optional parts are only added when present. When requireAllRequiredSubParts is false, any available part is summed. NaN is expected when the value can not be found.")] + public void ModuleValue(Dictionary moduleData, Module module, bool requireAllRequiredSubParts, double expected) + { + double value = moduleData.ModuleValue(module, requireAllRequiredSubParts); + + Assert.That(value, Is.EqualTo(expected).Within(1e-12)); + } + + /***************************************************/ + + [TestCase(true)] + [TestCase(false)] + [Description("Tests that ModuleValue returns NaN, rather than throwing, when the module data is null.")] + public void ModuleValueNullData(bool requireAllRequiredSubParts) + { + Dictionary moduleData = null; + + Assert.That(moduleData.ModuleValue(Module.A1toA3, requireAllRequiredSubParts), Is.NaN); + } + + /***************************************************/ + + [TestCase(true)] + [TestCase(false)] + [Description("Tests that ModuleValue returns NaN for a combination made up of only optional parts when none of the parts are present, instead of returning a value of zero.")] + public void ModuleValueOnlyOptionalPartsNonePresent(bool requireAllRequiredSubParts) + { + Dictionary moduleData = new Dictionary { { Module.A1, 1 } }; + + Assert.That(moduleData.ModuleValue(Module.B8, requireAllRequiredSubParts), Is.NaN); + } + + /***************************************************/ + + [Test] + [Description("Tests that requireAllRequiredSubParts set to false is applied to nested combinations as well, i.e. B1toB3 should sum B1_1, B2 and B3 even though B1_2, and with it a complete B1, is missing.")] + public void ModuleValueNotAllRequiredAppliesToNestedCombinations() + { + Dictionary moduleData = new Dictionary { { Module.B1_1, 1 }, { Module.B2, 3 }, { Module.B3, 4 } }; + + Assert.That(moduleData.ModuleValue(Module.B1toB3, false), Is.EqualTo(8).Within(1e-12)); + } + + /***************************************************/ + /**** Private Methods ****/ + /***************************************************/ + + private static IEnumerable ModuleValueCases() + { + double nan = double.NaN; + + // Explicitly set values + yield return Case("Direct value", new() { { Module.A1, 5 } }, Module.A1, true, 5); + yield return Case("Explicit combination value wins over parts", new() { { Module.A1toA3, 10 }, { Module.A1, 1 }, { Module.A2, 2 }, { Module.A3, 3 } }, Module.A1toA3, true, 10); + yield return Case("Missing non-combination module", new(), Module.A1, true, nan); + yield return Case("Missing non-combination module, not all required", new(), Module.A1, false, nan); + + // A1toA3: all parts required + yield return Case("A1toA3 from all parts", new() { { Module.A1, 1 }, { Module.A2, 2 }, { Module.A3, 3 } }, Module.A1toA3, true, 6); + yield return Case("A1toA3 missing required part", new() { { Module.A1, 1 }, { Module.A2, 2 } }, Module.A1toA3, true, nan); + yield return Case("A1toA3 missing part, not all required", new() { { Module.A1, 1 }, { Module.A2, 2 } }, Module.A1toA3, false, 3); + yield return Case("A1toA3 with no parts", new(), Module.A1toA3, true, nan); + yield return Case("A1toA3 with no parts, not all required", new(), Module.A1toA3, false, nan); + + // A5: A5_2 and A5_3 required, A5_1 and A5_4 optional + yield return Case("A5 from required parts only", new() { { Module.A5_2, 2 }, { Module.A5_3, 3 } }, Module.A5, true, 5); + yield return Case("A5 from all parts", new() { { Module.A5_1, 1 }, { Module.A5_2, 2 }, { Module.A5_3, 3 }, { Module.A5_4, 4 } }, Module.A5, true, 10); + yield return Case("A5 missing required A5_3", new() { { Module.A5_1, 1 }, { Module.A5_2, 2 } }, Module.A5, true, nan); + yield return Case("A5 missing required A5_2", new() { { Module.A5_3, 3 }, { Module.A5_4, 4 } }, Module.A5, true, nan); + yield return Case("A5 with only optional parts", new() { { Module.A5_1, 1 }, { Module.A5_4, 4 } }, Module.A5, true, nan); + yield return Case("A5 missing required A5_3, not all required", new() { { Module.A5_1, 1 }, { Module.A5_2, 2 } }, Module.A5, false, 3); + yield return Case("A5 with only optional parts, not all required", new() { { Module.A5_1, 1 }, { Module.A5_4, 4 } }, Module.A5, false, 5); + + // B8: all parts optional + yield return Case("B8 with one optional part", new() { { Module.B8_1, 2 } }, Module.B8, true, 2); + yield return Case("B8 with all optional parts", new() { { Module.B8_1, 1 }, { Module.B8_2, 2 }, { Module.B8_3, 3 } }, Module.B8, true, 6); + + // Nested combinations + yield return Case("B1toB3 from all nested parts", new() { { Module.B1_1, 1 }, { Module.B1_2, 2 }, { Module.B2, 3 }, { Module.B3, 4 } }, Module.B1toB3, true, 10); + yield return Case("B1toB3 with required nested part missing", new() { { Module.B1_1, 1 }, { Module.B2, 3 }, { Module.B3, 4 } }, Module.B1toB3, true, nan); + yield return Case("B1toB3 using explicit B1", new() { { Module.B1, 3 }, { Module.B2, 3 }, { Module.B3, 4 } }, Module.B1toB3, true, 10); + yield return Case("C1toC4 from nested parts", new() { { Module.C1, 1 }, { Module.C2, 2 }, { Module.C3, 3 }, { Module.C4, 4 } }, Module.C1toC4, true, 10); + yield return Case("C1toC4 using explicit C3toC4", new() { { Module.C1, 1 }, { Module.C2, 2 }, { Module.C3toC4, 7 } }, Module.C1toC4, true, 10); + yield return Case("C1toC4 missing C4", new() { { Module.C1, 1 }, { Module.C2, 2 }, { Module.C3, 3 } }, Module.C1toC4, true, nan); + } + + /***************************************************/ + + private static TestCaseData Case(string name, Dictionary moduleData, Module module, bool requireAllRequiredSubParts, double expected) + { + return new TestCaseData(moduleData, module, requireAllRequiredSubParts, expected).SetName($"ModuleValue: {name}"); + } + + /***************************************************/ + } +} From e49c3512932f6674644da876e77a37782cc38686 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isak=20N=C3=A4slund?= Date: Tue, 13 Oct 2026 14:34:17 +0200 Subject: [PATCH 14/25] Update ModuleValue.cs --- LifeCycleAssessment_Engine/Query/ModuleValue.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/LifeCycleAssessment_Engine/Query/ModuleValue.cs b/LifeCycleAssessment_Engine/Query/ModuleValue.cs index 7253453..615fd15 100644 --- a/LifeCycleAssessment_Engine/Query/ModuleValue.cs +++ b/LifeCycleAssessment_Engine/Query/ModuleValue.cs @@ -110,7 +110,7 @@ private static bool TryGetModuleValue(this IDictionary moduleDat { if (part.Item2) // If the part is required { - if (!moduleData.TryGetModuleValue(part.Item1, out double subValue)) + if (!moduleData.TryGetModuleValue(part.Item1, out double subValue, requireAllRequiredSubParts)) { value = double.NaN; return false; // Required part is missing @@ -121,7 +121,7 @@ private static bool TryGetModuleValue(this IDictionary moduleDat else { // If the part is not required, add its value if present - if (moduleData.TryGetModuleValue(part.Item1, out double subValue)) + if (moduleData.TryGetModuleValue(part.Item1, out double subValue, requireAllRequiredSubParts)) { value += subValue; atLeastOnePartPresent = true; @@ -145,7 +145,7 @@ private static bool TryGetModuleValue(this IDictionary moduleDat { // Add the value of the part if present. Pass on the setting to make sure nested combinations are also computed from available parts - if (moduleData.TryGetModuleValue(part.Item1, out double subValue)) + if (moduleData.TryGetModuleValue(part.Item1, out double subValue, requireAllRequiredSubParts)) { value += subValue; atLeastOnePartPresent = true; From f547fa117e9e4cfa680ee566de7ea560a88c56eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isak=20N=C3=A4slund?= Date: Tue, 13 Oct 2026 15:41:56 +0200 Subject: [PATCH 15/25] change nuget Do not need full ms compatibility - just need system drawing --- .../LifeCycleAssessment_Engine_Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/LifeCycleAssessment_Engine_Tests.csproj b/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/LifeCycleAssessment_Engine_Tests.csproj index ac2d126..77cd861 100644 --- a/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/LifeCycleAssessment_Engine_Tests.csproj +++ b/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/LifeCycleAssessment_Engine_Tests.csproj @@ -18,11 +18,11 @@ - + From 75d57cd6b6f64c1a76886c518b6ff9207e82e109 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isak=20N=C3=A4slund?= Date: Tue, 13 Oct 2026 16:35:02 +0200 Subject: [PATCH 16/25] Update test-names to TestArgsDisplayName Keeps grouping correct, but allows for clear displaynames --- .../LifeCycleAssessment_Engine_Tests/MaterialResultTests.cs | 2 +- .../LifeCycleAssessment_Engine_Tests/ModuleValueTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/MaterialResultTests.cs b/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/MaterialResultTests.cs index a95c596..a935357 100644 --- a/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/MaterialResultTests.cs +++ b/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/MaterialResultTests.cs @@ -157,7 +157,7 @@ private static IEnumerable CombinationCases() private static TestCaseData Case(string name, Dictionary indicators, Dictionary expected) { - return new TestCaseData(indicators, expected).SetName($"MaterialResult: {name}"); + return new TestCaseData(indicators, expected).SetArgDisplayNames($"{name}"); } /***************************************************/ diff --git a/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/ModuleValueTests.cs b/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/ModuleValueTests.cs index 57c067c..62bc925 100644 --- a/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/ModuleValueTests.cs +++ b/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/ModuleValueTests.cs @@ -125,7 +125,7 @@ private static IEnumerable ModuleValueCases() private static TestCaseData Case(string name, Dictionary moduleData, Module module, bool requireAllRequiredSubParts, double expected) { - return new TestCaseData(moduleData, module, requireAllRequiredSubParts, expected).SetName($"ModuleValue: {name}"); + return new TestCaseData(moduleData, module, requireAllRequiredSubParts, expected).SetArgDisplayNames($"{name}"); } /***************************************************/ From 9bfefd233ca1bdd2bd3fca7ed92fa1861820c33d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isak=20N=C3=A4slund?= Date: Tue, 13 Oct 2026 16:47:05 +0200 Subject: [PATCH 17/25] Minor improvement to results display --- .../LifeCycleAssessment_Engine_Tests/DataSource.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/DataSource.cs b/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/DataSource.cs index b2e4797..1ba1d93 100644 --- a/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/DataSource.cs +++ b/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/DataSource.cs @@ -159,7 +159,7 @@ private static WasteAndDisposalFactors DummyDisposalFactors(ref double v, double /***************************************************/ [Description("Returns a dummy takeoff and material template to be used when testing evaluation.")] - public static IEnumerable DummyTakeoffAndTemplates(double initialV, double increace, bool setA5ToWaste) + public static IEnumerable DummyTakeoffAndTemplates(double initialV, double increace, bool setA5ToWaste) { double v = initialV; @@ -171,7 +171,7 @@ public static IEnumerable DummyTakeoffAndTemplates(double initialV, do MaterialTakeoffItems = templates.Select((x, i) => new TakeoffItem { Material = new Material { Name = x.Name, Density = 1 }, Volume = (i + 1) * 42.543, Mass = (i + 1) * 42.543 * x.Density }).ToList(), }; - yield return new object[] { takeoff, templates, true }; + yield return new TestCaseData(new object[] { takeoff, templates, true }).SetArgDisplayNames("Takeoff and templates with EPDs"); List newTemplates = new List(); @@ -180,7 +180,7 @@ public static IEnumerable DummyTakeoffAndTemplates(double initialV, do newTemplates.Add(new Material { Name = templates[i].Name, Density = templates[i].Density, Properties = new List { DummyCombinedFactors(ref v, inc, setA5ToWaste, i, templates[i].Name + "EPD NAME", templates[i].Properties.OfType().FirstOrDefault().QuantityType) } }); } - yield return new object[] { takeoff, newTemplates, false }; + yield return new TestCaseData(new object[] { takeoff, newTemplates, false }).SetArgDisplayNames("Takeoff and templates with CombinedFactors"); } /***************************************************/ From 8bbe7706c078df011384d15d8be1cf2a2116259d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isak=20N=C3=A4slund?= Date: Tue, 13 Oct 2026 17:00:43 +0200 Subject: [PATCH 18/25] Change to single ResultingModuleValues method No need for IDispatch method any more, with COnfigs moved out of this space into its own method --- .../ResultingModuleValues.cs | 2 +- .../Query/EnvironmentalResults.cs | 4 +-- .../Query/ResultingModuleValues.cs | 32 ++++++++----------- 3 files changed, 16 insertions(+), 22 deletions(-) diff --git a/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/ResultingModuleValues.cs b/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/ResultingModuleValues.cs index 4810cbf..ee42f67 100644 --- a/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/ResultingModuleValues.cs +++ b/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/ResultingModuleValues.cs @@ -51,7 +51,7 @@ public class ResultingModuleValues : NUnitTest public void ResultingModuleValuesDoesNotContainCombinationOrParts(IEnvironmentalMetric metric, Dictionary precomputedValues, List expectedModules) { - var resultingValues = Query.IResultingModuleValues(metric, 1, precomputedValues); + var resultingValues = Query.ResultingModuleValues(metric, 1, precomputedValues); Assert.That(resultingValues.Keys, Is.EquivalentTo(expectedModules), "Resulting module values should contain either combination modules or their parts, but not both."); diff --git a/LifeCycleAssessment_Engine/Query/EnvironmentalResults.cs b/LifeCycleAssessment_Engine/Query/EnvironmentalResults.cs index 9b1459f..19eb57e 100644 --- a/LifeCycleAssessment_Engine/Query/EnvironmentalResults.cs +++ b/LifeCycleAssessment_Engine/Query/EnvironmentalResults.cs @@ -263,7 +263,7 @@ public static List EnvironmentalResults(this CombinedLifeCycleAs foreach (IEnvironmentalMetric metric in factorsProvider.EnvironmentalProductDeclaration.FilteredFactors(metricFilter)) { MetricType type = metric.IMetricType(); - Dictionary resultingValues = metric.IResultingModuleValues(quantityValue, precomputedModules); + Dictionary resultingValues = metric.ResultingModuleValues(quantityValue, precomputedModules); //Check if C2 and A4 results are defined explicitly, and override them if they are a4TransportResults.Remove(type); //Remove as used up @@ -331,7 +331,7 @@ public static MaterialResult EnvironmentalResults(this IEnvironmentalMetric metr if (evaluationConfig != null && !double.IsNaN(mass)) precomputedModules = evaluationConfig.IEvaluationConfigData(quantityValue, mass); - IDictionary resultingValues = metric.IResultingModuleValues(quantityValue, precomputedModules); + IDictionary resultingValues = metric.ResultingModuleValues(quantityValue, precomputedModules); return Create.MaterialResult(materialName, epdName, metric.IMetricType(), resultingValues as dynamic); } diff --git a/LifeCycleAssessment_Engine/Query/ResultingModuleValues.cs b/LifeCycleAssessment_Engine/Query/ResultingModuleValues.cs index b599a98..b3fceaf 100644 --- a/LifeCycleAssessment_Engine/Query/ResultingModuleValues.cs +++ b/LifeCycleAssessment_Engine/Query/ResultingModuleValues.cs @@ -48,7 +48,7 @@ public static partial class Query [Input("quantityValue", "The quantity value to evaluate all metrics by. All metric properties will be multiplied by this value. Quantity should correspond to the QuantityType on the EPD.")] [Input("precomputedValues", "Precomputed values for particular modules and metrics where the result values have been computed by other means than taking the quantityvalue times the metric factor. This can be user set, but is also used by the internal system for IEvaluationCOnfigs as well as for pre-computed values for things like transport and waste management on the CombinedLifecycleAssessmentFactors.")] [Output("resultValues", "The resulting values for each module.")] - public static Dictionary IResultingModuleValues(this IEnvironmentalMetric metric, double quantityValue, Dictionary precomputedValues) + public static Dictionary ResultingModuleValues(this IEnvironmentalMetric metric, double quantityValue, Dictionary precomputedValues) { if (metric == null) { @@ -66,28 +66,19 @@ public static Dictionary IResultingModuleValues(this IEnvironmen if (metric.Indicators.Count == 0) return new Dictionary(); - return ResultingModuleValues(metric, quantityValue, precomputedValues); - } - - /***************************************************/ - /**** Private Methods ****/ - /***************************************************/ - - [Description("Default methodology for getting the resulting values for each module of the provided EnvironmentalMetric given the provided quantityValue.\n" + - "The resulting values are computed as the values on the metric for each module multiplied by the quantity value.\n" + - "Please be mindful that the unit of the quantityValue should match the QuantityType on the EnvironmentalProductDeclaration to which the metric belongs.")] - [Input("metric", "The EnvironmentalMetric to get resulting values for. All module values on the metric will be extracted and multiplied by the qunatityValue.")] - [Input("quantityValue", "The quantity value to evaluate all metrics by. All metric properties will be multiplied by this value. Quantity should correspond to the QuantityType on the EPD.")] - [Output("resultValues", "The resulting values for each module.")] - private static Dictionary ResultingModuleValues(this IEnvironmentalMetric metric, double quantityValue, Dictionary precomputedValues) - { + //Compute the resulting values for each module based on the metric and quantity value + //This is the main loop that computes the resulting values for each module based on the metric and quantity value and for most evaluation, this is the critical step Dictionary resultingValues = new Dictionary(); foreach (var moduleData in metric.Indicators) { - resultingValues[moduleData.Key] = moduleData.Value * quantityValue ; //Evaluation value is base module data multiplied by quantity value + resultingValues[moduleData.Key] = moduleData.Value * quantityValue; //Evaluation value is base module data multiplied by quantity value } + //If precomputed values are provided, then they will be used to overwrite the resulting values for the corresponding modules. + //This is critical to ensure that precomputed values are respected and that any combinations that include the precomputed modules are removed from the resulting values as they will be different than the sum of their parts. + //This is, if a precomputed value is provided for for example A5_1, then the resulting values will not include A5, as it will be different than the sum of its parts (A5_1 + A5_2 + A5_3). Similarly, if a precomputed value is provided for A1toA3, then the resulting values will not include A1, A2 or A3, as they will be different than the combination value. + //When called as part of generating MaterialResults, the Combination modules will be computed at the end of the evaluation, so that they are always included in the resulting values, hence do not need to be computed here. if (precomputedValues != null) { var partModules = PartOfCombinationModules(); //Get a dictionary that for each modules lists the combinations it is part of @@ -105,9 +96,9 @@ private static Dictionary ResultingModuleValues(this IEnvironmen resultingValues[precomputed.Key] = val; //Set the precomputed value. It is assumed that the precomputed value is already evaluated for the quantity if (partModules.TryGetValue(precomputed.Key, out var combinations)) { - foreach(var combination in combinations) + foreach (var combination in combinations) { - if(!precompuedValueSet.Contains(combination)) //If not explicitly set + if (!precompuedValueSet.Contains(combination)) //If not explicitly set resultingValues.Remove(combination); //Remove any combination that includes the precomputed module as it will be different than the sum of its parts } } @@ -116,9 +107,12 @@ private static Dictionary ResultingModuleValues(this IEnvironmen } } + return resultingValues; } + /***************************************************/ + /**** Private Methods ****/ /***************************************************/ private static void RemoveCombinationParts(this Dictionary resultingValues, Module module, IReadOnlyDictionary> combinations, HashSet precompuedValueSet) From dc48a6d06bca4b8dc10bb70cdd74d7b7ee836420 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isak=20N=C3=A4slund?= Date: Wed, 14 Oct 2026 10:07:13 +0200 Subject: [PATCH 19/25] Remove not required IDictionary and dynamic casting the IDictionary was a leftover from previous investigations --- LifeCycleAssessment_Engine/Query/EnvironmentalResults.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LifeCycleAssessment_Engine/Query/EnvironmentalResults.cs b/LifeCycleAssessment_Engine/Query/EnvironmentalResults.cs index 19eb57e..a05bc57 100644 --- a/LifeCycleAssessment_Engine/Query/EnvironmentalResults.cs +++ b/LifeCycleAssessment_Engine/Query/EnvironmentalResults.cs @@ -331,9 +331,9 @@ public static MaterialResult EnvironmentalResults(this IEnvironmentalMetric metr if (evaluationConfig != null && !double.IsNaN(mass)) precomputedModules = evaluationConfig.IEvaluationConfigData(quantityValue, mass); - IDictionary resultingValues = metric.ResultingModuleValues(quantityValue, precomputedModules); + Dictionary resultingValues = metric.ResultingModuleValues(quantityValue, precomputedModules); - return Create.MaterialResult(materialName, epdName, metric.IMetricType(), resultingValues as dynamic); + return Create.MaterialResult(materialName, epdName, metric.IMetricType(), resultingValues); } /***************************************************/ From 3f6418c7adc68bb20679bec813021ddfe72e117e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isak=20N=C3=A4slund?= Date: Wed, 14 Oct 2026 10:09:09 +0200 Subject: [PATCH 20/25] Remove unused and incorrect private method --- .../Create/IElementResult/IElementResults.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/LifeCycleAssessment_Engine/Create/IElementResult/IElementResults.cs b/LifeCycleAssessment_Engine/Create/IElementResult/IElementResults.cs index 69dc450..c16dc9a 100644 --- a/LifeCycleAssessment_Engine/Create/IElementResult/IElementResults.cs +++ b/LifeCycleAssessment_Engine/Create/IElementResult/IElementResults.cs @@ -165,16 +165,6 @@ private static System.Reflection.ConstructorInfo GetElementResultConstructorInfo return elementResultType.GetConstructors().OrderByDescending(x => x.GetParameters().Count()).First(); } - /***************************************************/ - - [Description("Gets an ElementResult type matching the metric type by name.")] - private static Type ElementResultTypeFromMetric(Type metricType) - { - string metric = metricType.Name.Replace("Metric", ""); - Type elementResultType = BH.Engine.Base.Query.BHoMTypeList().Where(x => typeof(MaterialResult).IsAssignableFrom(x)).First(x => x.Name == metric + "ElementResult"); - return elementResultType; - } - /***************************************************/ /**** Private Fields ****/ /***************************************************/ From 5ea7ab14757bd743d0c5451c3524149bf32a0957 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isak=20N=C3=A4slund?= Date: Wed, 14 Oct 2026 10:33:52 +0200 Subject: [PATCH 21/25] Correct the description of SumModuleData --- LifeCycleAssessment_Engine/Query/SumModuleDataValues.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/LifeCycleAssessment_Engine/Query/SumModuleDataValues.cs b/LifeCycleAssessment_Engine/Query/SumModuleDataValues.cs index 4963d7e..6ca66fd 100644 --- a/LifeCycleAssessment_Engine/Query/SumModuleDataValues.cs +++ b/LifeCycleAssessment_Engine/Query/SumModuleDataValues.cs @@ -41,10 +41,11 @@ public static partial class Query /**** Public Methods - Interface ****/ /***************************************************/ - [Description("Gets a list of doubles corresponding to the sum of values for each property of the provided ILifeCycleAssessmentPhaseData, i.e. the first value will be the sum of A1 for all the provided ILifeCycleAssessmentPhaseDatas.")] - [Input("results", "List of ILifeCycleAssessmentPhaseData to get the sum data from.")] - [Input("onlyIncludeIfAllAvailable", "If true, only sums up values for a particular module if it is available on all items provided. If false, sum for modules where only part of the data is available is added as well.")] - [Output("values", "The values of the summed up material results as dictionary with the module as keys and resulting summed value for the module as value.")] + [Description("Sums the Indicators of the provided ILifeCycleAssessmentModuleData per Module, i.e. the value stored for module A1 in the returned dictionary will be the sum of the A1 values of all the provided items.\n" + + "All provided items need to be of the same type. If they are not, an error is raised and an empty dictionary is returned. An empty dictionary is also returned if no items are provided.")] + [Input("results", "The ILifeCycleAssessmentModuleData items to sum the indicators of. All items need to be of the same type.")] + [Input("onlyIncludeIfAllAvailable", "If true, a module is only summed and included in the result if it is available on all provided items. If false, modules available on only some of the items are included as well, summed over the items for which they are available.")] + [Output("values", "The summed indicators as a dictionary with the module as key and the sum of the values for that module over all provided items as value.")] public static Dictionary SumModuleDataValues(this IReadOnlyList>> results, bool onlyIncludeIfAllAvailable = true) { if (results.IsNullOrEmpty()) From 760d3461ba486aa577f71271c06d8ee030e739b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isak=20N=C3=A4slund?= Date: Wed, 14 Oct 2026 10:36:41 +0200 Subject: [PATCH 22/25] Update descriptions for the TotalMaterialBreakdown methods --- .../Query/TotalMaterialBreakdown.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/LifeCycleAssessment_Engine/Query/TotalMaterialBreakdown.cs b/LifeCycleAssessment_Engine/Query/TotalMaterialBreakdown.cs index 39cbb41..3d67924 100644 --- a/LifeCycleAssessment_Engine/Query/TotalMaterialBreakdown.cs +++ b/LifeCycleAssessment_Engine/Query/TotalMaterialBreakdown.cs @@ -40,10 +40,10 @@ public static partial class Query /**** Public Methods ****/ /***************************************************/ - [Description("Gets total MaterialResults for all provided element results grouped by MaterialName, EPDName and Metric, and returns a single MaterialResult for each group containing the total evaluated.")] + [Description("Gets the total MaterialResults for all provided element results. The material results of the element results are grouped by MaterialName, EnvironmentalProductDeclarationName and result type (metric), and a single MaterialResult is returned for each group with the indicators summed per module.")] [Input("elementResults", "The element results to extract the material breakdown from.")] - [Input("onlyIncludeIfAllAvailable", "If true, only sums up values for a particular module if it is available on all items provided. If false, sum for modules where only part of the data is available is added as well.")] - [Output("materialResults", "Material results with the total quantity per materal type.")] + [Input("onlyIncludeIfAllAvailable", "If true, a module is only summed and included for a group if it is available on all material results in that group. If false, modules available on only some of the material results in a group are included as well, summed over the results for which they are available.")] + [Output("materialResults", "One MaterialResult per unique combination of material name, EPD name and result type, with the indicators summed over all material results in the group.")] public static List TotalMaterialBreakdown(this IEnumerable> elementResults, bool onlyIncludeIfAllAvailable = true) { if (elementResults == null || !elementResults.Any()) @@ -54,10 +54,10 @@ public static List TotalMaterialBreakdown(this IEnumerable TotalMaterialBreakdown(this IEnumerable materialResults, bool onlyIncludeIfAllAvailable = true) { if (materialResults == null || !materialResults.Any()) @@ -77,10 +77,10 @@ public static List TotalMaterialBreakdown(this IEnumerable MaterialBreakdown(IEnumerable materialResults, bool onlyIncludeIfAllAvailable) { List breakDown = new List(); From f2f4003e4b5b40edaa2c8b6a69c46fc8b4afa610 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isak=20N=C3=A4slund?= Date: Wed, 14 Oct 2026 11:46:20 +0200 Subject: [PATCH 23/25] Add clarifying comment about boolean inputs to TryGetModuleValue for ConstructionWasteEmissions --- .../Query/ConstructionWasteEmissions.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/LifeCycleAssessment_Engine/Query/ConstructionWasteEmissions.cs b/LifeCycleAssessment_Engine/Query/ConstructionWasteEmissions.cs index df40488..bc9c94f 100644 --- a/LifeCycleAssessment_Engine/Query/ConstructionWasteEmissions.cs +++ b/LifeCycleAssessment_Engine/Query/ConstructionWasteEmissions.cs @@ -60,13 +60,14 @@ public static double ConstructionWasteEmissions(this Dictionary List missingModules = new List(); //Get out the cradle to gate metrics - if (resultingValues.TryGetModuleValue(Module.A1toA3, out double a1a3)) + //Called with require all subparts set to true, as need either the A1toA3 or all of A1, A2 and A3 to be present to compute the waste factor + if (resultingValues.TryGetModuleValue(Module.A1toA3, out double a1a3, true)) total += a1a3; else missingModules.Add(Module.A1toA3); //Get out the transport metrics - if(resultingValues.TryGetModuleValue(Module.A4, out double a4)) + if(resultingValues.TryGetModuleValue(Module.A4, out double a4, true)) total += a4; else missingModules.Add(Module.A4); @@ -74,13 +75,15 @@ public static double ConstructionWasteEmissions(this Dictionary //Get out the disposal metrics if (!constructionEmissions.ResuedOnSite) { - if (resultingValues.TryGetModuleValue(Module.C2, out double c2)) + if (resultingValues.TryGetModuleValue(Module.C2, out double c2, true)) total += c2; //Add C2 if not re-used on site else missingModules.Add(Module.C2); } //Get out the disposal metrics + //Called with require all subparts set to false, as only need either C3toC4 or C3 or C4 to be present to compute the waste factor + //This is due to the fact that it can be quite common that you only have factors available for just C3 or just C4, hence picking up just one of them should be seen as sufficient if (resultingValues.TryGetModuleValue(Module.C3toC4, out double c3c4, false)) total += c3c4; else From 55ed8c26c64662e3667bbb0d095cf9087b473636 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isak=20N=C3=A4slund?= Date: Wed, 14 Oct 2026 13:32:11 +0200 Subject: [PATCH 24/25] Improve Test case naming for ConstructionEmission tests --- .../ConstructionEmssionsTests.cs | 81 +++++++++++++++---- 1 file changed, 65 insertions(+), 16 deletions(-) diff --git a/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/ConstructionEmssionsTests.cs b/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/ConstructionEmssionsTests.cs index 22f8904..6af17ba 100644 --- a/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/ConstructionEmssionsTests.cs +++ b/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/ConstructionEmssionsTests.cs @@ -52,27 +52,76 @@ public void TestAddConstructionWasteEmissions(Dictionary resulti Assert.That(resultingValues[Module.A5_3], Is.EqualTo(assumedA5_3Value).Within(1e-9)); } - private static IEnumerable AddConstructionWasteEmissionsTestCases() + private static IEnumerable AddConstructionWasteEmissionsTestCases() { // Normal cases - yield return new object[] { new Dictionary { { Module.A1toA3, 100 }, { Module.A4, 20 }, { Module.C2, 10 }, { Module.C3, 10 } }, new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ResuedOnSite = false }, 140 }; - yield return new object[] { new Dictionary { { Module.A1toA3, 100 }, { Module.A4, 20 }, { Module.C2, 10 }, { Module.C3, 10 } }, new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ResuedOnSite = true }, 130 }; - yield return new object[] { new Dictionary { { Module.A1, 40 }, { Module.A2, 30 }, { Module.A3, 30 }, { Module.A4, 20 }, { Module.C2, 10 }, { Module.C3, 10 } }, new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ResuedOnSite = false }, 140 }; - yield return new object[] { new Dictionary { { Module.A1, 40 }, { Module.A2, 30 }, { Module.A3, 30 }, { Module.A4, 20 }, { Module.C2, 10 }, { Module.C3, 10 } }, new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ResuedOnSite = true }, 130 }; - yield return new object[] { new Dictionary { { Module.A1, 100 }, { Module.A2, 20 }, { Module.A3, 30 }, { Module.A4, 20 }, { Module.C2, 10 }, { Module.C3, 30 } }, new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.1 }, ResuedOnSite = false }, 23.3333333333333 }; - yield return new object[] { new Dictionary { { Module.A1, 40 }, { Module.A2, 30 }, { Module.A3, 30 }, { Module.A4, 20 }, { Module.C2, 10 }, { Module.C4, 10 } }, new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ResuedOnSite = true }, 130 }; - yield return new object[] { new Dictionary { { Module.A1, 40 }, { Module.A2, 30 }, { Module.A3, 30 }, { Module.A4, 20 }, { Module.C2, 10 }, { Module.C3toC4, 10 } }, new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ResuedOnSite = true }, 130 }; - yield return new object[] { new Dictionary { { Module.A1toA3, 100 }, { Module.A4, 20 }, { Module.C3, 10 } }, new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ResuedOnSite = true }, 130 }; + yield return Case("A1toA3 with all required modules, not reused on site, C2 included", + new Dictionary { { Module.A1toA3, 100 }, { Module.A4, 20 }, { Module.C2, 10 }, { Module.C3, 10 } }, + new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ResuedOnSite = false }, 140); + + yield return Case("A1toA3 with all required modules, reused on site, C2 excluded", + new Dictionary { { Module.A1toA3, 100 }, { Module.A4, 20 }, { Module.C2, 10 }, { Module.C3, 10 } }, + new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ResuedOnSite = true }, 130); + + yield return Case("Separate A1, A2 and A3, not reused on site, C2 included", + new Dictionary { { Module.A1, 40 }, { Module.A2, 30 }, { Module.A3, 30 }, { Module.A4, 20 }, { Module.C2, 10 }, { Module.C3, 10 } }, + new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ResuedOnSite = false }, 140); + + yield return Case("Separate A1, A2 and A3, reused on site, C2 excluded", + new Dictionary { { Module.A1, 40 }, { Module.A2, 30 }, { Module.A3, 30 }, { Module.A4, 20 }, { Module.C2, 10 }, { Module.C3, 10 } }, + new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ResuedOnSite = true }, 130); + + yield return Case("Waste rate of 0.1 gives waste factor of 1/9 of the summed modules", + new Dictionary { { Module.A1, 100 }, { Module.A2, 20 }, { Module.A3, 30 }, { Module.A4, 20 }, { Module.C2, 10 }, { Module.C3, 30 } }, + new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.1 }, ResuedOnSite = false }, 23.3333333333333); + + yield return Case("C4 used as disposal module in place of C3", + new Dictionary { { Module.A1, 40 }, { Module.A2, 30 }, { Module.A3, 30 }, { Module.A4, 20 }, { Module.C2, 10 }, { Module.C4, 10 } }, + new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ResuedOnSite = true }, 130); + + yield return Case("C3toC4 used as disposal module in place of C3", + new Dictionary { { Module.A1, 40 }, { Module.A2, 30 }, { Module.A3, 30 }, { Module.A4, 20 }, { Module.C2, 10 }, { Module.C3toC4, 10 } }, + new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ResuedOnSite = true }, 130); + + yield return Case("C2 not required when reused on site", + new Dictionary { { Module.A1toA3, 100 }, { Module.A4, 20 }, { Module.C3, 10 } }, + new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ResuedOnSite = true }, 130); // Missing modules cases - yield return new object[] { new Dictionary { { Module.A4, 20 }, { Module.C2, 10 }, { Module.C3, 10 } }, new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ResuedOnSite = false }, 0 }; - yield return new object[] { new Dictionary { { Module.A1toA3, 100 }, { Module.C2, 10 }, { Module.C3, 10 } }, new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ResuedOnSite = false }, 0 }; - yield return new object[] { new Dictionary { { Module.A1toA3, 100 }, { Module.A4, 20 }, { Module.C3, 10 } }, new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ResuedOnSite = false }, 0 }; - yield return new object[] { new Dictionary { { Module.A1toA3, 100 }, { Module.A4, 20 }, { Module.C2, 10 } }, new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ResuedOnSite = false }, 0 }; - yield return new object[] { new Dictionary { { Module.A2, 20 }, { Module.A3, 30 }, { Module.A4, 20 }, { Module.C2, 10 }, { Module.C3, 30 } }, new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.1 }, ResuedOnSite = false }, 0 }; - yield return new object[] { new Dictionary { { Module.A1, 100 }, { Module.A3, 30 }, { Module.A4, 20 }, { Module.C2, 10 }, { Module.C3, 30 } }, new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.1 }, ResuedOnSite = false }, 0 }; - yield return new object[] { new Dictionary { { Module.A1, 100 }, { Module.A2, 20 }, { Module.A4, 20 }, { Module.C2, 10 }, { Module.C3, 30 } }, new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.1 }, ResuedOnSite = false }, 0 }; + yield return Case("Missing A1toA3 gives error and no A5_3", + new Dictionary { { Module.A4, 20 }, { Module.C2, 10 }, { Module.C3, 10 } }, + new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ResuedOnSite = false }, 0); + + yield return Case("Missing A4 gives error and no A5_3", + new Dictionary { { Module.A1toA3, 100 }, { Module.C2, 10 }, { Module.C3, 10 } }, + new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ResuedOnSite = false }, 0); + yield return Case("Missing C2 when not reused on site gives error and no A5_3", + new Dictionary { { Module.A1toA3, 100 }, { Module.A4, 20 }, { Module.C3, 10 } }, + new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ResuedOnSite = false }, 0); + + yield return Case("Missing disposal (C3, C4 or C3toC4) gives error and no A5_3", + new Dictionary { { Module.A1toA3, 100 }, { Module.A4, 20 }, { Module.C2, 10 } }, + new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ResuedOnSite = false }, 0); + + yield return Case("Missing A1 when A2 and A3 are provided gives error and no A5_3", + new Dictionary { { Module.A2, 20 }, { Module.A3, 30 }, { Module.A4, 20 }, { Module.C2, 10 }, { Module.C3, 30 } }, + new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.1 }, ResuedOnSite = false }, 0); + + yield return Case("Missing A2 when A1 and A3 are provided gives error and no A5_3", + new Dictionary { { Module.A1, 100 }, { Module.A3, 30 }, { Module.A4, 20 }, { Module.C2, 10 }, { Module.C3, 30 } }, + new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.1 }, ResuedOnSite = false }, 0); + + yield return Case("Missing A3 when A1 and A2 are provided gives error and no A5_3", + new Dictionary { { Module.A1, 100 }, { Module.A2, 20 }, { Module.A4, 20 }, { Module.C2, 10 }, { Module.C3, 30 } }, + new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.1 }, ResuedOnSite = false }, 0); + } + + /***************************************************/ + + private static TestCaseData Case(string name, Dictionary resultingValues, ConstructionWasteEmissions constructionEmissions, double assumedA5_3Value) + { + return new TestCaseData(resultingValues, constructionEmissions, assumedA5_3Value).SetArgDisplayNames(name); } } } From a76f311b1c2e1d4ff5bc16ade208d1c9c4b7c4cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isak=20N=C3=A4slund?= Date: Wed, 14 Oct 2026 13:38:06 +0200 Subject: [PATCH 25/25] Align with name change in oM --- .../ConstructionEmssionsTests.cs | 30 +++++++++---------- .../DataSource.cs | 2 +- .../Evaluate.cs | 2 +- .../Query/ConstructionWasteEmissions.cs | 2 +- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/ConstructionEmssionsTests.cs b/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/ConstructionEmssionsTests.cs index 6af17ba..5481905 100644 --- a/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/ConstructionEmssionsTests.cs +++ b/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/ConstructionEmssionsTests.cs @@ -57,64 +57,64 @@ private static IEnumerable AddConstructionWasteEmissionsTestCases( // Normal cases yield return Case("A1toA3 with all required modules, not reused on site, C2 included", new Dictionary { { Module.A1toA3, 100 }, { Module.A4, 20 }, { Module.C2, 10 }, { Module.C3, 10 } }, - new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ResuedOnSite = false }, 140); + new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ReusedOnSite = false }, 140); yield return Case("A1toA3 with all required modules, reused on site, C2 excluded", new Dictionary { { Module.A1toA3, 100 }, { Module.A4, 20 }, { Module.C2, 10 }, { Module.C3, 10 } }, - new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ResuedOnSite = true }, 130); + new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ReusedOnSite = true }, 130); yield return Case("Separate A1, A2 and A3, not reused on site, C2 included", new Dictionary { { Module.A1, 40 }, { Module.A2, 30 }, { Module.A3, 30 }, { Module.A4, 20 }, { Module.C2, 10 }, { Module.C3, 10 } }, - new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ResuedOnSite = false }, 140); + new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ReusedOnSite = false }, 140); yield return Case("Separate A1, A2 and A3, reused on site, C2 excluded", new Dictionary { { Module.A1, 40 }, { Module.A2, 30 }, { Module.A3, 30 }, { Module.A4, 20 }, { Module.C2, 10 }, { Module.C3, 10 } }, - new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ResuedOnSite = true }, 130); + new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ReusedOnSite = true }, 130); yield return Case("Waste rate of 0.1 gives waste factor of 1/9 of the summed modules", new Dictionary { { Module.A1, 100 }, { Module.A2, 20 }, { Module.A3, 30 }, { Module.A4, 20 }, { Module.C2, 10 }, { Module.C3, 30 } }, - new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.1 }, ResuedOnSite = false }, 23.3333333333333); + new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.1 }, ReusedOnSite = false }, 23.3333333333333); yield return Case("C4 used as disposal module in place of C3", new Dictionary { { Module.A1, 40 }, { Module.A2, 30 }, { Module.A3, 30 }, { Module.A4, 20 }, { Module.C2, 10 }, { Module.C4, 10 } }, - new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ResuedOnSite = true }, 130); + new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ReusedOnSite = true }, 130); yield return Case("C3toC4 used as disposal module in place of C3", new Dictionary { { Module.A1, 40 }, { Module.A2, 30 }, { Module.A3, 30 }, { Module.A4, 20 }, { Module.C2, 10 }, { Module.C3toC4, 10 } }, - new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ResuedOnSite = true }, 130); + new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ReusedOnSite = true }, 130); yield return Case("C2 not required when reused on site", new Dictionary { { Module.A1toA3, 100 }, { Module.A4, 20 }, { Module.C3, 10 } }, - new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ResuedOnSite = true }, 130); + new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ReusedOnSite = true }, 130); // Missing modules cases yield return Case("Missing A1toA3 gives error and no A5_3", new Dictionary { { Module.A4, 20 }, { Module.C2, 10 }, { Module.C3, 10 } }, - new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ResuedOnSite = false }, 0); + new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ReusedOnSite = false }, 0); yield return Case("Missing A4 gives error and no A5_3", new Dictionary { { Module.A1toA3, 100 }, { Module.C2, 10 }, { Module.C3, 10 } }, - new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ResuedOnSite = false }, 0); + new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ReusedOnSite = false }, 0); yield return Case("Missing C2 when not reused on site gives error and no A5_3", new Dictionary { { Module.A1toA3, 100 }, { Module.A4, 20 }, { Module.C3, 10 } }, - new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ResuedOnSite = false }, 0); + new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ReusedOnSite = false }, 0); yield return Case("Missing disposal (C3, C4 or C3toC4) gives error and no A5_3", new Dictionary { { Module.A1toA3, 100 }, { Module.A4, 20 }, { Module.C2, 10 } }, - new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ResuedOnSite = false }, 0); + new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.5 }, ReusedOnSite = false }, 0); yield return Case("Missing A1 when A2 and A3 are provided gives error and no A5_3", new Dictionary { { Module.A2, 20 }, { Module.A3, 30 }, { Module.A4, 20 }, { Module.C2, 10 }, { Module.C3, 30 } }, - new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.1 }, ResuedOnSite = false }, 0); + new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.1 }, ReusedOnSite = false }, 0); yield return Case("Missing A2 when A1 and A3 are provided gives error and no A5_3", new Dictionary { { Module.A1, 100 }, { Module.A3, 30 }, { Module.A4, 20 }, { Module.C2, 10 }, { Module.C3, 30 } }, - new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.1 }, ResuedOnSite = false }, 0); + new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.1 }, ReusedOnSite = false }, 0); yield return Case("Missing A3 when A1 and A2 are provided gives error and no A5_3", new Dictionary { { Module.A1, 100 }, { Module.A2, 20 }, { Module.A4, 20 }, { Module.C2, 10 }, { Module.C3, 30 } }, - new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.1 }, ResuedOnSite = false }, 0); + new ConstructionWasteEmissions { WasteRate = new WasteRate { Rate = 0.1 }, ReusedOnSite = false }, 0); } /***************************************************/ diff --git a/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/DataSource.cs b/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/DataSource.cs index 1ba1d93..e7bfbf6 100644 --- a/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/DataSource.cs +++ b/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/DataSource.cs @@ -268,7 +268,7 @@ private static ConstructionWasteEmissions DummyConstructionsFactor(double v, dou { return new ConstructionWasteEmissions { - ResuedOnSite = ((int)Math.Round(v)) % 2 == 0, + ReusedOnSite = ((int)Math.Round(v)) % 2 == 0, WasteRate = new WasteRate { Rate = 0.5 }, }; } diff --git a/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/Evaluate.cs b/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/Evaluate.cs index 8443676..94cdb15 100644 --- a/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/Evaluate.cs +++ b/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/Evaluate.cs @@ -393,7 +393,7 @@ public static double WasteImpact(ConstructionWasteEmissions constructionEmission else return 0; - if (!constructionEmissions.ResuedOnSite) + if (!constructionEmissions.ReusedOnSite) { if (result.Indicators.ContainsKey(Module.C2)) expected += result.Indicators[Module.C2]; diff --git a/LifeCycleAssessment_Engine/Query/ConstructionWasteEmissions.cs b/LifeCycleAssessment_Engine/Query/ConstructionWasteEmissions.cs index bc9c94f..6a54f56 100644 --- a/LifeCycleAssessment_Engine/Query/ConstructionWasteEmissions.cs +++ b/LifeCycleAssessment_Engine/Query/ConstructionWasteEmissions.cs @@ -73,7 +73,7 @@ public static double ConstructionWasteEmissions(this Dictionary missingModules.Add(Module.A4); //Get out the disposal metrics - if (!constructionEmissions.ResuedOnSite) + if (!constructionEmissions.ReusedOnSite) { if (resultingValues.TryGetModuleValue(Module.C2, out double c2, true)) total += c2; //Add C2 if not re-used on site