From decdfd88d7ec36b136614cf1794a01a1b16ac271 Mon Sep 17 00:00:00 2001 From: LFLaverty Date: Wed, 27 Mar 2024 16:36:47 +0000 Subject: [PATCH] starts to add annual adjustments --- .../CalculatorTests.cs | 13 +- .../ShipCapacityTests.cs | 3 +- .../Calculator.cs | 17 +- .../Models/Enums/ShipType.cs | 6 + ...rbonIntensityIndicatorCalculatorService.cs | 7 +- ...rbonIntensityIndicatorCalculatorService.cs | 347 +++++++++++++++++- .../Impl/ShipCapacityCalculatorService.cs | 42 ++- README.md | 4 +- 8 files changed, 414 insertions(+), 25 deletions(-) diff --git a/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.Core.Tests/CalculatorTests.cs b/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.Core.Tests/CalculatorTests.cs index d152670..d35d5a2 100644 --- a/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.Core.Tests/CalculatorTests.cs +++ b/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.Core.Tests/CalculatorTests.cs @@ -16,12 +16,13 @@ public void TestCalculator() var _calc = new Calculator(); var result = _calc.CalculateAttainedCiiRating( - ShipType.BulkCarrier, - 1000, - 1000, - 1000, - TypeOfFuel.HEAVYFUELOIL, - 1000); + ShipType.RoRoPassengerShip, + grossTonnage: 25000, + deadweightTonnage: 0, + distanceTravelled: 150000, + TypeOfFuel.DIESEL_OR_GASOIL, + fuelConsumption: 1.9e+10 + ); Assert.AreNotEqual(ImoCiiRating.ERR, result); } diff --git a/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.Core.Tests/ShipCapacityTests.cs b/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.Core.Tests/ShipCapacityTests.cs index 2791100..bd599f7 100644 --- a/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.Core.Tests/ShipCapacityTests.cs +++ b/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.Core.Tests/ShipCapacityTests.cs @@ -86,7 +86,8 @@ public void TestValidateTonnageParamsSet_ArgumentOutOfRangeException(ShipType sh [DataRow(ShipType.RefrigeratedCargoCarrier, 250000, 0, 250000)] [DataRow(ShipType.CombinationCarrier, 250000, 0, 250000)] [DataRow(ShipType.LngCarrier, 250000, 0, 250000)] - [DataRow(ShipType.RoRoCargoShip, 250000, 0, 250000)] + [DataRow(ShipType.RoRoCargoShip, 0, 250000, 250000)] + [DataRow(ShipType.RoRoPassengerShip_HighSpeedSOLAS, 0, 250000, 250000)] [DataRow(ShipType.RoRoCargoShipVehicleCarrier, 0, 100000, 100000)] [DataRow(ShipType.RoRoPassengerShip, 0, 100000, 100000)] [DataRow(ShipType.RoRoCruisePassengerShip, 0, 100000, 100000)] diff --git a/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.Core/Calculator.cs b/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.Core/Calculator.cs index 7be55ea..99dd5d0 100644 --- a/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.Core/Calculator.cs +++ b/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.Core/Calculator.cs @@ -18,16 +18,27 @@ public Calculator() _carbonIntensityIndicatorService = new CarbonIntensityIndicatorCalculatorService(); } - + /// + /// + /// + /// + /// in long-tons + /// in long-tons + /// distance travelled in nautical miles + /// + /// quantity of fuel consumed in grams + /// public ImoCiiRating CalculateAttainedCiiRating(ShipType shipType, double grossTonnage, double deadweightTonnage, double distanceTravelled, TypeOfFuel fuelType, double fuelConsumption) { var shipCo2Emissions = _shipMassOfCo2EmissionsService.GetMassOfCo2Emissions(fuelType, fuelConsumption); - var shipCapacity = _shipCapacityService.GetShipCapacity(shipType, grossTonnage, deadweightTonnage); + var shipCapacity = _shipCapacityService.GetShipCapacity(shipType, deadweightTonnage, grossTonnage); var transportWork = _shipTransportWorkService.GetShipTransportWork(shipCapacity, distanceTravelled); var attainedCii = _carbonIntensityIndicatorService.GetAttainedCarbonIntensity(shipCo2Emissions, transportWork); + var requiredCii = _carbonIntensityIndicatorService.GetRequiredCarbonIntensity(shipType, shipCapacity, 2019); + var attainedRequiredRatio = attainedCii / requiredCii; - + return ImoCiiRating.ERR; } diff --git a/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.Core/Models/Enums/ShipType.cs b/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.Core/Models/Enums/ShipType.cs index e013661..8e71295 100644 --- a/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.Core/Models/Enums/ShipType.cs +++ b/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.Core/Models/Enums/ShipType.cs @@ -77,6 +77,12 @@ public enum ShipType /// /// RoRoPassengerShip = 110, + + /// + /// A type of high-speed ship designed to conform to SOLAS Chapter X standards + /// + /// + RoRoPassengerShip_HighSpeedSOLAS = 111, /// /// A type of ship designed primarily for passenger accommodation and leisure diff --git a/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.Core/Services/ICarbonIntensityIndicatorCalculatorService.cs b/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.Core/Services/ICarbonIntensityIndicatorCalculatorService.cs index 37690bf..9a16f24 100644 --- a/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.Core/Services/ICarbonIntensityIndicatorCalculatorService.cs +++ b/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.Core/Services/ICarbonIntensityIndicatorCalculatorService.cs @@ -1,8 +1,11 @@ -namespace EtiveMor.OpenImoCiiCalculator.Core.Services +using EtiveMor.OpenImoCiiCalculator.Core.Models.Enums; + +namespace EtiveMor.OpenImoCiiCalculator.Core.Services { public interface ICarbonIntensityIndicatorCalculatorService { double GetAttainedCarbonIntensity(double massOfCo2Emissions, double transportWork); - void GetReferenceCarbonIntensity(); + double GetRequiredCarbonIntensity(ShipType shipType, double capacity, int year); + Dictionary GetRequiredCarbonIntensity(ShipType shipType, double capacity); } } \ No newline at end of file diff --git a/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.Core/Services/Impl/CarbonIntensityIndicatorCalculatorService.cs b/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.Core/Services/Impl/CarbonIntensityIndicatorCalculatorService.cs index 450f795..8065672 100644 --- a/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.Core/Services/Impl/CarbonIntensityIndicatorCalculatorService.cs +++ b/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.Core/Services/Impl/CarbonIntensityIndicatorCalculatorService.cs @@ -1,4 +1,6 @@ -namespace EtiveMor.OpenImoCiiCalculator.Core.Services.Impl +using EtiveMor.OpenImoCiiCalculator.Core.Models.Enums; + +namespace EtiveMor.OpenImoCiiCalculator.Core.Services.Impl { public class CarbonIntensityIndicatorCalculatorService : ICarbonIntensityIndicatorCalculatorService { @@ -37,9 +39,348 @@ public double GetAttainedCarbonIntensity(double massOfCo2Emissions, double trans } - public void GetReferenceCarbonIntensity() + public Dictionary GetRequiredCarbonIntensity(ShipType shipType, double capacity) + { + var dict = new Dictionary(); + + for (int year = 2019; year <= 2030; year++) + { + // Your code here + dict.Add(year, GetRequiredCarbonIntensity(shipType, capacity, year)); + } + + return dict; + } + + + /// + /// Gets a ship's required CII in accordance with MEPC.323(74) + /// + /// + /// The type of ship + /// + /// + /// The ship's capacity according to MEPC 337(76). + /// + /// Note that the capacity must have been pre-calculated, + /// using + /// + /// + /// The required CII for a ship of the given type and capacity + /// + /// Thrown if capacity is equal or lower than 0 + public double GetRequiredCarbonIntensity(ShipType shipType, double capacity, int year) + { + if (capacity <= 0) + { + throw new ArgumentException("Capacity must be a positive value", nameof(capacity)); + } + + double a = GetValue(ValType.a, shipType, capacity); + double c = GetValue(ValType.c, shipType, capacity); + + double ciiReference = a * Math.Pow(capacity, -c); + + return ciiReference * (1 - GetAnnualReductionFactor(year)); + } + + /// + /// Gets an annual reduction factor for a given year, according to MEPC.338(76) + /// + /// the calendar year being analysed + /// the reduction factor + /// + /// Thrown if a year outside of the range 2019-2030 (inclusive) is provided + /// + private double GetAnnualReductionFactor(int year) + { + switch (year) { + case 2019: + return 0.00; + case 2020: + return 0.01; + case 2021: + return 0.02; + case 2022: + return 0.03; + case 2023: + return 0.05; + case 2024: + return 0.07; + case 2025: + return 0.09; + case 2026: + return 0.11; + case 2027: + return 0.13; + case 2028: + return 0.15; + case 2029: + return 0.17; + case 2030: + return 0.19; + default: + throw new NotSupportedException($"Year {year} is not supported"); + } + } + + /// + /// Gets either the `a` or `c` value for a given ship type and capacity + /// + /// The to return, must be either a or c + /// the type of ship being queried + /// the capacity of the ship being queried + /// + /// the `a` or `c` value for the given ship type and capacity + /// + /// Thrown if is not a or c + /// + /// Thrown if the ship type is not supported + /// + private double GetValue(ValType valType, ShipType shipType, double capacity) + { + if (valType != ValType.a && valType != ValType.c) + { + throw new ArgumentException($"Invalid value type '{valType}'", nameof(valType)); + } + + + return shipType switch + { + ShipType.BulkCarrier => GetBulkCarrierValue(valType, capacity), + ShipType.GasCarrier => GetGasCarrierValue(valType, capacity), + ShipType.Tanker => GetTankerValue(valType, capacity), + ShipType.ContainerShip => GetContainerShipValue(valType, capacity), + ShipType.GeneralCargoShip => GetGeneralCargoShipValue(valType, capacity), + ShipType.RefrigeratedCargoCarrier => GetRefrigeratedCargoCarrierValue(valType, capacity), + ShipType.CombinationCarrier => GetCombinationCarrierValue(valType, capacity), + ShipType.LngCarrier => GetLngCarrierShipValue(valType, capacity), + ShipType.RoRoCargoShipVehicleCarrier => GetRoRoCargoShipVehicleCarrierValue(valType, capacity), + ShipType.RoRoCargoShip => GetRoRoCargoShipValue(valType, capacity), + ShipType.RoRoPassengerShip => GetRoRoPassengerShipValue(valType, capacity), + ShipType.RoRoPassengerShip_HighSpeedSOLAS => GetRoRoPassengerShip_HighSpeedSOLASValue(valType, capacity), + ShipType.RoRoCruisePassengerShip => GetRoRoCruisePassengerShipValue(valType, capacity), + ShipType.UNKNOWN => throw new NotSupportedException($"Unsupported {nameof(shipType)} '{shipType}'"), + _ => throw new NotSupportedException($"Unsupported {nameof(shipType)} '{shipType}'") + }; + } + + + /// + /// Gets the appropriate `a` or `c` value for a LNG Carrier, according to + /// Table 1: MEPC.353(78) + /// + /// The to return, must be either a or c + /// the capacity of the ship being queried + /// + /// The `a` or `c` value for a LNG Carrier + /// + private double GetLngCarrierShipValue(ValType valType, double capacity) + { + if (capacity >= 100000) + { + return valType == ValType.a ? 9.827 : 0.000; + } + if (capacity >= 65000) + { + return valType == ValType.a ? 14479E10 : 2.673; + } + return valType == ValType.a ? 14779E10 : 2.673; + } + + /// + /// Gets the appropriate `a` or `c` value for a General Cargo ship, according to + /// Table 1: MEPC.353(78) + /// + /// The to return, must be either a or c + /// the capacity of the ship being queried + /// + /// The `a` or `c` value for a General Cargo ship + /// + private double GetGeneralCargoShipValue(ValType valType, double capacity) + { + if (capacity >= 20000) + { + return valType == ValType.a ? 31948 : 0.792; + } + return valType == ValType.a ? 588 : 0.3885; + } + + /// + /// Gets the appropriate `a` or `c` value for a Bulk Carrier ship, according to + /// Table 1: MEPC.353(78) + /// + /// The to return, must be either a or c + /// the capacity of the ship being queried + /// + /// The `a` or `c` value for a Bulk Carrier ship + /// + private double GetBulkCarrierValue(ValType valType, double capacity) + { + if (capacity >= 279000) + { + return valType == ValType.a ? 4745 : 0.622; + } + return valType == ValType.a ? 4745 : 0.622; + } + + /// + /// Gets the appropriate `a` or `c` value for a Gas Carrier ship, according to + /// Table 1: MEPC.353(78) + /// + /// The to return, must be either a or c + /// the capacity of the ship being queried + /// + /// The `a` or `c` value for a Gas Carrier ship + /// + private double GetGasCarrierValue(ValType valType, double capacity) + { + if (capacity >= 65000) + { + return valType == ValType.a ? 14405E7 : 2.071; + } + return valType == ValType.a ? 8104 : 0.639; + } + + /// + /// Gets the appropriate `a` or `c` value for a Tanker ship, according to + /// Table 1: MEPC.353(78) + /// + /// The to return, must be either a or c + /// the capacity of the ship being queried + /// + /// The `a` or `c` value for a Tanker ship + /// + private double GetTankerValue(ValType valType, double capacity) + { + return valType == ValType.a ? 5247 : 0.610; + } + + /// + /// Gets the appropriate `a` or `c` value for a ContainerShip ship, according to + /// Table 1: MEPC.353(78) + /// + /// The to return, must be either a or c + /// the capacity of the ship being queried + /// + /// The `a` or `c` value for a ContainerShip ship + /// + private double GetContainerShipValue(ValType valType, double capacity) + { + return valType == ValType.a ? 1984 : 0.489; + } + + /// + /// Gets the appropriate `a` or `c` value for a RefrigeratedCargoCarrier ship, according to + /// Table 1: MEPC.353(78) + /// + /// The to return, must be either a or c + /// the capacity of the ship being queried + /// + /// The `a` or `c` value for a RefrigeratedCargoCarrier ship + /// + private double GetRefrigeratedCargoCarrierValue(ValType valType, double capacity) + { + return valType == ValType.a ? 4600 : 0.557; + } + + /// + /// Gets the appropriate `a` or `c` value for a CombinationCarrier ship, according to + /// Table 1: MEPC.353(78) + /// + /// The to return, must be either a or c + /// the capacity of the ship being queried + /// + /// The `a` or `c` value for a CombinationCarrier ship + /// + private double GetCombinationCarrierValue(ValType valType, double capacity) + { + return valType == ValType.a ? 5119 : 622; + } + + /// + /// Gets the appropriate `a` or `c` value for a RoRoCargoShipVehicleCarrier ship, according to + /// Table 1: MEPC.353(78) + /// + /// The to return, must be either a or c + /// the capacity of the ship being queried + /// + /// The `a` or `c` value for a RoRoCargoShipVehicleCarrier ship + /// + private double GetRoRoCargoShipVehicleCarrierValue(ValType valType, double capacity) + { + if (capacity >= 57700) + { + return valType == ValType.a ? 3627 : 0.590; + } + if (capacity >= 30000) + { + return valType == ValType.a ? 5739 : 0.590; + } + return valType == ValType.a ? 330 : 329; + } + + /// + /// Gets the appropriate `a` or `c` value for a RoRoCargoShip ship, according to + /// Table 1: MEPC.353(78) + /// + /// The to return, must be either a or c + /// the capacity of the ship being queried + /// + /// The `a` or `c` value for a RoRoCargoShip ship + /// + private double GetRoRoCargoShipValue(ValType valType, double capacity) + { + return valType == ValType.a ? 1967 : 0.485; + } + + /// + /// Gets the appropriate `a` or `c` value for a RoRoPassengerShip ship, according to + /// Table 1: MEPC.353(78) + /// + /// The to return, must be either a or c + /// the capacity of the ship being queried + /// + /// The `a` or `c` value for a RoRoPassengerShip ship + /// + private double GetRoRoPassengerShipValue(ValType valType, double capacity) + { + return valType == ValType.a ? 1012 : 0.460; + } + + /// + /// Gets the appropriate `a` or `c` value for a RoRoPassengerShip ship, according to + /// Table 1: MEPC.353(78) + /// + /// The to return, must be either a or c + /// the capacity of the ship being queried + /// + /// The `a` or `c` value for a RoRoPassengerShip ship + /// + private double GetRoRoPassengerShip_HighSpeedSOLASValue(ValType valType, double capacity) + { + return valType == ValType.a ? 4196 : 0.460; + } + + /// + /// Gets the appropriate `a` or `c` value for a RoRoCruisePassengerShip ship, according to + /// Table 1: MEPC.353(78) + /// + /// The to return, must be either a or c + /// the capacity of the ship being queried + /// + /// The `a` or `c` value for a RoRoCruisePassengerShip ship + /// + private double GetRoRoCruisePassengerShipValue(ValType valType, double capacity) + { + return valType == ValType.a ? 930 : 0.383; + } + + private enum ValType { - throw new NotImplementedException(); + a, + c, + ERR } } } diff --git a/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.Core/Services/Impl/ShipCapacityCalculatorService.cs b/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.Core/Services/Impl/ShipCapacityCalculatorService.cs index 67e4d4d..446e39f 100644 --- a/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.Core/Services/Impl/ShipCapacityCalculatorService.cs +++ b/EtiveMor.OpenImoCiiCalculator/EtiveMor.OpenImoCiiCalculator.Core/Services/Impl/ShipCapacityCalculatorService.cs @@ -8,13 +8,13 @@ namespace EtiveMor.OpenImoCiiCalculator.Core.Services.Impl public class ShipCapacityCalculatorService : IShipCapacityCalculatorService { /// - /// Calculates the ship's capacity according to the MEPC 337(76) guidelines + /// Calculates the ship's capacity according to the MEPC.353(78)guidelines /// /// /// The ship to calculate the capacity for /// /// - /// The ship's type capacity according to MEPC 337(76) + /// The ship's type capacity according to MEPC.353(78) /// public double GetShipCapacity(Ship ship) { @@ -22,7 +22,7 @@ public double GetShipCapacity(Ship ship) } /// - /// Calculates the ship's capacity according to the MEPC 337(76) guidelines + /// Calculates the ship's capacity according to the MEPC.353(78)guidelines /// /// /// The type of the ship (e.g., BulkCarrier, RoRoCargoShip, CruisePassengerShip, etc.) @@ -38,7 +38,7 @@ public double GetShipCapacity(Ship ship) /// fees. /// /// - /// The ship's type capacity according to MEPC 337(76) + /// The ship's type capacity according to MEPC.353(78) /// public double GetShipCapacity(ShipType shipType, double deadweightTonnage, double grossTonnage) { @@ -48,13 +48,33 @@ public double GetShipCapacity(ShipType shipType, double deadweightTonnage, doubl switch (shipType) { case ShipType.BulkCarrier: - return deadweightTonnage > 279000 ? 279000 : deadweightTonnage; - case ShipType.RoRoCargoShipVehicleCarrier: + return deadweightTonnage >= 279000 ? 279000 : deadweightTonnage; + case ShipType.GasCarrier: + return deadweightTonnage; + case ShipType.Tanker: + return deadweightTonnage; + case ShipType.ContainerShip: + return deadweightTonnage; + case ShipType.GeneralCargoShip: + return deadweightTonnage; + case ShipType.RefrigeratedCargoCarrier: + return deadweightTonnage; + case ShipType.CombinationCarrier: + return deadweightTonnage; + case ShipType.LngCarrier: + return deadweightTonnage < 65000 ? 65000 : deadweightTonnage; + case ShipType.RoRoCargoShipVehicleCarrier: + return deadweightTonnage >= 57700 ? 57700 : grossTonnage; + case ShipType.RoRoCargoShip: + return grossTonnage; case ShipType.RoRoPassengerShip: + return grossTonnage; + case ShipType.RoRoPassengerShip_HighSpeedSOLAS: + return grossTonnage; case ShipType.RoRoCruisePassengerShip: return grossTonnage; default: - return deadweightTonnage; + throw new ArgumentException($"Unsupported {nameof(shipType)}: {shipType}"); } } @@ -126,14 +146,18 @@ private void ValidateTonnageParamsSet(ShipType shipType, double deadweightTonnag ? grossTonnage : throw new InvalidOperationException(), - ShipType.RoRoCargoShip => ValidateTonnage(deadweightTonnage, nameof(deadweightTonnage), shipType) - ? deadweightTonnage + ShipType.RoRoCargoShip => ValidateTonnage(grossTonnage, nameof(grossTonnage), shipType) + ? grossTonnage : throw new InvalidOperationException(), ShipType.RoRoPassengerShip => ValidateTonnage(grossTonnage, nameof(grossTonnage), shipType) ? grossTonnage : throw new InvalidOperationException(), + ShipType.RoRoPassengerShip_HighSpeedSOLAS => ValidateTonnage(grossTonnage, nameof(grossTonnage), shipType) + ? grossTonnage + : throw new InvalidOperationException(), + ShipType.RoRoCruisePassengerShip => ValidateTonnage(grossTonnage, nameof(grossTonnage), shipType) ? grossTonnage : throw new InvalidOperationException(), diff --git a/README.md b/README.md index 90c07dd..0a4d2c3 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,9 @@ An unofficial open source implementation of the International Maritime Organisation (IMO)'s Carbon Intensity Indicator (CII). -The CII indicator aims to make the carbon intensity of any given ship easy to understand, transparent, and standardised. It does so by ranking all ships globally on an A to E rating (A being the best, E being the worst). Ship emission intensity calculations consider a mixture of weight, distance travelled in the calendar year, and the fuel used in their main engines (for a comprehensive explanation, see the [methodology section](#methodology)). Grades are re-calculated annually, as are the boundaries of what is considered "good". This moving target is intended to encourage shipping firms to constantly improve the carbon intensity of their ships. +The CII indicator aims to make the carbon intensity of any given ship easy to understand, transparent, and standardised. It does so by ranking all ships globally on an A to E rating (A being the best, E being the worst). Ship emission intensity calculations consider a mixture of weight, distance travelled in the calendar year, and the fuel used in their main engines (for a comprehensive explanation, see the [methodology section](#methodology)). + +Grades are re-calculated annually. The boundaries of what is considered "good" is a moving target, described in [table 4](#table-4-annual-carbon-reduction-factors-z). This moving target is intended to encourage shipping firms to constantly improve the carbon intensity of their ships to 2030. The specification for this software can be found in [IMO's resolution MEPC.353(78)](https://wwwcdn.imo.org/localresources/en/KnowledgeCentre/IndexofIMOResolutions/MEPCDocuments/MEPC.353(78).pdf), adopted in June 2021. Additional references, summaries, & resolutions can be found in the [References \& datasets](#references--datasets) section.