From ea87381bae75d18d227d4f858b495ca31eb5e834 Mon Sep 17 00:00:00 2001 From: LFLaverty Date: Mon, 22 Apr 2024 15:10:24 +0100 Subject: [PATCH] adds support & unit tests for multiple fuel types --- .../CalculatorTests.cs | 164 +++++++++++++++++- .../Models/CalculationResult.cs | 37 +++- .../ShipCarbonIntensityCalculator.cs | 90 +++++++++- .../Program.cs | 58 +++++++ README.md | 13 +- 5 files changed, 348 insertions(+), 14 deletions(-) diff --git a/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.Core.Tests/CalculatorTests.cs b/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.Core.Tests/CalculatorTests.cs index ae0666b..825bdfc 100644 --- a/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.Core.Tests/CalculatorTests.cs +++ b/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.Core.Tests/CalculatorTests.cs @@ -16,15 +16,15 @@ public class CalculatorTests /// Tests a known and pre-calculated set of inputs for a RoRoPassengerShip /// [TestMethod] - public void TestCalculator() + public void TestCalculatorSingleFuel() { var _calc = new ShipCarbonIntensityCalculator(); var result = _calc.CalculateAttainedCiiRating( - ShipType.RoRoPassengerShip, - grossTonnage: 25000, - deadweightTonnage: 0, - distanceTravelled: 150000, + ShipType.RoRoPassengerShip, + grossTonnage: 25000, + deadweightTonnage: 0, + distanceTravelled: 150000, TypeOfFuel.DIESEL_OR_GASOIL, fuelConsumption: 1.9e+10, 2019 @@ -42,6 +42,78 @@ public void TestCalculator() Assert.IsTrue(result.Results.Count(result => result.IsEstimatedYear) == 11); } + /// + /// Tests a known and pre-calculated set of inputs for a RoRoPassengerShip in the multi-fuel scenario + /// + [TestMethod] + public void TestCalculatorMultiFuel() + { + var _calc = new ShipCarbonIntensityCalculator(); + + var result = _calc.CalculateAttainedCiiRating( + ShipType.RoRoPassengerShip, + grossTonnage: 25000, + deadweightTonnage: 0, + distanceTravelled: 150000, + new List { + new FuelTypeConsumption + { + FuelConsumption = 1.9e+10, + FuelType = TypeOfFuel.DIESEL_OR_GASOIL + } + }, + 2019 + ); + + System.Diagnostics.Debug.WriteLine("Basic result is:"); + + string json = JsonConvert.SerializeObject(result, Formatting.Indented); + System.Diagnostics.Debug.WriteLine(json); + + Assert.IsNotNull(result); + Assert.AreEqual(result.Results.Count(), 12); + + Assert.IsTrue(result.Results.Count(result => result.IsMeasuredYear) == 1); + Assert.IsTrue(result.Results.Count(result => result.IsEstimatedYear) == 11); + } + + + [TestMethod] + public void TestCalculatorMultiFuelSameResultAsSingleFuel() + { + var _calc = new ShipCarbonIntensityCalculator(); + + var resultMultiFuel = _calc.CalculateAttainedCiiRating( + ShipType.RoRoPassengerShip, + grossTonnage: 25000, + deadweightTonnage: 0, + distanceTravelled: 150000, + new List { + new FuelTypeConsumption + { + FuelConsumption = 1.9e+10, + FuelType = TypeOfFuel.DIESEL_OR_GASOIL + } + }, + 2019 + ); + + var resultSingleFuel = _calc.CalculateAttainedCiiRating( + ShipType.RoRoPassengerShip, + grossTonnage: 25000, + deadweightTonnage: 0, + distanceTravelled: 150000, + TypeOfFuel.DIESEL_OR_GASOIL, + fuelConsumption: 1.9e+10, + 2019 + ); + + var singleFuelAsJsonObj = JsonConvert.SerializeObject(resultSingleFuel); + var multiFuelAsJsonObj = JsonConvert.SerializeObject(resultMultiFuel); + + Assert.AreEqual(singleFuelAsJsonObj, multiFuelAsJsonObj); + } + /// @@ -141,7 +213,7 @@ public void TestRoRoPassengerShipReturnsExpectedValues( [DataRow(ShipType.BulkCarrier, 25000, 0, TypeOfFuel.DIESEL_OR_GASOIL, 1e+10, 2029, ImoCiiRating.E, 7.240951252672751, 8.549333333333333)] [DataRow(ShipType.BulkCarrier, 25000, 0, TypeOfFuel.DIESEL_OR_GASOIL, 1e+10, 2030, ImoCiiRating.E, 7.0664704995963, 8.549333333333333)] [TestMethod] - public void TestBulkCarrierReturnsExpectedValues( + public void TestMultiFuelBulkCarrierReturnsExpectedValues( ShipType shipType, double deadweightTonnage, double grossTonnage, @@ -184,5 +256,85 @@ public void TestBulkCarrierReturnsExpectedValues( Assert.AreEqual(result.Results.First(c => c.Year == year).Rating, expectedRating, $"{nameof(expectedRating)} value was incorrect"); Assert.AreNotEqual(result.Results.First(c => c.Year == year).IsMeasuredYear, result.Results.First(c => c.Year == year).IsEstimatedYear); } + + + + + + + /// + /// Tests a known and pre-calculated set of inputs for a RoRoPassengerShip in the multi-fuel scenario + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + [DataRow(ShipType.BulkCarrier, 25000, 0, TypeOfFuel.DIESEL_OR_GASOIL, 1e+10, 2019, ImoCiiRating.C, 8.724037653822592, 8.549333333333333)] + [DataRow(ShipType.BulkCarrier, 25000, 0, TypeOfFuel.DIESEL_OR_GASOIL, 1e+10, 2020, ImoCiiRating.C, 8.636797277284366, 8.549333333333333)] + [DataRow(ShipType.BulkCarrier, 25000, 0, TypeOfFuel.DIESEL_OR_GASOIL, 1e+10, 2021, ImoCiiRating.C, 8.54955690074614, 8.549333333333333)] + [DataRow(ShipType.BulkCarrier, 25000, 0, TypeOfFuel.DIESEL_OR_GASOIL, 1e+10, 2022, ImoCiiRating.C, 8.462316524207914, 8.549333333333333)] + [DataRow(ShipType.BulkCarrier, 25000, 0, TypeOfFuel.DIESEL_OR_GASOIL, 1e+10, 2023, ImoCiiRating.C, 8.287835771131462, 8.549333333333333)] + [DataRow(ShipType.BulkCarrier, 25000, 0, TypeOfFuel.DIESEL_OR_GASOIL, 1e+10, 2024, ImoCiiRating.C, 8.11335501805501, 8.549333333333333)] + [DataRow(ShipType.BulkCarrier, 25000, 0, TypeOfFuel.DIESEL_OR_GASOIL, 1e+10, 2025, ImoCiiRating.D, 7.938874264978558, 8.549333333333333)] + [DataRow(ShipType.BulkCarrier, 25000, 0, TypeOfFuel.DIESEL_OR_GASOIL, 1e+10, 2026, ImoCiiRating.D, 7.764393511902107, 8.549333333333333)] + [DataRow(ShipType.BulkCarrier, 25000, 0, TypeOfFuel.DIESEL_OR_GASOIL, 1e+10, 2027, ImoCiiRating.D, 7.589912758825655, 8.549333333333333)] + [DataRow(ShipType.BulkCarrier, 25000, 0, TypeOfFuel.DIESEL_OR_GASOIL, 1e+10, 2028, ImoCiiRating.D, 7.415432005749203, 8.549333333333333)] + [DataRow(ShipType.BulkCarrier, 25000, 0, TypeOfFuel.DIESEL_OR_GASOIL, 1e+10, 2029, ImoCiiRating.E, 7.240951252672751, 8.549333333333333)] + [DataRow(ShipType.BulkCarrier, 25000, 0, TypeOfFuel.DIESEL_OR_GASOIL, 1e+10, 2030, ImoCiiRating.E, 7.0664704995963, 8.549333333333333)] + [TestMethod] + public void TestBulkCarrierReturnsExpectedValues( + ShipType shipType, + double deadweightTonnage, + double grossTonnage, + TypeOfFuel typeOfFuel, + double fuelConsumptionInGrams, + int year, + ImoCiiRating expectedRating, + double expectedRequiredCii, + double expectedAttainedCii) + { + double expectedArRatio = expectedAttainedCii / expectedRequiredCii; + + var _calc = new ShipCarbonIntensityCalculator(); + + var result = _calc.CalculateAttainedCiiRating( + shipType, + grossTonnage: grossTonnage, + deadweightTonnage: deadweightTonnage, + distanceTravelled: 150000, + new List { + new FuelTypeConsumption + { + FuelConsumption = fuelConsumptionInGrams, + FuelType = typeOfFuel + } + }, + year + ); + + + string json = JsonConvert.SerializeObject(result, Formatting.Indented); + System.Diagnostics.Debug.WriteLine(json); + + Assert.IsNotNull(result); + Assert.AreEqual(result.Results.Count(), 12); + + Assert.IsTrue(result.Results.Count(result => result.IsMeasuredYear) == 1); + Assert.IsTrue(result.Results.Count(result => result.IsEstimatedYear) == 11); + + Assert.AreEqual(result.Results.First(c => c.Year == year).Year, year, $"{nameof(year)} value was incorrect"); + Assert.AreEqual(result.Results.First(c => c.Year == year).VectorBoundariesForYear.ShipType, shipType, $"{nameof(shipType)} value was incorrect"); + Assert.AreEqual(result.Results.First(c => c.Year == year).RequiredCii, expectedRequiredCii, $"{nameof(expectedRequiredCii)} value was incorrect"); + Assert.AreEqual(result.Results.First(c => c.Year == year).AttainedRequiredRatio, expectedArRatio, $"{nameof(expectedArRatio)} value was incorrect"); + Assert.AreEqual(result.Results.First(c => c.Year == year).AttainedCii, expectedAttainedCii, $"{nameof(expectedAttainedCii)} value was incorrect"); + Assert.AreEqual(result.Results.First(c => c.Year == year).Rating, expectedRating, $"{nameof(expectedRating)} value was incorrect"); + Assert.AreNotEqual(result.Results.First(c => c.Year == year).IsMeasuredYear, result.Results.First(c => c.Year == year).IsEstimatedYear); + } } } diff --git a/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.Core/Models/CalculationResult.cs b/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.Core/Models/CalculationResult.cs index cf311bb..6f5939a 100644 --- a/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.Core/Models/CalculationResult.cs +++ b/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.Core/Models/CalculationResult.cs @@ -14,10 +14,9 @@ public class CalculationResult public CalculationResult(IEnumerable results) { Results = results; + } - - /// /// Contains a collection of CII Ratings for each year /// between 2019 and 2030 @@ -35,6 +34,11 @@ public class ResultYear /// /// if true, the CII rating, and all other values are measured /// if false, the CII rating, and all other values are estimates + /// + /// If true, the , , and + /// will all be generated against this year. If false, these + /// properties are equivalent to the most recent year data was provided for (for example, if this year is + /// 2026, and data exists for 2020 and 2021, the properties will match the 2021 data). /// public bool IsMeasuredYear { get; set; } @@ -45,11 +49,40 @@ public class ResultYear /// if false, the CII rating, and all other values are measured /// public bool IsEstimatedYear { get { return !IsMeasuredYear; } } + + /// + /// The year this result references + /// public int Year { get; set; } + + /// + /// The ship's IMO CII Rating, from A to E + /// public ImoCiiRating Rating { get; set; } + + /// + /// The ship's required carbon intensity for this year + /// public double RequiredCii { get; set; } + + /// + /// The ship's attained Carbon Intensity Indicator for this year + /// public double AttainedCii { get; set; } + /// + /// The Co2e Emissions calculated for this year + /// + public double CalculatedCo2eEmissions { get; set; } + /// + /// The Ship Capacity calculated for this year + /// + public double CalculatedShipCapacity { get; set; } + /// + /// The Transport Work calculated for this year + /// + public double CalculatedTransportWork { get; set; } + /// /// This is the ratio of Attained:Required CII /// diff --git a/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.Core/ShipCarbonIntensityCalculator.cs b/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.Core/ShipCarbonIntensityCalculator.cs index 8814b8e..ce02ccc 100644 --- a/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.Core/ShipCarbonIntensityCalculator.cs +++ b/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.Core/ShipCarbonIntensityCalculator.cs @@ -4,6 +4,8 @@ using EtiveMor.OpenImoCiiCalculator.Core.Models.MeasurementModels; using EtiveMor.OpenImoCiiCalculator.Core.Services; using EtiveMor.OpenImoCiiCalculator.Core.Services.Impl; +using System.Security.Cryptography.X509Certificates; +using Microsoft.VisualBasic.FileIO; namespace EtiveMor.OpenImoCiiCalculator.Core { @@ -26,15 +28,80 @@ public ShipCarbonIntensityCalculator() } /// + /// Calculate the attained CII rating for a ship for a given year /// + /// This method accepts multiple fuel types. /// - /// + /// The type of ship /// in long-tons /// in long-tons /// distance travelled in nautical miles - /// + /// The type of fuel /// quantity of fuel consumed in grams - /// + /// + /// A containing details of the ship's carbon intensity rating + /// + public CalculationResult CalculateAttainedCiiRating( + ShipType shipType, + double grossTonnage, + double deadweightTonnage, + double distanceTravelled, + IEnumerable fuelTypeConsumptions, + int targetYear) + { + if (fuelTypeConsumptions == null || fuelTypeConsumptions.Count() == 0) + { + throw new ArgumentException("FuelTypeConsumptions must be provided"); + } + double shipCo2Emissions = 0; + foreach (var consumption in fuelTypeConsumptions) + { + shipCo2Emissions += _shipMassOfCo2EmissionsService.GetMassOfCo2Emissions(consumption.FuelType, consumption.FuelConsumption); + } + var shipCapacity = _shipCapacityService.GetShipCapacity(shipType, deadweightTonnage, grossTonnage); + var transportWork = _shipTransportWorkService.GetShipTransportWork(shipCapacity, distanceTravelled); + + List results = new List(); + for (int year = 2019; year <= 2030; year++) + { + var attainedCiiInYear = _carbonIntensityIndicatorService.GetAttainedCarbonIntensity(shipCo2Emissions, transportWork); + var requiredCiiInYear = _carbonIntensityIndicatorService.GetRequiredCarbonIntensity(shipType, shipCapacity, year); + + var vectors = _ratingBoundariesService.GetBoundaries(new Ship(shipType, deadweightTonnage, grossTonnage), requiredCiiInYear, year); + var rating = GetImoCiiRatingFromVectors(vectors, attainedCiiInYear, year); + + results.Add(new ResultYear + { + IsMeasuredYear = targetYear == year, + Year = year, + AttainedCii = attainedCiiInYear, + RequiredCii = requiredCiiInYear, + Rating = rating, + VectorBoundariesForYear = vectors, + CalculatedCo2eEmissions = shipCo2Emissions, + CalculatedShipCapacity = shipCapacity, + CalculatedTransportWork = transportWork + }); + } + + return new CalculationResult(results); + } + + /// + /// Calculate the attained CII rating for a ship for a given year + /// + /// This method accepts exactly one fuel type. For multiple fuel + /// types, use the method + /// + /// The type of ship + /// in long-tons + /// in long-tons + /// distance travelled in nautical miles + /// The type of fuel + /// quantity of fuel consumed in grams + /// + /// A containing details of the ship's carbon intensity rating + /// public CalculationResult CalculateAttainedCiiRating( ShipType shipType, double grossTonnage, @@ -64,7 +131,10 @@ public CalculationResult CalculateAttainedCiiRating( AttainedCii = attainedCiiInYear, RequiredCii = requiredCiiInYear, Rating = rating, - VectorBoundariesForYear = vectors + VectorBoundariesForYear = vectors, + CalculatedCo2eEmissions = shipCo2Emissions, + CalculatedShipCapacity = shipCapacity, + CalculatedTransportWork = transportWork }); } @@ -102,5 +172,17 @@ private ImoCiiRating GetImoCiiRatingFromVectors(ShipDdVectorBoundaries boundarie } } + + public class AnnaulConsumption + { + public int TargetYear { get; set; } + public IEnumerable FuelConsumption { get; set; } + } + + public class FuelTypeConsumption + { + public TypeOfFuel FuelType { get; set; } + public double FuelConsumption { get; set; } + } } diff --git a/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.DemoConsoleApp/Program.cs b/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.DemoConsoleApp/Program.cs index d22043b..b048e83 100644 --- a/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.DemoConsoleApp/Program.cs +++ b/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.DemoConsoleApp/Program.cs @@ -2,12 +2,41 @@ using EtiveMor.OpenImoCiiCalculator.Core.Models.Enums; using EtiveMor.OpenImoCiiCalculator.Core.Services.Impl; using Newtonsoft.Json; +using System.Net.Http.Headers; namespace EtiveMor.OpenImoCiiCalculator.DemoConsoleApp { internal class Program { static void Main(string[] args) + { + Console.WriteLine("---------------------"); + Console.WriteLine("Generating a multi-fuel ship report..."); + Console.WriteLine("---------------------"); + + MainMultiFuelCalculation(args); + + Console.WriteLine("---------------------"); + Console.WriteLine("Completed the multi-fuel ship report..."); + Console.WriteLine("---------------------"); + + + Console.WriteLine("---------------------"); + Console.WriteLine("Generating a single-fuel ship report..."); + Console.WriteLine("---------------------"); + + MainOneFuelCalculation(args); + + Console.WriteLine("---------------------"); + Console.WriteLine("Completed the single-fuel ship report..."); + Console.WriteLine("---------------------"); + } + + /// + /// Runs the single fuel calculation with sample data + /// + /// + static void MainOneFuelCalculation(string[] args) { Console.WriteLine("Generating a ship result now..."); @@ -28,5 +57,34 @@ static void Main(string[] args) Console.WriteLine("Press any key to finish"); Console.ReadKey(); } + + + /// + /// Runs the multi-fuel calculation with sample data + /// + /// + static void MainMultiFuelCalculation(string[] args) + { + var calculator = new ShipCarbonIntensityCalculator(); + + var result = calculator.CalculateAttainedCiiRating( + ShipType.RoRoPassengerShip, + grossTonnage: 25000, + deadweightTonnage: 0, + distanceTravelled: 150000, + new List { + new FuelTypeConsumption + { + FuelConsumption = 1.9e+10, + FuelType = TypeOfFuel.DIESEL_OR_GASOIL + } + }, + 2019); + + string json = JsonConvert.SerializeObject(result, Formatting.Indented); + Console.WriteLine(json); + Console.WriteLine("Press any key to finish"); + Console.ReadKey(); + } } } \ No newline at end of file diff --git a/README.md b/README.md index e8e54fe..c656858 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,6 @@ The specification for this software can be found in [IMO's resolution MEPC.353(7 The following features are on the roadmap for the application: -- Support for multiple engines & fuel types for ships. Currently the application only considers fuel consumption by a ship's main engine. - Support for Dependency Injection (DI). Currently the application does not - Support for IMO Resolution MEPC.355(78). Currently the application considers fuel consumption only. Support for MEPC355(78) will bring additional CII properties, for example the lighting in crew quarters. @@ -80,6 +79,10 @@ int targetYear - Fuel consumption is measured in grams, and accepts scientific notation like `1.9e+10` - Year must refer to the measured year. For example, if a ship's fuel consumption is known in 2022, all other results will be based from that point +**_Multiple Fuel Type calculations_** + +That there are two CalculateAttainedCiiRating methods. One for a ship which consumes a single fuel type, and another which consumes multiple fuel types. Both methods are available at `CalculateAttainedCiiRating`. + ### Calculator Result Format ```json @@ -93,6 +96,9 @@ int targetYear "requiredCii": 19.184190519387734, "attainedCii": 16.243733333333335, "attainedRequiredRatio": 0.8467249799733408, + "calculatedCo2eEmissions": 60914000000.0, + "calculatedShipCapacity": 25000.0, + "calculatedTransportWork": 3750000000.0, "vectorBoundariesForYear": { "year": 2019, "shipType": 110, @@ -123,6 +129,9 @@ int targetYear | `requiredCii` | The actual intensity required for the ship to be considered in-range of the IMO's regulations (note that from 2027 onwards, this is a projection) | | `attainedCii` | The estimated or actual intensity attained for the ship in the given year | | `attainedRequiredRatio` | The ratio between `requiredCii` and `attainedCii` | +| `calculatedCo2eEmissions` | The calculated CO2e emissions this result was based on | +| `calculatedShipCapacity` | The calculated ship capacity this result was based on | +| `calculatedTransportWork` | The calculated transport work this result was based on | | `vectorBoundariesForYear.year` | the year in question (repeats `.year`) | | `vectorBoundariesForYear.shipType` | the type of ship `EtiveMor.OpenImoCiiCalculator.Core.Models.Enums.ShipType` | | `vectorBoundariesForYear.weightClassification` | the weight classification the ship has been considered for (see MEPC.354(78)) | @@ -254,7 +263,7 @@ Returns the product of a ship's capacity and its distance sailed ## Ship CO2 Emissions Methodology -The sum of a ship's $CO_2$ emissions over a given year are calculated by multiplying the mass of consumed fuel by the fuel's emissions factor. +The sum of a ship's $CO_2$ emissions over a given year are calculated by multiplying the mass of consumed fuel by the fuel's emissions factor. If the ship consumes multiple fuel types, the calculation is repeated for each fuel type & consumption mass, then those results are summed together. **Method Accepts**: - `fuelType`, an enum derrived from [Table 2](#table-2-mepc36479-mass-conversion-between-fuel-consumption-and-co_2-emissions)'s *Fuel Type* column