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"); + } + }); } /***************************************************/ diff --git a/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/ConstructionEmssionsTests.cs b/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/ConstructionEmssionsTests.cs index 2225ffc..5481905 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) { @@ -50,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 }, 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 }, 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 }, 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 }, 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 }, 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 }, 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 }, 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 }, ReusedOnSite = 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 }, 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 }, 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 }, 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 }, 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 }, 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 }, 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 }, ReusedOnSite = false }, 0); + } + + /***************************************************/ + + private static TestCaseData Case(string name, Dictionary resultingValues, ConstructionWasteEmissions constructionEmissions, double assumedA5_3Value) + { + return new TestCaseData(resultingValues, constructionEmissions, assumedA5_3Value).SetArgDisplayNames(name); } } } diff --git a/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/DataSource.cs b/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/DataSource.cs index b2e4797..e7bfbf6 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"); } /***************************************************/ @@ -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/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/LifeCycleAssessment_Engine_Tests.csproj b/.ci/unit-tests/LifeCycleAssessment_Engine_Tests/LifeCycleAssessment_Engine_Tests.csproj index ac2d388..77cd861 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 @@ -22,6 +22,7 @@ + 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..a935357 --- /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).SetArgDisplayNames($"{name}"); + } + + /***************************************************/ + } +} 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..62bc925 --- /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).SetArgDisplayNames($"{name}"); + } + + /***************************************************/ + } +} 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..ee42f67 --- /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 - 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 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.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."); + + + } + + /***************************************************/ + + 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 } + }; + } + + + } +} + + 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 ****/ /***************************************************/ 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/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) }; diff --git a/LifeCycleAssessment_Engine/Query/ConstructionWasteEmissions.cs b/LifeCycleAssessment_Engine/Query/ConstructionWasteEmissions.cs new file mode 100644 index 0000000..6a54f56 --- /dev/null +++ b/LifeCycleAssessment_Engine/Query/ConstructionWasteEmissions.cs @@ -0,0 +1,109 @@ +/* + * 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 + //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, true)) + total += a4; + else + missingModules.Add(Module.A4); + + //Get out the disposal metrics + if (!constructionEmissions.ReusedOnSite) + { + 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 + 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(); + + } + + /***************************************************/ + + } +} + + + diff --git a/LifeCycleAssessment_Engine/Query/EnvironmentalResults.cs b/LifeCycleAssessment_Engine/Query/EnvironmentalResults.cs index eef9cd9..a05bc57 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("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.")] @@ -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("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.")] [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.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 @@ -302,6 +308,7 @@ public static List EnvironmentalResults(this CombinedLifeCycleAs /***************************************************/ + [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.")] @@ -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,9 +326,14 @@ 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); + + Dictionary resultingValues = metric.ResultingModuleValues(quantityValue, precomputedModules); - return Create.MaterialResult(materialName, epdName, metric.IMetricType(), resultingValues as dynamic); + return Create.MaterialResult(materialName, epdName, metric.IMetricType(), resultingValues); } /***************************************************/ @@ -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..29129e5 --- /dev/null +++ b/LifeCycleAssessment_Engine/Query/EvaluationConfigData.cs @@ -0,0 +1,167 @@ +/* + * 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 ****/ + /***************************************************/ + + [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 new Dictionary(); + } + + /***************************************************/ + } +} + + + diff --git a/LifeCycleAssessment_Engine/Query/ModuleValue.cs b/LifeCycleAssessment_Engine/Query/ModuleValue.cs new file mode 100644 index 0000000..615fd15 --- /dev/null +++ b/LifeCycleAssessment_Engine/Query/ModuleValue.cs @@ -0,0 +1,171 @@ +/* + * 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; + bool atLeastOnePartPresent = false; + foreach (var part in parts) + { + if (part.Item2) // If the part is required + { + if (!moduleData.TryGetModuleValue(part.Item1, out double subValue, requireAllRequiredSubParts)) + { + value = double.NaN; + 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, requireAllRequiredSubParts)) + { + value += subValue; + atLeastOnePartPresent = 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 + { + // 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) + { + + // 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, requireAllRequiredSubParts)) + { + value += subValue; + atLeastOnePartPresent = true; + } + } + + if (!atLeastOnePartPresent) + value = double.NaN; + + return atLeastOnePartPresent; + } + } + + value = double.NaN; + return false; + } + + /***************************************************/ + } +} + + + diff --git a/LifeCycleAssessment_Engine/Query/ResultingModuleValues.cs b/LifeCycleAssessment_Engine/Query/ResultingModuleValues.cs index 1dda5a2..b3fceaf 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("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.")] [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 ResultingModuleValues(this IEnvironmentalMetric metric, double quantityValue, Dictionary precomputedValues) { if (metric == null) { @@ -67,31 +67,18 @@ public static Dictionary IResultingModuleValues(this IEnvironmen return new Dictionary(); - - 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); - } - - /***************************************************/ - /**** 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 @@ -109,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 } } @@ -120,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) @@ -140,162 +130,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); - } - - /***************************************************/ } } 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()) 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(); 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; + } + + /***************************************************/ + } +} + + +