diff --git a/README.md b/README.md index 280cd26..973e356 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,9 @@ -# Open _IMO Carbon Intensity Indicator (CII)_ Calculator 🚢 +# Open _IMO Carbon Intensity Indicator (CII)_ Calculator (C#) 🚢 + +### Other versions + +[A Python version is available here](./python-impl/README.md) + ## What is this? diff --git a/python-impl/README.md b/python-impl/README.md new file mode 100644 index 0000000..1cf9061 --- /dev/null +++ b/python-impl/README.md @@ -0,0 +1,357 @@ +# Open _IMO Carbon Intensity Indicator (CII)_ Calculator (Python) 🚢 + +### Other versions + +[A C# version is available here](../README.md) + +## What is this? + +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)). + +Each grade from A to E, coloured gradually from green to red + +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 graph below demonstrates the gradual tightening of the IMO's Carbon Intensity requirements over time. The nearer to 2030, the lower a ship's Attained CII must be to achieve an A grade. + +A graph showing the gradual + +The specification for this software can be found in [IMO's resolution MEPC.354(78)](https://wwwcdn.imo.org/localresources/en/KnowledgeCentre/IndexofIMOResolutions/MEPCDocuments/MEPC.354(78).pdf), adopted in June 2022. Additional references, summaries, & resolutions can be found in the [References & datasets](#references--datasets) section. + +> **NOTE** +> The repository code & software is provided as-is. While best-efforts are made to ensure its results are accurate inline with the IMO's CII specifications, the results it produces are _**estimates**_ and _**guidance**_. Results should not be considered proof of regulatory compliance. + +# Table of Contents +- [Open IMO Carbon Intensity Indicator (CII) Calculator 🚢](#open-imo-carbon-intensity-indicator-cii-calculator-) + - [What is this?](#what-is-this) +- [Table of Contents](#table-of-contents) +- [Software](#software) + - [Software Roadmap](#software-roadmap) + - [Getting Started](#getting-started) + - [Calculator Result Format](#calculator-result-format) +- [Methodology](#methodology) + - [Ship Grade Ratio Methodology](#ship-grade-ratio-methodology) + - [Ship Grade Worked example](#ship-grade-worked-example) + - [Ship Attained Carbon Intensity Methodology](#ship-attained-carbon-intensity-methodology) + - [Ship transport work methodology](#ship-transport-work-methodology) + - [Ship CO2 Emissions Methodology](#ship-co2-emissions-methodology) + - [Ship Capacity Methodology](#ship-capacity-methodology) +- [Reference Tables](#reference-tables) + - [Table 1: MEPC.353(78) - Shipping Capacity Tables](#table-1-mepc35378---shipping-capacity-tables) + - [Table 2: MEPC.364(79) Mass Conversion between fuel consumption and CO2 emissions](#table-2-mepc36479-mass-conversion-between-fuel-consumption-and-co2-emissions) + - [Table 3: MEPC.339(76) - Ship Grading Boundaries](#table-3-mepc33976---ship-grading-boundaries) + - [Table 4: Annual Carbon Reduction Factors (Z%)](#table-4-annual-carbon-reduction-factors-z) + - [Table 5: Common shipping measurement conversions](#table-5-common-shipping-measurement-conversions) +- [Shipping Terminology & Glossary](#shipping-terminology--glossary) +- [References & datasets](#references--datasets) + - [Further Reading](#further-reading) + - [Useful datasets (mixed public and private)](#useful-datasets-mixed-public-and-private) + +# Software + +## Running tests + +To run the unit tests for this Python implementation, use the following command from the `python-impl` directory: + +```bash +pytest +``` + +This will automatically discover and run all tests in the `tests/` directory. + +## Software Roadmap + +The following features are on the roadmap for the application: + +- 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. + +## Getting Started + +The library can be installed as a Python package (coming soon). There is a Python module at `open_imo_cii_calculator/ship_carbon_intensity_calculator.py`, which demonstrates how to create a new instance of the calculator, submit data, and receive results in the format `CalculationResult`. + +When submitting data to the `calculate_attained_cii_rating` method, the following parameters are required: + +```python +ShipType ship_type, +float gross_tonnage, +float deadweight_tonnage, +float distance_travelled, +Iterable[FuelTypeConsumption] fuel_type_consumptions, +int target_year +``` + +- The ship's type must be one of `ShipType` (see `open_imo_cii_calculator/models/ship_type.py`). +- Gross Tonnage is measured in [long-tons](https://en.wikipedia.org/wiki/Long_ton) (1.016047 metric tonnes, or 1,016.047 kilograms) +- Deadweight Tonnage is measured in [long-tons](https://en.wikipedia.org/wiki/Long_ton) (1.016047 metric tonnes, or 1,016.047 kilograms) +- Distance travelled is measured in [nautical miles](https://en.wikipedia.org/wiki/Nautical_mile) (1,852 metres) +- Fuel type must be one of `TypeOfFuel` (see `open_imo_cii_calculator/models/fuel_type.py`) +- 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_** + +There is a `calculate_attained_cii_rating` method for ships which consume multiple fuel types. Pass a list of `FuelTypeConsumption` objects to the method. + +### Calculator Result Format + +The result is a `CalculationResult` object containing a list of `ResultYear` objects, each with the following properties: + +- `is_measured_year`: describes if the result array was generated based on this year +- `is_estimated_year`: describes if the result array was NOT generated based on this year (is always equal to not is_measured_year) +- `year`: describes the year in question +- `rating`: describes the rating for the ship in the given year. A=1, B=2, C=3, D=4, E=5. 0 indicates an error. See `ImoCiiRating` in `open_imo_cii_calculator/models/imo_cii_rating.py` +- `required_cii`: 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) +- `attained_cii`: The estimated or actual intensity attained for the ship in the given year +- `attained_required_ratio`: The ratio between `required_cii` and `attained_cii` +- `calculated_co2e_emissions`: The calculated CO2e emissions this result was based on +- `calculated_ship_capacity`: The calculated ship capacity this result was based on +- `calculated_transport_work`: The calculated transport work this result was based on +- `vector_boundaries_for_year`: An object with the following properties: + - `year`: the year in question + - `ship_type`: the type of ship (`ShipType`) + - `weight_classification`: the weight classification the ship has been considered for + - `capacity_unit`: describes if this ship was measured against its Deadweight or Gross Tonnage (`CapacityUnit`) + - `boundary_dd_vectors`: a dictionary with keys `SUPERIOR`, `LOWER`, `UPPER`, `INFERIOR` (see `ImoCiiBoundary`) + +# Methodology + +A ship's Carbon Intensity Indicator (CII) is measured by calculating its transport workload in a given calendar year, then calculating the mass of $CO_2$ produced by the ship in that year. The ship's *Attained CII* is the product of its $transportWork$ and the $massOfCO_2Emissions$ in one calendar year. + +> $AttainedCII = massOfCo2Emissions / transportWork$ + +Ships are split into 12 categories, for example "Bulk Carrier", "Tanker", "Cruise Passenger Ship" among others (see [Table 1](#table-1-mepc35378---shipping-capacity-tables) for a comprehensive list). A ship is compared internally among its category peers but never across categories, for example, a *Bulk Carrier* is not directly comparable to a *LNG Carrier* in this system. + +**Inputs** +- The type of ship +- The type of fuel used by the ship's main engine +- The capacity of the ship, measured in either Deadweight Tonnage (DWT) or Gross Tonnage (GT) +- The distance travelled by the ship in one calendar year, measured in nautical miles + +The ship's *Attained CII* is then compared to its *Required CII* to produce an easy to understand grade for the ship. The grading scheme is in the range A to E, where A is the most efficient bracket, C represents a ship at-or-near its CII, and E is the least efficient. + +| Grade | Description | +| ----- | ---- | +| A | CII below the *Superior Boundary* | +| B | CII above the *Superior Boundary* and below the *Lower Boundary* | +| C | CII between the *Lower Boundary* and the *Upper Boundary* | +| D | CII above the *Upper Boundary* and below the *Inferior Boundary* | +| E | CII above the *Inferior Boundary* | + +![Graphical representation of IMO's ship boundaries, indicating the CII requirements to attain an A, B, C, D, E grade](../README_assets/imo_boundaries_diagram.png "IMO Shipping Grades Diagram") + +*Fig1.* IMO Boundaries, after [IMO MEPC.354(78)](https://wwwcdn.imo.org/localresources/en/KnowledgeCentre/IndexofIMOResolutions/MEPCDocuments/MEPC.354(78).pdf) + +## Ship Grade Ratio Methodology + +A ship's grade is calculated by comparing its ***Attained CII*** to its ***Required CII*** to give its performance $A/R$ ratio. If the ship's $A/R$ ratio falls below the boundary for its class of [Ship Type](#table-3-mepc33976---ship-grading-boundaries), it attains a higher (better) grade. Boundaries are calculated as: + +> $shipTypeRequiredCII \times exp(d_i)$. + +### Ship Grade Worked example +The worked example below considers a *Bulk Carrier*, with a Deadweight Tonnage below 279,000. Assuming the *Bulk Carrier*'s $required CII$ is: + +> $10g CO_2 / DWT.NM$ + +> **IMPORTANT** +> For some ship types, $GT \times NM$ should be used instead of $DWT \times NM$, see [Table 1](#table-1-mepc35378---shipping-capacity-tables) and [transport work done methodology](#ship-transport-work-methodology) for a comprehensive guide. + +Then the boundaries are calculated with: + +- $10 \times exp(d1)$ +- $10 \times exp(d2)$ +- $10 \times exp(d3)$ +- $10 \times exp(d4)$. + +The $exp(d_i)$ rating boundaries for each ship type can be found in [Table 3](#table-3-mepc33976---ship-grading-boundaries). The resultant boundaries for the *Bulk Carrier* in question are: + +| Boundary Type | Required CII | Boundary's Lower Threshold | +| ------------- | --------------- | --------------- | +| Superior | $= 10 \times exp(d1)$
$= 10 \times 0.86$
$= 8.6$ | $8.6 gCO_2/transportWork$ | +| Lower | $= 10 \times exp(d2)$
$= 10 \times 0.94$
$= 9.4$ | $9.4 gCO_2/transportWork$ | +| Upper | $= 10 \times exp(d3)$
$= 10 \times 1.06$
$= 10.6$ | $10.6 gCO_2/transportWork$ | +| Inferior | $= 10 \times exp(d4)$
$= 10 \times 1.18$
$= 11.8$ | $11.8 gCO_2/transportWork$ | + +Grades are then derived from these boundaries, by comparing the ship's *Attained CII* to the thresholds across a given calendar year: + +| Grade | Higher than | Lower than | Description | +| ----- | :----: | :----: | ---- | +| A | | 8.6 | Below *Superior Boundary* | +| B | 8.6 | 9.4 | Above *Superior Boundary*,
Below *Lower Boundary* | +| C | 9.4 | 10.6 | Above *Lower Boundary*,
Below *Upper Boundary* | +| D | 10.6 | 11.8 | Above *Upper Boundary*,
Below *Inferior Boundary* | +| E | 11.8 | | Above *Inferior Boundary* | + +**Example Results**: + +- If the ship's *Attained CII* was $9gCO_2/ DWT \times NM$, the ship receives a grade `B`, as its *Attained CII* was above the threshold for *Superior Boundary*, but below the threshold for *Lower Boundary*. +- If the ship's *Attained CII* was $11gCO_2/ DWT \times NM$, the ship receives a grade `D`, as its *Attained CII* was above the threshold for *Upper Boundary*, but below the threshold for *Inferior Boundary*. + +--- + +## Ship Attained Carbon Intensity Methodology + +A ship's Attained carbon intensity is calculated by taking the [mass of its aggregate CO2 emissions](#ship-co2-emissions-methodology) in a calendar year, and dividing it by its [transport work done](#ship-transport-work-methodology) in the calendar year. + +> $AttainedCII = massOfCo2Emissions / transportWork$ + +**Method accepts**: +- `mass_of_co2_emissions`, the mass of $CO_2$ emissions in the calendar year + - See [co2 emissions methodology](#ship-co2-emissions-methodology) to calculate +- `transport_work`, the work carried out by the ship in the calendar year + - See [transport work methodology](#ship-transport-work-methodology) to calculate + +**Method Returns**: +- A `float` representing the ship's *Attained Carbon Intensity* + +**Implementation**: + +Returns the quotient of a ship's mass of $CO_2$ emissions and its $transportWork$. + +## Ship transport work methodology + +A ship's transport work is calculated by taking its [capacity](#ship-capacity-methodology) and multiplying it by the distance sailed in nautical miles in the calendar year. + +> $capacity \times distanceSailed$ + +**Method accepts**: +- `capacity` the ship's capacity for cargo or passengers + - See [ship capacity methodology](#ship-capacity-methodology) to calculate +- `distance_sailed` the distance sailed in Nautical Miles in the calendar year + +**Implementation**: + +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. 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**: +- `fuel_type`, an enum derived from [Table 2](#table-2-mepc36479-mass-conversion-between-fuel-consumption-and-co2-emissions)'s *Fuel Type* column +- `fuel_consumption_mass`, a `float` representing the mass of fuel consumed in grams (g) over the given year + +**Method Returns**: +- A `float` representing the $M$ mass of $CO_2$ emitted by the ship across one calendar year + +**Implementation**: + +The sum of $CO_2$ emissions $M$ from fuel consumption in a given calendar year is + +> $M = FC_j \times C_{{f_j}}$ + +Where: +- $j$ is the fuel type +- $FC_j$ is the mass in grams of the consumed fuel type `j` in one calendar year +- $C_{{f_j}}$ is the fuel oil mass to CO2 mass conversion factor, given in Table 2's $C_F$ column + +## Ship Capacity Methodology + +A ship's capacity is measured by either its Deadweight Tonnage (DWT) or Gross Tonnage (GT). The only exception is `Bulk Carriers`, which have a capacity capped at 279,000. + +To calculate a ship's Capacity in accordance with the IMO's MEPC353(78) guidelines: + +**Method accepts**: +- `ship_type`, an enum, derived from *Table 1*'s *Ship Type* column +- `deadweight_tonnage`, the *deadweight tonnage* of the ship, provided in long tons +- `gross_tonnage`, the *gross tonnage* of the ship, provided in long tons + +**Method returns**: +- a `float` representing the ship's capacity in imperial *long tons* + +**Implementation**: + +$Capacity$ of a given ship is calculated using the following rules: + +- If the ship is a `Bulk Carrier`, and its DWT is 279,000 or above, its capacity is capped at 279,000 +- If the ship is a `Bulk Carrier`, and its DWT is below 279,000, its capacity is equal to its DWT +- If the ship is a `Ro-ro cargo ship (vehicle carrier)`, a `Ro-ro passenger ship` or a `Cruise passenger ship`, its capacity is equal to its Gross Tonnage +- Otherwise, the ships capacity is equal to its DWT + +The full implementation detail can be found in **[Table 1](#table-1-mepc35378---shipping-capacity-tables)**'s *Ship Type*, *Ship weight*, and *Capacity* columns. + +**Exceptions**: + +- `ValueError` is raised if the DWT is set to 0, when ship type is set to anything other than `Ro-ro cargo ship (vehicle carrier)`, `Ro-ro passenger ship` or `Cruise passenger ship` +- `ValueError` is raised if the GT is set to 0, when ship type is set to `Ro-ro cargo ship (vehicle carrier)`, `Ro-ro passenger ship` or `Cruise passenger ship` + +--- + +# Reference Tables + +## Table 1: MEPC.353(78) - Shipping Capacity Tables + +The following table describes how to determine a given ship type's *Capacity*. + +*Table Source*: [IMO: MEPC.353(78)](https://wwwcdn.imo.org/localresources/en/KnowledgeCentre/IndexofIMOResolutions/MEPCDocuments/MEPC.353(78).pdf) + +_Same as C# version, see root README for table._ + +--- + +## Table 2: MEPC.364(79) Mass Conversion between fuel consumption and CO2 emissions + +The following table describes how to convert from the fuel used by a ship's main engine $ME_{{(i)}}$ to the amount of $CO_2$ produced. Fuel consumption is measured in grams (g), as is the output $CO_2$ emission + +*Table source*: [IMO: MEPC.364(79)](https://wwwcdn.imo.org/localresources/en/KnowledgeCentre/IndexofIMOResolutions/MEPCDocuments/MEPC.364(79).pdf) + +_Same as C# version, see root README for table._ + +--- + +## Table 3: MEPC.339(76) - Ship Grading Boundaries + +The following table describes the $dd$ vectors used to determine the rating boundaries for ship types. The columns $dd$ $exp(d_i)$ values represent the boundaries the IMO's rating system in the baseline year (2019). + +*Table source (2022)*: [IMO: MEPC.354(78)](https://wwwcdn.imo.org/localresources/en/KnowledgeCentre/IndexofIMOResolutions/MEPCDocuments/MEPC.354(78).pdf) +*Previous source (2021)*: [IMO: MEPC.339(76)](https://wwwcdn.imo.org/localresources/en/OurWork/Environment/Documents/Air%20pollution/MEPC.339(76).pdf) + +_Same as C# version, see root README for table._ + +--- + +## Table 4: Annual Carbon Reduction Factors (Z%) + +The following table describes the reduction factor to be applied to a ship's $requiredCII$ on an annual basis. IMO have +to date released figures up to 2026. In the table, the values from 2027 onwards are **unofficial estimates** based on the pattern to 2026. IMO aims to release +new reduction factors + +*Table Source*: [IMO: MEPC.338(76)](https://wwwcdn.imo.org/localresources/en/OurWork/Environment/Documents/Air%20pollution/MEPC.338(76).pdf) + +_Same as C# version, see root README for table._ + +--- + +## Table 5: Common shipping measurement conversions + +Often in shipping, non-metric measurements are used. Conversions are detailed below + +_Same as C# version, see root README for table._ + +--- + +# Shipping Terminology & Glossary + +_Same as C# version, see root README for table._ + +# References & datasets + +- IMO: MEPC.337(76) - Carbon Intensity Index (CII) spec: https://wwwcdn.imo.org/localresources/en/KnowledgeCentre/IndexofIMOResolutions/MEPCDocuments/MEPC.337(76).pdf +- IMO: MEPC.364(79) - Energy Efficiency Design Index (EEDI) spec: https://wwwcdn.imo.org/localresources/en/KnowledgeCentre/IndexofIMOResolutions/MEPCDocuments/MEPC.364(79).pdf +- IMO: MEPC.339(76) - 2021 Guidelines on the operational carbon intensity rating of ships (CII Rating Guidelines, G4): https://wwwcdn.imo.org/localresources/en/OurWork/Environment/Documents/Air%20pollution/MEPC.339(76).pdf +- IMO: MEPC.339(76) - 2022 Guidelines on the operational carbon intensity rating of ships (CII Rating Guidelines, G4): https://wwwcdn.imo.org/localresources/en/KnowledgeCentre/IndexofIMOResolutions/MEPCDocuments/MEPC.354(78).pdf +- ISO 8217:2017 (Current standard) - Petroleum products, Fuels (class F), Specifications of marine fuels: https://www.iso.org/standard/64247.html +- ISO/FDIS 8217 (Standard under development) - Products from petroleum, synthetic and renewable sources, Fuels (class F), Specifications of marine fuel: https://www.iso.org/standard/80579.html + +## Further Reading + +- IMO's press briefing, including links to the comprehensive guidelines: https://www.imo.org/en/MediaCentre/PressBriefings/pages/CII-and-EEXI-entry-into-force.aspx +- Society of Naval Architecture Students summary of CII Calculations: https://github.com/snascusat/CII-Calculator +- DNV's summary of EEXI and CII requirements: https://www.dnv.com/news/eexi-and-cii-requirements-taking-effect-from-1-january-2023-237817/ + +## Useful datasets (mixed public and private) + +- UNStats (public, non-commercial dataset): https://unstats.un.org/bigdata/task-teams/ttt-dashboards/ +- Dataliastic (private commercial dataset): https://datalastic.com/pricing/ +- Marine Traffic (private commercial dataset): https://servicedocs.marinetraffic.com/ +- Windward.AI (private commercial datasets): https://windward.ai/ diff --git a/python-impl/demo-console-app/README.md b/python-impl/demo-console-app/README.md new file mode 100644 index 0000000..5ddd064 --- /dev/null +++ b/python-impl/demo-console-app/README.md @@ -0,0 +1,6 @@ +# Python Demo Console App for Open-IMO-CII-Calculator + +This directory contains a small Python console application that mirrors the demo functionality of the C# app in `EtiveMor.OpenImoCiiCalculator.DemoConsoleApp`. + +- See `main.py` for entry point. +- Run with: `python main.py` diff --git a/python-impl/demo-console-app/demo-results/.gitignore b/python-impl/demo-console-app/demo-results/.gitignore new file mode 100644 index 0000000..a320ef6 --- /dev/null +++ b/python-impl/demo-console-app/demo-results/.gitignore @@ -0,0 +1,5 @@ +*.json + +!3b6c5150-8e0a-45bf-9094-5edb474d04ee-multi-fuel-demo.json +!35acf7dc-60a3-4a6a-9773-a22c9f5fc699-single-fuel-demo.json +!b3fa068c-1312-4486-8090-ca2f2faf27a3-single-fuel-gas-carrier-demo.json \ No newline at end of file diff --git a/python-impl/demo-console-app/demo-results/.gitkeep b/python-impl/demo-console-app/demo-results/.gitkeep new file mode 100644 index 0000000..f3c0216 --- /dev/null +++ b/python-impl/demo-console-app/demo-results/.gitkeep @@ -0,0 +1,2 @@ +# This directory will contain output JSON files from demo runs. +# Files are named as {uuid}-single-fuel-demo.json or {uuid}-multi-fuel-demo.json diff --git a/python-impl/demo-console-app/demo-results/35acf7dc-60a3-4a6a-9773-a22c9f5fc699-single-fuel-demo.json b/python-impl/demo-console-app/demo-results/35acf7dc-60a3-4a6a-9773-a22c9f5fc699-single-fuel-demo.json new file mode 100644 index 0000000..9b1999e --- /dev/null +++ b/python-impl/demo-console-app/demo-results/35acf7dc-60a3-4a6a-9773-a22c9f5fc699-single-fuel-demo.json @@ -0,0 +1,328 @@ +{ + "results": [ + { + "is_measured_year": true, + "is_estimated_year": false, + "year": 2019, + "required_cii": 19.184190519387734, + "attained_cii": 16.243733333333335, + "attained_required_ratio": 0.8467249799733408, + "rating": 2, + "calculated_co2e_emissions": 60914000000.0, + "calculated_ship_capacity": 25000, + "calculated_transport_work": 3750000000, + "vector_boundaries_for_year": { + "year": 2019, + "ship_type": 110, + "weight_classification": { + "upper_limit": 0, + "lower_limit": Infinity + }, + "capacity_unit": 3, + "boundary_dd_vectors": { + "1": 14.579984794734678, + "2": 17.649455277836715, + "3": 21.869977192102013, + "4": 24.939447675204054 + } + } + }, + { + "is_measured_year": false, + "is_estimated_year": true, + "year": 2020, + "required_cii": 18.992348614193855, + "attained_cii": 16.243733333333335, + "attained_required_ratio": 0.8552777575488293, + "rating": 2, + "calculated_co2e_emissions": 60914000000.0, + "calculated_ship_capacity": 25000, + "calculated_transport_work": 3750000000, + "vector_boundaries_for_year": { + "year": 2020, + "ship_type": 110, + "weight_classification": { + "upper_limit": 0, + "lower_limit": Infinity + }, + "capacity_unit": 3, + "boundary_dd_vectors": { + "1": 14.43418494678733, + "2": 17.472960725058346, + "3": 21.651277420180993, + "4": 24.690053198452013 + } + } + }, + { + "is_measured_year": false, + "is_estimated_year": true, + "year": 2021, + "required_cii": 18.80050670899998, + "attained_cii": 16.243733333333335, + "attained_required_ratio": 0.8640050816054499, + "rating": 2, + "calculated_co2e_emissions": 60914000000.0, + "calculated_ship_capacity": 25000, + "calculated_transport_work": 3750000000, + "vector_boundaries_for_year": { + "year": 2021, + "ship_type": 110, + "weight_classification": { + "upper_limit": 0, + "lower_limit": Infinity + }, + "capacity_unit": 3, + "boundary_dd_vectors": { + "1": 14.288385098839985, + "2": 17.29646617227998, + "3": 21.432577648259976, + "4": 24.440658721699975 + } + } + }, + { + "is_measured_year": false, + "is_estimated_year": true, + "year": 2022, + "required_cii": 18.6086648038061, + "attained_cii": 16.243733333333335, + "attained_required_ratio": 0.8729123504879803, + "rating": 2, + "calculated_co2e_emissions": 60914000000.0, + "calculated_ship_capacity": 25000, + "calculated_transport_work": 3750000000, + "vector_boundaries_for_year": { + "year": 2022, + "ship_type": 110, + "weight_classification": { + "upper_limit": 0, + "lower_limit": Infinity + }, + "capacity_unit": 3, + "boundary_dd_vectors": { + "1": 14.142585250892637, + "2": 17.119971619501612, + "3": 21.21387787633895, + "4": 24.19126424494793 + } + } + }, + { + "is_measured_year": false, + "is_estimated_year": true, + "year": 2023, + "required_cii": 18.224980993418345, + "attained_cii": 16.243733333333335, + "attained_required_ratio": 0.8912894526035168, + "rating": 2, + "calculated_co2e_emissions": 60914000000.0, + "calculated_ship_capacity": 25000, + "calculated_transport_work": 3750000000, + "vector_boundaries_for_year": { + "year": 2023, + "ship_type": 110, + "weight_classification": { + "upper_limit": 0, + "lower_limit": Infinity + }, + "capacity_unit": 3, + "boundary_dd_vectors": { + "1": 13.850985554997942, + "2": 16.76698251394488, + "3": 20.77647833249691, + "4": 23.69247529144385 + } + } + }, + { + "is_measured_year": false, + "is_estimated_year": true, + "year": 2024, + "required_cii": 17.84129718303059, + "attained_cii": 16.243733333333335, + "attained_required_ratio": 0.9104569677132699, + "rating": 2, + "calculated_co2e_emissions": 60914000000.0, + "calculated_ship_capacity": 25000, + "calculated_transport_work": 3750000000, + "vector_boundaries_for_year": { + "year": 2024, + "ship_type": 110, + "weight_classification": { + "upper_limit": 0, + "lower_limit": Infinity + }, + "capacity_unit": 3, + "boundary_dd_vectors": { + "1": 13.55938585910325, + "2": 16.413993408388144, + "3": 20.33907878865487, + "4": 23.19368633793977 + } + } + }, + { + "is_measured_year": false, + "is_estimated_year": true, + "year": 2025, + "required_cii": 17.45761337264284, + "attained_cii": 16.243733333333335, + "attained_required_ratio": 0.9304670109597152, + "rating": 3, + "calculated_co2e_emissions": 60914000000.0, + "calculated_ship_capacity": 25000, + "calculated_transport_work": 3750000000, + "vector_boundaries_for_year": { + "year": 2025, + "ship_type": 110, + "weight_classification": { + "upper_limit": 0, + "lower_limit": Infinity + }, + "capacity_unit": 3, + "boundary_dd_vectors": { + "1": 13.267786163208559, + "2": 16.061004302831414, + "3": 19.901679244812836, + "4": 22.69489738443569 + } + } + }, + { + "is_measured_year": false, + "is_estimated_year": true, + "year": 2026, + "required_cii": 17.073929562255085, + "attained_cii": 16.243733333333335, + "attained_required_ratio": 0.9513763819925177, + "rating": 3, + "calculated_co2e_emissions": 60914000000.0, + "calculated_ship_capacity": 25000, + "calculated_transport_work": 3750000000, + "vector_boundaries_for_year": { + "year": 2026, + "ship_type": 110, + "weight_classification": { + "upper_limit": 0, + "lower_limit": Infinity + }, + "capacity_unit": 3, + "boundary_dd_vectors": { + "1": 12.976186467313864, + "2": 15.708015197274678, + "3": 19.464279700970796, + "4": 22.196108430931613 + } + } + }, + { + "is_measured_year": false, + "is_estimated_year": true, + "year": 2027, + "required_cii": 16.690245751867327, + "attained_cii": 16.243733333333335, + "attained_required_ratio": 0.9732471034176333, + "rating": 3, + "calculated_co2e_emissions": 60914000000.0, + "calculated_ship_capacity": 25000, + "calculated_transport_work": 3750000000, + "vector_boundaries_for_year": { + "year": 2027, + "ship_type": 110, + "weight_classification": { + "upper_limit": 0, + "lower_limit": Infinity + }, + "capacity_unit": 3, + "boundary_dd_vectors": { + "1": 12.684586771419168, + "2": 15.35502609171794, + "3": 19.02688015712875, + "4": 21.697319477427527 + } + } + }, + { + "is_measured_year": false, + "is_estimated_year": true, + "year": 2028, + "required_cii": 16.306561941479572, + "attained_cii": 16.243733333333335, + "attained_required_ratio": 0.996147035262754, + "rating": 3, + "calculated_co2e_emissions": 60914000000.0, + "calculated_ship_capacity": 25000, + "calculated_transport_work": 3750000000, + "vector_boundaries_for_year": { + "year": 2028, + "ship_type": 110, + "weight_classification": { + "upper_limit": 0, + "lower_limit": Infinity + }, + "capacity_unit": 3, + "boundary_dd_vectors": { + "1": 12.392987075524475, + "2": 15.002036986161206, + "3": 18.58948061328671, + "4": 21.198530523923445 + } + } + }, + { + "is_measured_year": false, + "is_estimated_year": true, + "year": 2029, + "required_cii": 15.922878131091819, + "attained_cii": 16.243733333333335, + "attained_required_ratio": 1.0201505782811335, + "rating": 3, + "calculated_co2e_emissions": 60914000000.0, + "calculated_ship_capacity": 25000, + "calculated_transport_work": 3750000000, + "vector_boundaries_for_year": { + "year": 2029, + "ship_type": 110, + "weight_classification": { + "upper_limit": 0, + "lower_limit": Infinity + }, + "capacity_unit": 3, + "boundary_dd_vectors": { + "1": 12.101387379629783, + "2": 14.649047880604474, + "3": 18.152081069444673, + "4": 20.699741570419366 + } + } + }, + { + "is_measured_year": false, + "is_estimated_year": true, + "year": 2030, + "required_cii": 15.539194320704066, + "attained_cii": 16.243733333333335, + "attained_required_ratio": 1.0453394814485688, + "rating": 3, + "calculated_co2e_emissions": 60914000000.0, + "calculated_ship_capacity": 25000, + "calculated_transport_work": 3750000000, + "vector_boundaries_for_year": { + "year": 2030, + "ship_type": 110, + "weight_classification": { + "upper_limit": 0, + "lower_limit": Infinity + }, + "capacity_unit": 3, + "boundary_dd_vectors": { + "1": 11.80978768373509, + "2": 14.296058775047742, + "3": 17.714681525602632, + "4": 20.200952616915288 + } + } + } + ] +} \ No newline at end of file diff --git a/python-impl/demo-console-app/demo-results/3b6c5150-8e0a-45bf-9094-5edb474d04ee-multi-fuel-demo.json b/python-impl/demo-console-app/demo-results/3b6c5150-8e0a-45bf-9094-5edb474d04ee-multi-fuel-demo.json new file mode 100644 index 0000000..7dae491 --- /dev/null +++ b/python-impl/demo-console-app/demo-results/3b6c5150-8e0a-45bf-9094-5edb474d04ee-multi-fuel-demo.json @@ -0,0 +1,328 @@ +{ + "results": [ + { + "is_measured_year": true, + "is_estimated_year": false, + "year": 2019, + "required_cii": 19.184190519387734, + "attained_cii": 19.089333333333332, + "attained_required_ratio": 0.9950554501656697, + "rating": 3, + "calculated_co2e_emissions": 71585000000.0, + "calculated_ship_capacity": 25000, + "calculated_transport_work": 3750000000, + "vector_boundaries_for_year": { + "year": 2019, + "ship_type": 110, + "weight_classification": { + "upper_limit": 0, + "lower_limit": Infinity + }, + "capacity_unit": 3, + "boundary_dd_vectors": { + "1": 14.579984794734678, + "2": 17.649455277836715, + "3": 21.869977192102013, + "4": 24.939447675204054 + } + } + }, + { + "is_measured_year": false, + "is_estimated_year": true, + "year": 2020, + "required_cii": 18.992348614193855, + "attained_cii": 19.089333333333332, + "attained_required_ratio": 1.0051065153188583, + "rating": 3, + "calculated_co2e_emissions": 71585000000.0, + "calculated_ship_capacity": 25000, + "calculated_transport_work": 3750000000, + "vector_boundaries_for_year": { + "year": 2020, + "ship_type": 110, + "weight_classification": { + "upper_limit": 0, + "lower_limit": Infinity + }, + "capacity_unit": 3, + "boundary_dd_vectors": { + "1": 14.43418494678733, + "2": 17.472960725058346, + "3": 21.651277420180993, + "4": 24.690053198452013 + } + } + }, + { + "is_measured_year": false, + "is_estimated_year": true, + "year": 2021, + "required_cii": 18.80050670899998, + "attained_cii": 19.089333333333332, + "attained_required_ratio": 1.0153627042506832, + "rating": 3, + "calculated_co2e_emissions": 71585000000.0, + "calculated_ship_capacity": 25000, + "calculated_transport_work": 3750000000, + "vector_boundaries_for_year": { + "year": 2021, + "ship_type": 110, + "weight_classification": { + "upper_limit": 0, + "lower_limit": Infinity + }, + "capacity_unit": 3, + "boundary_dd_vectors": { + "1": 14.288385098839985, + "2": 17.29646617227998, + "3": 21.432577648259976, + "4": 24.440658721699975 + } + } + }, + { + "is_measured_year": false, + "is_estimated_year": true, + "year": 2022, + "required_cii": 18.6086648038061, + "attained_cii": 19.089333333333332, + "attained_required_ratio": 1.0258303609955357, + "rating": 3, + "calculated_co2e_emissions": 71585000000.0, + "calculated_ship_capacity": 25000, + "calculated_transport_work": 3750000000, + "vector_boundaries_for_year": { + "year": 2022, + "ship_type": 110, + "weight_classification": { + "upper_limit": 0, + "lower_limit": Infinity + }, + "capacity_unit": 3, + "boundary_dd_vectors": { + "1": 14.142585250892637, + "2": 17.119971619501612, + "3": 21.21387787633895, + "4": 24.19126424494793 + } + } + }, + { + "is_measured_year": false, + "is_estimated_year": true, + "year": 2023, + "required_cii": 18.224980993418345, + "attained_cii": 19.089333333333332, + "attained_required_ratio": 1.0474267896480733, + "rating": 3, + "calculated_co2e_emissions": 71585000000.0, + "calculated_ship_capacity": 25000, + "calculated_transport_work": 3750000000, + "vector_boundaries_for_year": { + "year": 2023, + "ship_type": 110, + "weight_classification": { + "upper_limit": 0, + "lower_limit": Infinity + }, + "capacity_unit": 3, + "boundary_dd_vectors": { + "1": 13.850985554997942, + "2": 16.76698251394488, + "3": 20.77647833249691, + "4": 23.69247529144385 + } + } + }, + { + "is_measured_year": false, + "is_estimated_year": true, + "year": 2024, + "required_cii": 17.84129718303059, + "attained_cii": 19.089333333333332, + "attained_required_ratio": 1.069952096952333, + "rating": 3, + "calculated_co2e_emissions": 71585000000.0, + "calculated_ship_capacity": 25000, + "calculated_transport_work": 3750000000, + "vector_boundaries_for_year": { + "year": 2024, + "ship_type": 110, + "weight_classification": { + "upper_limit": 0, + "lower_limit": Infinity + }, + "capacity_unit": 3, + "boundary_dd_vectors": { + "1": 13.55938585910325, + "2": 16.413993408388144, + "3": 20.33907878865487, + "4": 23.19368633793977 + } + } + }, + { + "is_measured_year": false, + "is_estimated_year": true, + "year": 2025, + "required_cii": 17.45761337264284, + "attained_cii": 19.089333333333332, + "attained_required_ratio": 1.0934675276545818, + "rating": 3, + "calculated_co2e_emissions": 71585000000.0, + "calculated_ship_capacity": 25000, + "calculated_transport_work": 3750000000, + "vector_boundaries_for_year": { + "year": 2025, + "ship_type": 110, + "weight_classification": { + "upper_limit": 0, + "lower_limit": Infinity + }, + "capacity_unit": 3, + "boundary_dd_vectors": { + "1": 13.267786163208559, + "2": 16.061004302831414, + "3": 19.901679244812836, + "4": 22.69489738443569 + } + } + }, + { + "is_measured_year": false, + "is_estimated_year": true, + "year": 2026, + "required_cii": 17.073929562255085, + "attained_cii": 19.089333333333332, + "attained_required_ratio": 1.1180398316468196, + "rating": 3, + "calculated_co2e_emissions": 71585000000.0, + "calculated_ship_capacity": 25000, + "calculated_transport_work": 3750000000, + "vector_boundaries_for_year": { + "year": 2026, + "ship_type": 110, + "weight_classification": { + "upper_limit": 0, + "lower_limit": Infinity + }, + "capacity_unit": 3, + "boundary_dd_vectors": { + "1": 12.976186467313864, + "2": 15.708015197274678, + "3": 19.464279700970796, + "4": 22.196108430931613 + } + } + }, + { + "is_measured_year": false, + "is_estimated_year": true, + "year": 2027, + "required_cii": 16.690245751867327, + "attained_cii": 19.089333333333332, + "attained_required_ratio": 1.143741896742149, + "rating": 4, + "calculated_co2e_emissions": 71585000000.0, + "calculated_ship_capacity": 25000, + "calculated_transport_work": 3750000000, + "vector_boundaries_for_year": { + "year": 2027, + "ship_type": 110, + "weight_classification": { + "upper_limit": 0, + "lower_limit": Infinity + }, + "capacity_unit": 3, + "boundary_dd_vectors": { + "1": 12.684586771419168, + "2": 15.35502609171794, + "3": 19.02688015712875, + "4": 21.697319477427527 + } + } + }, + { + "is_measured_year": false, + "is_estimated_year": true, + "year": 2028, + "required_cii": 16.306561941479572, + "attained_cii": 19.089333333333332, + "attained_required_ratio": 1.1706534707831409, + "rating": 4, + "calculated_co2e_emissions": 71585000000.0, + "calculated_ship_capacity": 25000, + "calculated_transport_work": 3750000000, + "vector_boundaries_for_year": { + "year": 2028, + "ship_type": 110, + "weight_classification": { + "upper_limit": 0, + "lower_limit": Infinity + }, + "capacity_unit": 3, + "boundary_dd_vectors": { + "1": 12.392987075524475, + "2": 15.002036986161206, + "3": 18.58948061328671, + "4": 21.198530523923445 + } + } + }, + { + "is_measured_year": false, + "is_estimated_year": true, + "year": 2029, + "required_cii": 15.922878131091819, + "attained_cii": 19.089333333333332, + "attained_required_ratio": 1.198861988151409, + "rating": 4, + "calculated_co2e_emissions": 71585000000.0, + "calculated_ship_capacity": 25000, + "calculated_transport_work": 3750000000, + "vector_boundaries_for_year": { + "year": 2029, + "ship_type": 110, + "weight_classification": { + "upper_limit": 0, + "lower_limit": Infinity + }, + "capacity_unit": 3, + "boundary_dd_vectors": { + "1": 12.101387379629783, + "2": 14.649047880604474, + "3": 18.152081069444673, + "4": 20.699741570419366 + } + } + }, + { + "is_measured_year": false, + "is_estimated_year": true, + "year": 2030, + "required_cii": 15.539194320704066, + "attained_cii": 19.089333333333332, + "attained_required_ratio": 1.2284635187230488, + "rating": 4, + "calculated_co2e_emissions": 71585000000.0, + "calculated_ship_capacity": 25000, + "calculated_transport_work": 3750000000, + "vector_boundaries_for_year": { + "year": 2030, + "ship_type": 110, + "weight_classification": { + "upper_limit": 0, + "lower_limit": Infinity + }, + "capacity_unit": 3, + "boundary_dd_vectors": { + "1": 11.80978768373509, + "2": 14.296058775047742, + "3": 17.714681525602632, + "4": 20.200952616915288 + } + } + } + ] +} \ No newline at end of file diff --git a/python-impl/demo-console-app/demo-results/b3fa068c-1312-4486-8090-ca2f2faf27a3-single-fuel-gas-carrier-demo.json b/python-impl/demo-console-app/demo-results/b3fa068c-1312-4486-8090-ca2f2faf27a3-single-fuel-gas-carrier-demo.json new file mode 100644 index 0000000..dcd9b24 --- /dev/null +++ b/python-impl/demo-console-app/demo-results/b3fa068c-1312-4486-8090-ca2f2faf27a3-single-fuel-gas-carrier-demo.json @@ -0,0 +1,328 @@ +{ + "results": [ + { + "is_measured_year": true, + "is_estimated_year": false, + "year": 2019, + "required_cii": 7.169036365047443, + "attained_cii": 3.0555555555555554, + "attained_required_ratio": 0.4262156585580849, + "rating": 1, + "calculated_co2e_emissions": 27500000000.0, + "calculated_ship_capacity": 60000, + "calculated_transport_work": 9000000000, + "vector_boundaries_for_year": { + "year": 2019, + "ship_type": 20, + "weight_classification": { + "upper_limit": 0, + "lower_limit": 64999 + }, + "capacity_unit": 1, + "boundary_dd_vectors": { + "1": 6.093680910290327, + "2": 6.810584546795071, + "3": 7.59917854695029, + "4": 8.961295456309305 + } + } + }, + { + "is_measured_year": false, + "is_estimated_year": true, + "year": 2020, + "required_cii": 7.097346001396969, + "attained_cii": 3.0555555555555554, + "attained_required_ratio": 0.43052086723038885, + "rating": 1, + "calculated_co2e_emissions": 27500000000.0, + "calculated_ship_capacity": 60000, + "calculated_transport_work": 9000000000, + "vector_boundaries_for_year": { + "year": 2020, + "ship_type": 20, + "weight_classification": { + "upper_limit": 0, + "lower_limit": 64999 + }, + "capacity_unit": 1, + "boundary_dd_vectors": { + "1": 6.032744101187423, + "2": 6.74247870132712, + "3": 7.523186761480787, + "4": 8.87168250174621 + } + } + }, + { + "is_measured_year": false, + "is_estimated_year": true, + "year": 2021, + "required_cii": 7.025655637746494, + "attained_cii": 3.0555555555555554, + "attained_required_ratio": 0.4349139373041683, + "rating": 1, + "calculated_co2e_emissions": 27500000000.0, + "calculated_ship_capacity": 60000, + "calculated_transport_work": 9000000000, + "vector_boundaries_for_year": { + "year": 2021, + "ship_type": 20, + "weight_classification": { + "upper_limit": 0, + "lower_limit": 64999 + }, + "capacity_unit": 1, + "boundary_dd_vectors": { + "1": 5.97180729208452, + "2": 6.674372855859169, + "3": 7.447194976011284, + "4": 8.782069547183118 + } + } + }, + { + "is_measured_year": false, + "is_estimated_year": true, + "year": 2022, + "required_cii": 6.95396527409602, + "attained_cii": 3.0555555555555554, + "attained_required_ratio": 0.4393975861423556, + "rating": 1, + "calculated_co2e_emissions": 27500000000.0, + "calculated_ship_capacity": 60000, + "calculated_transport_work": 9000000000, + "vector_boundaries_for_year": { + "year": 2022, + "ship_type": 20, + "weight_classification": { + "upper_limit": 0, + "lower_limit": 64999 + }, + "capacity_unit": 1, + "boundary_dd_vectors": { + "1": 5.9108704829816165, + "2": 6.606267010391218, + "3": 7.371203190541781, + "4": 8.692456592620024 + } + } + }, + { + "is_measured_year": false, + "is_estimated_year": true, + "year": 2023, + "required_cii": 6.810584546795071, + "attained_cii": 3.0555555555555554, + "attained_required_ratio": 0.4486480616400894, + "rating": 1, + "calculated_co2e_emissions": 27500000000.0, + "calculated_ship_capacity": 60000, + "calculated_transport_work": 9000000000, + "vector_boundaries_for_year": { + "year": 2023, + "ship_type": 20, + "weight_classification": { + "upper_limit": 0, + "lower_limit": 64999 + }, + "capacity_unit": 1, + "boundary_dd_vectors": { + "1": 5.78899686477581, + "2": 6.470055319455317, + "3": 7.219219619602776, + "4": 8.51323068349384 + } + } + }, + { + "is_measured_year": false, + "is_estimated_year": true, + "year": 2024, + "required_cii": 6.667203819494121, + "attained_cii": 3.0555555555555554, + "attained_required_ratio": 0.45829640705170427, + "rating": 1, + "calculated_co2e_emissions": 27500000000.0, + "calculated_ship_capacity": 60000, + "calculated_transport_work": 9000000000, + "vector_boundaries_for_year": { + "year": 2024, + "ship_type": 20, + "weight_classification": { + "upper_limit": 0, + "lower_limit": 64999 + }, + "capacity_unit": 1, + "boundary_dd_vectors": { + "1": 5.667123246570003, + "2": 6.333843628519415, + "3": 7.067236048663768, + "4": 8.334004774367651 + } + } + }, + { + "is_measured_year": false, + "is_estimated_year": true, + "year": 2025, + "required_cii": 6.523823092193173, + "attained_cii": 3.0555555555555554, + "attained_required_ratio": 0.4683688555583351, + "rating": 1, + "calculated_co2e_emissions": 27500000000.0, + "calculated_ship_capacity": 60000, + "calculated_transport_work": 9000000000, + "vector_boundaries_for_year": { + "year": 2025, + "ship_type": 20, + "weight_classification": { + "upper_limit": 0, + "lower_limit": 64999 + }, + "capacity_unit": 1, + "boundary_dd_vectors": { + "1": 5.545249628364197, + "2": 6.197631937583514, + "3": 6.915252477724764, + "4": 8.154778865241466 + } + } + }, + { + "is_measured_year": false, + "is_estimated_year": true, + "year": 2026, + "required_cii": 6.380442364892224, + "attained_cii": 3.0555555555555554, + "attained_required_ratio": 0.47889399837987073, + "rating": 1, + "calculated_co2e_emissions": 27500000000.0, + "calculated_ship_capacity": 60000, + "calculated_transport_work": 9000000000, + "vector_boundaries_for_year": { + "year": 2026, + "ship_type": 20, + "weight_classification": { + "upper_limit": 0, + "lower_limit": 64999 + }, + "capacity_unit": 1, + "boundary_dd_vectors": { + "1": 5.423376010158391, + "2": 6.061420246647613, + "3": 6.7632689067857585, + "4": 7.975552956115281 + } + } + }, + { + "is_measured_year": false, + "is_estimated_year": true, + "year": 2027, + "required_cii": 6.2370616375912755, + "attained_cii": 3.0555555555555554, + "attained_required_ratio": 0.4899030558138907, + "rating": 1, + "calculated_co2e_emissions": 27500000000.0, + "calculated_ship_capacity": 60000, + "calculated_transport_work": 9000000000, + "vector_boundaries_for_year": { + "year": 2027, + "ship_type": 20, + "weight_classification": { + "upper_limit": 0, + "lower_limit": 64999 + }, + "capacity_unit": 1, + "boundary_dd_vectors": { + "1": 5.301502391952584, + "2": 5.925208555711712, + "3": 6.611285335846753, + "4": 7.796327046989094 + } + } + }, + { + "is_measured_year": false, + "is_estimated_year": true, + "year": 2028, + "required_cii": 6.093680910290327, + "attained_cii": 3.0555555555555554, + "attained_required_ratio": 0.5014301865389235, + "rating": 1, + "calculated_co2e_emissions": 27500000000.0, + "calculated_ship_capacity": 60000, + "calculated_transport_work": 9000000000, + "vector_boundaries_for_year": { + "year": 2028, + "ship_type": 20, + "weight_classification": { + "upper_limit": 0, + "lower_limit": 64999 + }, + "capacity_unit": 1, + "boundary_dd_vectors": { + "1": 5.179628773746778, + "2": 5.78899686477581, + "3": 6.459301764907747, + "4": 7.6171011378629085 + } + } + }, + { + "is_measured_year": false, + "is_estimated_year": true, + "year": 2029, + "required_cii": 5.950300182989378, + "attained_cii": 3.0555555555555554, + "attained_required_ratio": 0.5135128416362469, + "rating": 1, + "calculated_co2e_emissions": 27500000000.0, + "calculated_ship_capacity": 60000, + "calculated_transport_work": 9000000000, + "vector_boundaries_for_year": { + "year": 2029, + "ship_type": 20, + "weight_classification": { + "upper_limit": 0, + "lower_limit": 64999 + }, + "capacity_unit": 1, + "boundary_dd_vectors": { + "1": 5.057755155540971, + "2": 5.652785173839908, + "3": 6.307318193968741, + "4": 7.437875228736722 + } + } + }, + { + "is_measured_year": false, + "is_estimated_year": true, + "year": 2030, + "required_cii": 5.806919455688429, + "attained_cii": 3.0555555555555554, + "attained_required_ratio": 0.5261921710593641, + "rating": 1, + "calculated_co2e_emissions": 27500000000.0, + "calculated_ship_capacity": 60000, + "calculated_transport_work": 9000000000, + "vector_boundaries_for_year": { + "year": 2030, + "ship_type": 20, + "weight_classification": { + "upper_limit": 0, + "lower_limit": 64999 + }, + "capacity_unit": 1, + "boundary_dd_vectors": { + "1": 4.935881537335164, + "2": 5.516573482904008, + "3": 6.155334623029735, + "4": 7.258649319610536 + } + } + } + ] +} \ No newline at end of file diff --git a/python-impl/demo-console-app/main.py b/python-impl/demo-console-app/main.py new file mode 100644 index 0000000..2e3624c --- /dev/null +++ b/python-impl/demo-console-app/main.py @@ -0,0 +1,12 @@ +from single_fuel_demo import single_fuel_demo +from multi_fuel_demo import multi_fuel_demo +from single_fuel_gas_carrier_demo import single_fuel_gas_carrier_demo + + +def main(): + multi_fuel_demo() + single_fuel_demo() + single_fuel_gas_carrier_demo() + +if __name__ == "__main__": + main() diff --git a/python-impl/demo-console-app/multi_fuel_demo.py b/python-impl/demo-console-app/multi_fuel_demo.py new file mode 100644 index 0000000..01a7664 --- /dev/null +++ b/python-impl/demo-console-app/multi_fuel_demo.py @@ -0,0 +1,71 @@ +import json +import os +import uuid +from open_imo_cii_calculator.ship_carbon_intensity_calculator import ShipCarbonIntensityCalculator +from open_imo_cii_calculator.models.fuel_type import TypeOfFuel +from open_imo_cii_calculator.models.ship_type import ShipType +from open_imo_cii_calculator.models.dto.fuel_type_consumption import FuelTypeConsumption + +def ship_dd_vector_boundaries_to_dict(vb): + return { + "year": vb.year, + "ship_type": vb.ship_type, + "weight_classification": { + "upper_limit": vb.weight_classification.upper_limit, + "lower_limit": vb.weight_classification.lower_limit + } if vb.weight_classification else None, + "capacity_unit": vb.capacity_unit, + "boundary_dd_vectors": vb.boundary_dd_vectors + } + +def result_year_to_dict(ry): + return { + "is_measured_year": ry.is_measured_year, + "is_estimated_year": ry.is_estimated_year, + "year": ry.year, + "required_cii": ry.required_cii, + "attained_cii": ry.attained_cii, + "attained_required_ratio": ry.attained_required_ratio, + "rating": int(ry.rating) if hasattr(ry.rating, 'value') else ry.rating, + "calculated_co2e_emissions": ry.calculated_co2e_emissions, + "calculated_ship_capacity": ry.calculated_ship_capacity, + "calculated_transport_work": ry.calculated_transport_work, + "vector_boundaries_for_year": ship_dd_vector_boundaries_to_dict(ry.vector_boundaries_for_year) if ry.vector_boundaries_for_year else None + } + +def calculation_result_to_dict(result): + return { + "results": [result_year_to_dict(ry) for ry in result.results] + } + +def multi_fuel_demo(): + print("---------------------") + print("Generating a multi-fuel ship report...") + print("---------------------") + + calculator = ShipCarbonIntensityCalculator() + fuel_consumption_diesel_megatons = 12_500 + fuel_consumption_lightfuel_megatons = 10_000 + result = calculator.calculate_attained_cii_rating( + ship_type=ShipType.RORO_PASSENGER_SHIP, + gross_tonnage=25_000, + deadweight_tonnage=0, + distance_travelled=150_000, + fuel_type_consumptions=[ + FuelTypeConsumption(TypeOfFuel.DIESEL_OR_GASOIL, fuel_consumption_diesel_megatons * 1_000_000), + FuelTypeConsumption(TypeOfFuel.LIGHT_FUEL_OIL, fuel_consumption_lightfuel_megatons * 1_000_000) + ], + target_year=2019 + ) + result_dict = calculation_result_to_dict(result) + print(json.dumps(result_dict, indent=2, default=str)) + # Write to file + os.makedirs("demo-results", exist_ok=True) + run_id = str(uuid.uuid4()) + out_path = os.path.join("demo-results", f"{run_id}-multi-fuel-demo.json") + with open(out_path, "w") as f: + json.dump(result_dict, f, indent=2, default=str) + print(f"Result written to {out_path}") + print("---------------------") + print("Completed the multi-fuel ship report...") + print("---------------------") diff --git a/python-impl/demo-console-app/single_fuel_demo.py b/python-impl/demo-console-app/single_fuel_demo.py new file mode 100644 index 0000000..ebdb2d5 --- /dev/null +++ b/python-impl/demo-console-app/single_fuel_demo.py @@ -0,0 +1,71 @@ +import json +import os +import uuid +from open_imo_cii_calculator.ship_carbon_intensity_calculator import ShipCarbonIntensityCalculator +from open_imo_cii_calculator.models.fuel_type import TypeOfFuel +from open_imo_cii_calculator.models.ship_type import ShipType +from open_imo_cii_calculator.models.dto.fuel_type_consumption import FuelTypeConsumption + + +# Utility to convert CalculationResult and ResultYear to dict for JSON serialization + +def ship_dd_vector_boundaries_to_dict(vb): + return { + "year": vb.year, + "ship_type": vb.ship_type, + "weight_classification": { + "upper_limit": vb.weight_classification.upper_limit, + "lower_limit": vb.weight_classification.lower_limit + } if vb.weight_classification else None, + "capacity_unit": vb.capacity_unit, + "boundary_dd_vectors": vb.boundary_dd_vectors + } + +def result_year_to_dict(ry): + return { + "is_measured_year": ry.is_measured_year, + "is_estimated_year": ry.is_estimated_year, + "year": ry.year, + "required_cii": ry.required_cii, + "attained_cii": ry.attained_cii, + "attained_required_ratio": ry.attained_required_ratio, + "rating": int(ry.rating) if hasattr(ry.rating, 'value') else ry.rating, + "calculated_co2e_emissions": ry.calculated_co2e_emissions, + "calculated_ship_capacity": ry.calculated_ship_capacity, + "calculated_transport_work": ry.calculated_transport_work, + "vector_boundaries_for_year": ship_dd_vector_boundaries_to_dict(ry.vector_boundaries_for_year) if ry.vector_boundaries_for_year else None + } + +def calculation_result_to_dict(result): + return { + "results": [result_year_to_dict(ry) for ry in result.results] + } + + +def single_fuel_demo(): + print("---------------------") + print("Generating a single-fuel ship report...") + print("---------------------") + + calculator = ShipCarbonIntensityCalculator() + fuel_consumption_megatons = 19_000 + result = calculator.calculate_attained_cii_rating( + ship_type=ShipType.RORO_PASSENGER_SHIP, + gross_tonnage=25_000, + deadweight_tonnage=0, + distance_travelled=150_000, + fuel_type_consumptions=[FuelTypeConsumption(TypeOfFuel.DIESEL_OR_GASOIL, fuel_consumption_megatons * 1_000_000)], + target_year=2019 + ) + result_dict = calculation_result_to_dict(result) + print(json.dumps(result_dict, indent=2, default=str)) + # Write to file + os.makedirs("demo-results", exist_ok=True) + run_id = str(uuid.uuid4()) + out_path = os.path.join("demo-results", f"{run_id}-single-fuel-demo.json") + with open(out_path, "w") as f: + json.dump(result_dict, f, indent=2, default=str) + print(f"Result written to {out_path}") + print("---------------------") + print("Completed the single-fuel ship report...") + print("---------------------") diff --git a/python-impl/demo-console-app/single_fuel_gas_carrier_demo.py b/python-impl/demo-console-app/single_fuel_gas_carrier_demo.py new file mode 100644 index 0000000..d31c11d --- /dev/null +++ b/python-impl/demo-console-app/single_fuel_gas_carrier_demo.py @@ -0,0 +1,73 @@ +import json +import os +import uuid +from open_imo_cii_calculator.ship_carbon_intensity_calculator import ShipCarbonIntensityCalculator +from open_imo_cii_calculator.models.fuel_type import TypeOfFuel +from open_imo_cii_calculator.models.ship_type import ShipType +from open_imo_cii_calculator.models.dto.fuel_type_consumption import FuelTypeConsumption + +def ship_dd_vector_boundaries_to_dict(vb): + return { + "year": vb.year, + "ship_type": vb.ship_type, + "weight_classification": { + "upper_limit": vb.weight_classification.upper_limit, + "lower_limit": vb.weight_classification.lower_limit + } if vb.weight_classification else None, + "capacity_unit": vb.capacity_unit, + "boundary_dd_vectors": vb.boundary_dd_vectors + } + +def result_year_to_dict(ry): + return { + "is_measured_year": ry.is_measured_year, + "is_estimated_year": ry.is_estimated_year, + "year": ry.year, + "required_cii": ry.required_cii, + "attained_cii": ry.attained_cii, + "attained_required_ratio": ry.attained_required_ratio, + "rating": int(ry.rating) if hasattr(ry.rating, 'value') else ry.rating, + "calculated_co2e_emissions": ry.calculated_co2e_emissions, + "calculated_ship_capacity": ry.calculated_ship_capacity, + "calculated_transport_work": ry.calculated_transport_work, + "vector_boundaries_for_year": ship_dd_vector_boundaries_to_dict(ry.vector_boundaries_for_year) if ry.vector_boundaries_for_year else None + } + +def calculation_result_to_dict(result): + return { + "results": [result_year_to_dict(ry) for ry in result.results] + } + +def single_fuel_gas_carrier_demo(): + print("---------------------") + print("Generating a single-fuel Gas Carrier ship report...") + print("---------------------") + + calculator = ShipCarbonIntensityCalculator() + # Sample values for a Gas Carrier (DWT < 65,000) + ship_type = ShipType.GAS_CARRIER + gross_tonnage = 0 + deadweight_tonnage = 60000 # less than 65,000 DWT + distance_travelled = 150_000 + fuel_type = TypeOfFuel.LIQUIFIED_NATURAL_GAS + fuel_consumption_megatons = 10_000 + result = calculator.calculate_attained_cii_rating( + ship_type=ship_type, + gross_tonnage=gross_tonnage, + deadweight_tonnage=deadweight_tonnage, + distance_travelled=distance_travelled, + fuel_type_consumptions=[FuelTypeConsumption(fuel_type, fuel_consumption_megatons * 1_000_000)], + target_year=2019 + ) + result_dict = calculation_result_to_dict(result) + print(json.dumps(result_dict, indent=2, default=str)) + # Write to file + os.makedirs("demo-results", exist_ok=True) + run_id = str(uuid.uuid4()) + out_path = os.path.join("demo-results", f"{run_id}-single-fuel-gas-carrier-demo.json") + with open(out_path, "w") as f: + json.dump(result_dict, f, indent=2, default=str) + print(f"Result written to {out_path}") + print("---------------------") + print("Completed the single-fuel Gas Carrier ship report...") + print("---------------------") diff --git a/python-impl/open_imo_cii_calculator/__init__.py b/python-impl/open_imo_cii_calculator/__init__.py new file mode 100644 index 0000000..c43898f --- /dev/null +++ b/python-impl/open_imo_cii_calculator/__init__.py @@ -0,0 +1,6 @@ +""" +Open IMO CII Calculator - A Python implementation of the IMO's Carbon Intensity Indicator (CII) calculations +""" +from open_imo_cii_calculator.ship_carbon_intensity_calculator import ShipCarbonIntensityCalculator + +__all__ = ['ShipCarbonIntensityCalculator'] \ No newline at end of file diff --git a/python-impl/open_imo_cii_calculator/models/__init__.py b/python-impl/open_imo_cii_calculator/models/__init__.py new file mode 100644 index 0000000..8964d25 --- /dev/null +++ b/python-impl/open_imo_cii_calculator/models/__init__.py @@ -0,0 +1,3 @@ +""" +This module initializes the models package. +""" \ No newline at end of file diff --git a/python-impl/open_imo_cii_calculator/models/capacity_unit.py b/python-impl/open_imo_cii_calculator/models/capacity_unit.py new file mode 100644 index 0000000..8f78df6 --- /dev/null +++ b/python-impl/open_imo_cii_calculator/models/capacity_unit.py @@ -0,0 +1,11 @@ +from enum import IntEnum + +class CapacityUnit(IntEnum): + """ + An enum describing the units of capacity measurement for ships + """ + ERR = 0 + DWT = 1 + DWT_CAP_HIGH = 2 + GT = 3 + GT_CAP_LOW = 4 \ No newline at end of file diff --git a/python-impl/open_imo_cii_calculator/models/dto/annual_consumption.py b/python-impl/open_imo_cii_calculator/models/dto/annual_consumption.py new file mode 100644 index 0000000..ff023ac --- /dev/null +++ b/python-impl/open_imo_cii_calculator/models/dto/annual_consumption.py @@ -0,0 +1,18 @@ +from typing import List +from open_imo_cii_calculator.models.dto.fuel_type_consumption import FuelTypeConsumption + +class AnnualConsumption: + """ + A request object to hold metadata about the annual consumption of a ship + """ + + def __init__(self, target_year: int, fuel_consumption: List[FuelTypeConsumption]): + """ + Initialize an annual consumption instance + + Args: + - target_year: The year this consumption measures + - fuel_consumption: The total fuel consumption in the given year + """ + self.target_year = target_year + self.fuel_consumption = fuel_consumption \ No newline at end of file diff --git a/python-impl/open_imo_cii_calculator/models/dto/calculation_result.py b/python-impl/open_imo_cii_calculator/models/dto/calculation_result.py new file mode 100644 index 0000000..2bc30b6 --- /dev/null +++ b/python-impl/open_imo_cii_calculator/models/dto/calculation_result.py @@ -0,0 +1,16 @@ +from typing import List +from open_imo_cii_calculator.models.dto.result_year import ResultYear + +class CalculationResult: + """ + Contains results of CII calculations for multiple years + """ + + def __init__(self, results: List[ResultYear]): + """ + Initialize a calculation result instance + + Args: + - results: Contains a collection of CII Ratings for each year between 2019 and 2030 + """ + self.results = results \ No newline at end of file diff --git a/python-impl/open_imo_cii_calculator/models/dto/fuel_type_consumption.py b/python-impl/open_imo_cii_calculator/models/dto/fuel_type_consumption.py new file mode 100644 index 0000000..8b28cad --- /dev/null +++ b/python-impl/open_imo_cii_calculator/models/dto/fuel_type_consumption.py @@ -0,0 +1,17 @@ +from open_imo_cii_calculator.models.fuel_type import TypeOfFuel + +class FuelTypeConsumption: + """ + A request object to hold metadata about the fuel consumption of a ship + """ + + def __init__(self, fuel_type: TypeOfFuel, fuel_consumption: float): + """ + Initialize a fuel type consumption instance + + Args: + - fuel_type: The type of fuel consumed by the ship + - fuel_consumption: The amount of fuel consumed by the ship (in grams) + """ + self.fuel_type = fuel_type + self.fuel_consumption = fuel_consumption \ No newline at end of file diff --git a/python-impl/open_imo_cii_calculator/models/dto/result_year.py b/python-impl/open_imo_cii_calculator/models/dto/result_year.py new file mode 100644 index 0000000..2eca24f --- /dev/null +++ b/python-impl/open_imo_cii_calculator/models/dto/result_year.py @@ -0,0 +1,64 @@ +from typing import List, Optional +from open_imo_cii_calculator.models.imo_cii_rating import ImoCiiRating +from open_imo_cii_calculator.models.measurement_models.ship_dd_vector_boundaries import ShipDdVectorBoundaries + +class ResultYear: + """ + Results for a specific year's CII calculation + """ + + def __init__(self, + is_measured_year: bool = False, + year: int = 0, + required_cii: float = 0.0, + attained_cii: float = 0.0, + rating: ImoCiiRating = ImoCiiRating.UNKNOWN, + vector_boundaries_for_year: Optional[ShipDdVectorBoundaries] = None, + calculated_co2e_emissions: float = 0.0, + calculated_ship_capacity: float = 0.0, + calculated_transport_work: float = 0.0): + """ + Initialize a result year instance + + Args: + - is_measured_year: Indicates if this year is a measured year. If true, all values are measured; if false, all values are estimates + - year: The year this result references + - required_cii: The ship's required carbon intensity for this year + - attained_cii: The ship's attained Carbon Intensity Indicator for this year + - rating: The ship's IMO CII Rating, from A to E + - vector_boundaries_for_year: The VectorBoundaries for this ship/year + - calculated_co2e_emissions: The Co2e Emissions calculated for this year + - calculated_ship_capacity: The Ship Capacity calculated for this year + - calculated_transport_work: The Transport Work calculated for this year + """ + self.is_measured_year = is_measured_year + self.year = year + self.required_cii = required_cii + self.attained_cii = attained_cii + self.rating = rating + self.vector_boundaries_for_year = vector_boundaries_for_year + self.calculated_co2e_emissions = calculated_co2e_emissions + self.calculated_ship_capacity = calculated_ship_capacity + self.calculated_transport_work = calculated_transport_work + + @property + def is_estimated_year(self) -> bool: + """ + Indicates if this year is an estimated year + + Returns: + - True if the year is estimated, False otherwise + """ + return not self.is_measured_year + + @property + def attained_required_ratio(self) -> float: + """ + The ratio of Attained:Required CII + + Returns: + - The ratio of Attained to Required CII + """ + if self.required_cii > 0: + return self.attained_cii / self.required_cii + return 0.0 \ No newline at end of file diff --git a/python-impl/open_imo_cii_calculator/models/fuel_type.py b/python-impl/open_imo_cii_calculator/models/fuel_type.py new file mode 100644 index 0000000..f926ea8 --- /dev/null +++ b/python-impl/open_imo_cii_calculator/models/fuel_type.py @@ -0,0 +1,18 @@ +from enum import IntEnum + +class TypeOfFuel(IntEnum): + """ + An enum describing the possible types of fuel used by ships + considered by the IMO's Carbon Intensity Indicator (CII) rating system + """ + UNKNOWN = 0 + DIESEL_OR_GASOIL = 10 + LIGHT_FUEL_OIL = 20 + HEAVY_FUEL_OIL = 30 + LIQUIFIED_PETROLEUM_PROPANE = 40 + LIQUIFIED_PETROLEUM_BUTANE = 50 + ETHANE = 60 + LIQUIFIED_NATURAL_GAS = 70 + METHANOL = 80 + ETHANOL = 90 + OTHER = 100 \ No newline at end of file diff --git a/python-impl/open_imo_cii_calculator/models/imo_cii_boundary.py b/python-impl/open_imo_cii_calculator/models/imo_cii_boundary.py new file mode 100644 index 0000000..a714ca4 --- /dev/null +++ b/python-impl/open_imo_cii_calculator/models/imo_cii_boundary.py @@ -0,0 +1,11 @@ +from enum import IntEnum + +class ImoCiiBoundary(IntEnum): + """ + An enum describing the possible IMO Carbon Intensity Indicator (CII) boundaries + used to determine ship ratings + """ + SUPERIOR = 1 + LOWER = 2 + UPPER = 3 + INFERIOR = 4 \ No newline at end of file diff --git a/python-impl/open_imo_cii_calculator/models/imo_cii_rating.py b/python-impl/open_imo_cii_calculator/models/imo_cii_rating.py new file mode 100644 index 0000000..17501f1 --- /dev/null +++ b/python-impl/open_imo_cii_calculator/models/imo_cii_rating.py @@ -0,0 +1,19 @@ +from enum import IntEnum + +class ImoCiiRating(IntEnum): + """ + An enum describing the possible IMO Carbon Intensity Indicator (CII) ratings + 0 indicates an error + A indicates the best rating + B indicates the second best rating + C indicates the third best rating + D indicates the fourth best rating + E indicates the worst rating + """ + UNKNOWN = -1 + ERR = 0 + A = 1 + B = 2 + C = 3 + D = 4 + E = 5 \ No newline at end of file diff --git a/python-impl/open_imo_cii_calculator/models/measurement_models/ship_dd_vector_boundaries.py b/python-impl/open_imo_cii_calculator/models/measurement_models/ship_dd_vector_boundaries.py new file mode 100644 index 0000000..266fbfb --- /dev/null +++ b/python-impl/open_imo_cii_calculator/models/measurement_models/ship_dd_vector_boundaries.py @@ -0,0 +1,33 @@ +from typing import Dict +from open_imo_cii_calculator.models.ship_type import ShipType +from open_imo_cii_calculator.models.capacity_unit import CapacityUnit +from open_imo_cii_calculator.models.imo_cii_boundary import ImoCiiBoundary +from open_imo_cii_calculator.models.measurement_models.weight_classification import WeightClassification + +class ShipDdVectorBoundaries: + """ + IMO MEPC.354(78) ddvectors for a given year for the specified ship type + """ + + def __init__(self, + ship_type: ShipType, + weight_classification: WeightClassification, + capacity_unit: CapacityUnit, + boundary_dd_vectors: Dict[ImoCiiBoundary, float], + year: int): + """ + Initialize ship ddvector boundaries + + Args: + - ship_type (ShipType): The type of ship to generate ddvector boundaries for + - weight_classification (WeightClassification): The weight classification of the ship. If these ddvectors have a min/max + weight boundary in MEPC.354(78), this describes the lower and upper bound of that classification. + - capacity_unit (CapacityUnit): Indicates the capacity unit these ddvectors are calculated against + - boundary_dd_vectors (Dict[ImoCiiBoundary, float]): The ddvectors for the specified ship type, weight classification and capacity unit + - year (int): The year these ddvectors apply to + """ + self.year = year + self.ship_type = ship_type + self.weight_classification = weight_classification + self.capacity_unit = capacity_unit + self.boundary_dd_vectors = boundary_dd_vectors \ No newline at end of file diff --git a/python-impl/open_imo_cii_calculator/models/measurement_models/weight_classification.py b/python-impl/open_imo_cii_calculator/models/measurement_models/weight_classification.py new file mode 100644 index 0000000..94c25ff --- /dev/null +++ b/python-impl/open_imo_cii_calculator/models/measurement_models/weight_classification.py @@ -0,0 +1,15 @@ +class WeightClassification: + """ + Represents weight classification boundaries for ship categories + """ + + def __init__(self, upper_limit: int, lower_limit: int): + """ + Initialize a weight classification with upper and lower limits + + Args: + - upper_limit (int): The upper weight limit for this classification + - lower_limit (int): The lower weight limit for this classification + """ + self.upper_limit = upper_limit + self.lower_limit = lower_limit \ No newline at end of file diff --git a/python-impl/open_imo_cii_calculator/models/ship_models/ship.py b/python-impl/open_imo_cii_calculator/models/ship_models/ship.py new file mode 100644 index 0000000..db23d8d --- /dev/null +++ b/python-impl/open_imo_cii_calculator/models/ship_models/ship.py @@ -0,0 +1,19 @@ +from open_imo_cii_calculator.models.ship_type import ShipType + +class Ship: + """ + Class representing a ship with its properties used for CII calculations + """ + + def __init__(self, ship_type: ShipType, deadweight_tonnage: float, gross_tonnage: float): + """ + Initialize a Ship instance with its core properties + + Args: + - ship_type (ShipType): The type of ship (e.g., BULK_CARRIER, TANKER, etc.) + - deadweight_tonnage (float): The deadweight tonnage (DWT) in long-tons + - gross_tonnage (float): The gross tonnage (GT) in long-tons + """ + self.ship_type = ship_type + self.deadweight_tonnage = deadweight_tonnage + self.gross_tonnage = gross_tonnage \ No newline at end of file diff --git a/python-impl/open_imo_cii_calculator/models/ship_type.py b/python-impl/open_imo_cii_calculator/models/ship_type.py new file mode 100644 index 0000000..d3d8d13 --- /dev/null +++ b/python-impl/open_imo_cii_calculator/models/ship_type.py @@ -0,0 +1,46 @@ +from enum import IntEnum + +class ShipType(IntEnum): + """ + An enum describing the possible ship types outlined in MEPC 337(76) + """ + UNKNOWN = 0 + + # A type of ship designed to carry unpackaged bulk cargo (e.g., grain, coal, ore) in its cargo holds. + BULK_CARRIER = 10 + + # A type of ship designed to transport gases in specially designed tanks. + GAS_CARRIER = 20 + + # A type of ship designed to carry petroleum products or other liquid cargoes in bulk. + TANKER = 30 + + # A type of ship designed to carry containers, which are standardized boxes used for transporting cargo. + CONTAINER_SHIP = 40 + + # A type of ship designed to carry a variety of packaged goods, including manufactured products, food, and raw materials. + GENERAL_CARGO_SHIP = 50 + + # A type of ship designed to carry multiple types of cargo, such as bulk cargo, containers, and general cargo. + COMBINATION_CARRIER = 60 + + # A type of ship designed to carry refrigerated cargo, such as perishable food items or temperature-sensitive goods. + REFRIGERATED_CARGO_CARRIER = 70 + + # A type of ship designed to transport liquefied natural gas (LNG) in specially designed tanks. + LNG_CARRIER = 80 + + # A type of ship designed to carry wheeled cargo, such as cars, trucks, or trailers, that can be driven on and off the ship using built-in ramps. + RORO_CARGO_SHIP_VEHICLE_CARRIER = 90 + + # A type of ship designed to carry wheeled cargo using built-in ramps for loading and unloading vehicles. + RORO_CARGO_SHIP = 100 + + # A type of ship designed to carry both wheeled cargo and passengers, with built-in ramps for loading and unloading vehicles. + RORO_PASSENGER_SHIP = 110 + + # A type of high-speed ship designed to conform to SOLAS Chapter X standards + RORO_PASSENGER_SHIP_HIGH_SPEED_SOLAS = 111 + + # A type of ship designed primarily for passenger accommodation and leisure activities, often including amenities such as restaurants, entertainment venues, and recreational facilities. + CRUISE_PASSENGER_SHIP = 120 \ No newline at end of file diff --git a/python-impl/open_imo_cii_calculator/services/__init__.py b/python-impl/open_imo_cii_calculator/services/__init__.py new file mode 100644 index 0000000..8c9fd00 --- /dev/null +++ b/python-impl/open_imo_cii_calculator/services/__init__.py @@ -0,0 +1,17 @@ +""" +This module initializes the services package. +""" +# Import services to make them available at the package level +from open_imo_cii_calculator.services.carbon_intensity_indicator_calculator_service import CarbonIntensityIndicatorCalculatorService +from open_imo_cii_calculator.services.ship_capacity_calculator_service import ShipCapacityCalculatorService +from open_imo_cii_calculator.services.ship_mass_of_co2_emissions_calculator_service import ShipMassOfCo2EmissionsCalculatorService +from open_imo_cii_calculator.services.ship_transport_work_calculator_service import ShipTransportWorkCalculatorService +from open_imo_cii_calculator.services.rating_boundaries_service import RatingBoundariesService + +__all__ = [ + 'CarbonIntensityIndicatorCalculatorService', + 'ShipCapacityCalculatorService', + 'ShipMassOfCo2EmissionsCalculatorService', + 'ShipTransportWorkCalculatorService', + 'RatingBoundariesService' +] \ No newline at end of file diff --git a/python-impl/open_imo_cii_calculator/services/carbon_intensity_indicator_calculator_service.py b/python-impl/open_imo_cii_calculator/services/carbon_intensity_indicator_calculator_service.py new file mode 100644 index 0000000..510785b --- /dev/null +++ b/python-impl/open_imo_cii_calculator/services/carbon_intensity_indicator_calculator_service.py @@ -0,0 +1,310 @@ +""" +Service for calculating Carbon Intensity Indicator (CII) +""" +import math +from enum import Enum +from typing import Dict + +from open_imo_cii_calculator.models.ship_type import ShipType +from open_imo_cii_calculator.services.reduction_factor_utils import apply_annual_reduction_factor + + +class ValType(Enum): + """Enumeration for coefficient types used in CII calculations""" + A = "a" + C = "c" + + +class CarbonIntensityIndicatorCalculatorService: + """ + Service for calculating the Carbon Intensity Indicator (CII) for ships in accordance with IMO regulations + """ + + def get_attained_carbon_intensity(self, mass_of_co2_emissions: float, transport_work: float) -> float: + """ + Gets a ship's attained carbon intensity, which is the ratio of the cumulative mass + of CO2 emissions in a calendar year to the ship's transport work in a calendar year + + Args: + - mass_of_co2_emissions (float): The cumulative mass of CO2 emissions in a calendar year in grams (g) + - transport_work (float): The ship's transport work in a calendar year + + Returns: + - float: A ship's attained Carbon Intensity (CII) + + Raises: + - ValueError: If mass_of_co2_emissions or transport_work is less than or equal to zero + """ + if mass_of_co2_emissions <= 0: + raise ValueError("Mass of CO2 emissions must be a positive value") + + if transport_work <= 0: + raise ValueError("Transport work must be a positive value") + + return mass_of_co2_emissions / transport_work + + def get_required_carbon_intensity(self, ship_type: ShipType, capacity: float, year: int) -> float: + """ + Gets a ship's required CII in accordance with MEPC.323(74) + + Args: + - ship_type (ShipType): The type of ship + - capacity (float): The ship's capacity according to MEPC 337(76) (pre-calculated) + - year (int): The calendar year being analyzed + + Returns: + - (float): The required CII for a ship of the given type and capacity + + Raises: + ValueError: If capacity is equal or lower than 0 + """ + if capacity <= 0: + raise ValueError("Capacity must be a positive value") + + a = self._get_value(ValType.A, ship_type, capacity) + c = self._get_value(ValType.C, ship_type, capacity) + + cii_reference = a * math.pow(capacity, -c) + + return apply_annual_reduction_factor(cii_reference, year) + + def get_required_carbon_intensity_dict(self, ship_type: ShipType, capacity: float) -> Dict[int, float]: + """ + Gets a ship's required CII for years 2019-2030 + + Args: + - ship_type (ShipType): The type of ship + - capacity (float): The ship's capacity + + Returns: + - (Dict[int, float]): Dictionary mapping years to their required CII values + """ + cii_dict = {} + + for year in range(2019, 2031): + cii_dict[year] = self.get_required_carbon_intensity(ship_type, capacity, year) + + return cii_dict + + def _get_value(self, val_type: ValType, ship_type: ShipType, capacity: float) -> float: + """ + Gets either the `a` or `c` value for a given ship type and capacity + + Args: + - val_type (ValType): The coefficient type to return (a or c) + - ship_type (ShipType): The type of ship being queried + - capacity (float): The capacity of the ship being queried + + Returns: + - (float): The `a` or `c` value for the given ship type and capacity + + Raises: + ValueError: If val_type is not a or c + ValueError: If the ship type is not supported + """ + if val_type not in [ValType.A, ValType.C]: + raise ValueError(f"Invalid value type '{val_type}'") + + ship_type_handlers = { + ShipType.BULK_CARRIER: self._get_bulk_carrier_value, + ShipType.GAS_CARRIER: self._get_gas_carrier_value, + ShipType.TANKER: self._get_tanker_value, + ShipType.CONTAINER_SHIP: self._get_container_ship_value, + ShipType.GENERAL_CARGO_SHIP: self._get_general_cargo_ship_value, + ShipType.REFRIGERATED_CARGO_CARRIER: self._get_refrigerated_cargo_carrier_value, + ShipType.COMBINATION_CARRIER: self._get_combination_carrier_value, + ShipType.LNG_CARRIER: self._get_lng_carrier_ship_value, + ShipType.RORO_CARGO_SHIP_VEHICLE_CARRIER: self._get_roro_cargo_ship_vehicle_carrier_value, + ShipType.RORO_CARGO_SHIP: self._get_roro_cargo_ship_value, + ShipType.RORO_PASSENGER_SHIP: self._get_roro_passenger_ship_value, + ShipType.RORO_PASSENGER_SHIP_HIGH_SPEED_SOLAS: self._get_roro_passenger_ship_high_speed_solas_value, + ShipType.CRUISE_PASSENGER_SHIP: self._get_roro_cruise_passenger_ship_value + } + + if ship_type not in ship_type_handlers or ship_type == ShipType.UNKNOWN: + raise ValueError(f"Unsupported ship type: {ship_type}") + + return ship_type_handlers[ship_type](val_type, capacity) + + def _get_lng_carrier_ship_value(self, val_type: ValType, capacity: float) -> float: + """ + Gets the appropriate `a` or `c` value for a LNG Carrier, according to Table 1: MEPC.353(78) + + Args: + - val_type (ValType): The coefficient type to return (a or c) + - capacity (float): The capacity of the ship being queried + + Returns: + - (float): The `a` or `c` value for a LNG Carrier + """ + if capacity >= 100000: + return 9.827 if val_type == ValType.A else 0.000 + if capacity >= 65000: + return 14479E10 if val_type == ValType.A else 2.673 + return 14779E10 if val_type == ValType.A else 2.673 + + def _get_general_cargo_ship_value(self, val_type: ValType, capacity: float) -> float: + """ + Gets the appropriate `a` or `c` value for a General Cargo ship, according to Table 1: MEPC.353(78) + + Args: + - val_type (ValType): The coefficient type to return (a or c) + - capacity (float): The capacity of the ship being queried + + Returns: + - (float): The `a` or `c` value for a General Cargo ship + """ + if capacity >= 20000: + return 31948 if val_type == ValType.A else 0.792 + return 588 if val_type == ValType.A else 0.3885 + + def _get_bulk_carrier_value(self, val_type: ValType, capacity: float) -> float: + """ + Gets the appropriate `a` or `c` value for a Bulk Carrier ship, according to Table 1: MEPC.353(78) + + Args: + - val_type (ValType): The coefficient type to return (a or c) + - capacity (float): The capacity of the ship being queried + + Returns: + - (float): The `a` or `c` value for a Bulk Carrier ship + """ + if capacity >= 279000: + return 4745 if val_type == ValType.A else 0.622 + return 4745 if val_type == ValType.A else 0.622 + + def _get_gas_carrier_value(self, val_type: ValType, capacity: float) -> float: + """ + Gets the appropriate `a` or `c` value for a Gas Carrier ship, according to Table 1: MEPC.353(78) + + Args: + - val_type (ValType): The coefficient type to return (a or c) + - capacity (float): The capacity of the ship being queried + + Returns: + - (float): The `a` or `c` value for a Gas Carrier ship + """ + if capacity >= 65000: + return 14405E7 if val_type == ValType.A else 2.071 + return 8104 if val_type == ValType.A else 0.639 + + def _get_tanker_value(self, val_type: ValType, capacity: float) -> float: + """ + Gets the appropriate `a` or `c` value for a Tanker ship, according to Table 1: MEPC.353(78) + + Args: + - val_type (ValType): The coefficient type to return (a or c) + - capacity (float): The capacity of the ship being queried + + Returns: + - (float): The `a` or `c` value for a Tanker ship + """ + return 5247 if val_type == ValType.A else 0.610 + + def _get_container_ship_value(self, val_type: ValType, capacity: float) -> float: + """ + Gets the appropriate `a` or `c` value for a Container ship, according to Table 1: MEPC.353(78) + + Args: + - val_type (ValType): The coefficient type to return (a or c) + - capacity (float): The capacity of the ship being queried + + Returns: + - (float): The `a` or `c` value for a Container ship + """ + return 1984 if val_type == ValType.A else 0.489 + + def _get_refrigerated_cargo_carrier_value(self, val_type: ValType, capacity: float) -> float: + """ + Gets the appropriate `a` or `c` value for a Refrigerated Cargo Carrier ship, according to Table 1: MEPC.353(78) + + Args: + - val_type (ValType): The coefficient type to return (a or c) + - capacity (float): The capacity of the ship being queried + + Returns: + - (float): The `a` or `c` value for a Refrigerated Cargo Carrier ship + """ + return 4600 if val_type == ValType.A else 0.557 + + def _get_combination_carrier_value(self, val_type: ValType, capacity: float) -> float: + """ + Gets the appropriate `a` or `c` value for a Combination Carrier ship, according to Table 1: MEPC.353(78) + + Args: + - val_type (ValType): The coefficient type to return (a or c) + - capacity (float): The capacity of the ship being queried + + Returns: + - (float): The `a` or `c` value for a Combination Carrier ship + """ + return 5119 if val_type == ValType.A else 622 + + def _get_roro_cargo_ship_vehicle_carrier_value(self, val_type: ValType, capacity: float) -> float: + """ + Gets the appropriate `a` or `c` value for a RoRo Cargo Ship Vehicle Carrier, according to Table 1: MEPC.353(78) + + Args: + - val_type (ValType): The coefficient type to return (a or c) + - capacity (float): The capacity of the ship being queried + + Returns: + - (float): The `a` or `c` value for a RoRo Cargo Ship Vehicle Carrier + """ + if capacity >= 57700: + return 3627 if val_type == ValType.A else 0.590 + if capacity >= 30000: + return 5739 if val_type == ValType.A else 0.590 + return 330 if val_type == ValType.A else 329 + + def _get_roro_cargo_ship_value(self, val_type: ValType, capacity: float) -> float: + """ + Gets the appropriate `a` or `c` value for a RoRo Cargo Ship, according to Table 1: MEPC.353(78) + + Args: + - val_type (ValType): The coefficient type to return (a or c) + - capacity (float): The capacity of the ship being queried + + Returns: + - (float): The `a` or `c` value for a RoRo Cargo Ship + """ + return 1967 if val_type == ValType.A else 0.485 + + def _get_roro_passenger_ship_value(self, val_type: ValType, capacity: float) -> float: + """ + Gets the appropriate `a` or `c` value for a RoRo Passenger Ship, according to Table 1: MEPC.353(78) + + Args: + - val_type (ValType): The coefficient type to return (a or c) + - capacity (float): The capacity of the ship being queried + + Returns: + - (float): The `a` or `c` value for a RoRo Passenger Ship + """ + return 2023 if val_type == ValType.A else 0.460 + + def _get_roro_passenger_ship_high_speed_solas_value(self, val_type: ValType, capacity: float) -> float: + """ + Gets the appropriate `a` or `c` value for a RoRo Passenger Ship (High Speed SOLAS), according to Table 1: MEPC.353(78) + + Args: + - val_type (ValType): The coefficient type to return (a or c) + - capacity (float): The capacity of the ship being queried + + Returns: + - (float): The `a` or `c` value for a RoRo Passenger Ship (High Speed SOLAS) + """ + return 4196 if val_type == ValType.A else 0.460 + + def _get_roro_cruise_passenger_ship_value(self, val_type: ValType, capacity: float) -> float: + """ + Gets the appropriate `a` or `c` value for a Cruise Passenger Ship, according to Table 1: MEPC.353(78) + + Args: + - val_type (ValType): The coefficient type to return (a or c) + - capacity (float): The capacity of the ship being queried + + Returns: + - (float): The `a` or `c` value for a Cruise Passenger Ship + """ + return 930 if val_type == ValType.A else 0.383 \ No newline at end of file diff --git a/python-impl/open_imo_cii_calculator/services/rating_boundaries_service.py b/python-impl/open_imo_cii_calculator/services/rating_boundaries_service.py new file mode 100644 index 0000000..32a8e48 --- /dev/null +++ b/python-impl/open_imo_cii_calculator/services/rating_boundaries_service.py @@ -0,0 +1,286 @@ +""" +Service for calculating rating boundaries according to MEPC.354(78) guidelines +""" +from typing import Dict + +from open_imo_cii_calculator.models.ship_type import ShipType +from open_imo_cii_calculator.models.capacity_unit import CapacityUnit +from open_imo_cii_calculator.models.imo_cii_boundary import ImoCiiBoundary +from open_imo_cii_calculator.models.measurement_models.ship_dd_vector_boundaries import ShipDdVectorBoundaries +from open_imo_cii_calculator.models.measurement_models.weight_classification import WeightClassification +from open_imo_cii_calculator.models.ship_models.ship import Ship + + +class RatingBoundariesService: + """ + Service for calculating ship rating boundaries according to MEPC.354(78) guidelines + """ + + def get_boundaries(self, ship: Ship, required_cii_in_year: float, year: int) -> ShipDdVectorBoundaries: + """ + Returns the ship grading boundaries outlined in MEPC.354(78) for a given + ship and required CII in a year. + + Args: + - ship (Ship): The ship object containing ship type and tonnage information + - required_cii_in_year (float): The required CII value for the specified year + - year (int): The year for which to calculate the boundaries + + Returns: + - ShipDdVectorBoundaries: The rating boundaries for the ship + + Raises: + - ValueError: If the ship tonnage is invalid or ship type is not supported + """ + self._validate_ship_tonnage_valid(ship) + + if ship.ship_type == ShipType.BULK_CARRIER: + if ship.deadweight_tonnage <= 0: + raise ValueError(f"Deadweight tonnage must be greater than 0 for ship type {ship.ship_type}") + + return ShipDdVectorBoundaries( + ship.ship_type, + WeightClassification(0, float('inf')), + CapacityUnit.DWT, + { + ImoCiiBoundary.SUPERIOR: 0.86 * required_cii_in_year, + ImoCiiBoundary.LOWER: 0.94 * required_cii_in_year, + ImoCiiBoundary.UPPER: 1.06 * required_cii_in_year, + ImoCiiBoundary.INFERIOR: 1.18 * required_cii_in_year + }, + year + ) + elif ship.ship_type == ShipType.GAS_CARRIER: + if ship.deadweight_tonnage >= 65000: + return ShipDdVectorBoundaries( + ship.ship_type, + WeightClassification(65000, float('inf')), + CapacityUnit.DWT, + { + ImoCiiBoundary.SUPERIOR: 0.81 * required_cii_in_year, + ImoCiiBoundary.LOWER: 0.91 * required_cii_in_year, + ImoCiiBoundary.UPPER: 1.12 * required_cii_in_year, + ImoCiiBoundary.INFERIOR: 1.44 * required_cii_in_year + }, + year + ) + else: + return ShipDdVectorBoundaries( + ship.ship_type, + WeightClassification(0, 65000 - 1), + CapacityUnit.DWT, + { + ImoCiiBoundary.SUPERIOR: 0.85 * required_cii_in_year, + ImoCiiBoundary.LOWER: 0.95 * required_cii_in_year, + ImoCiiBoundary.UPPER: 1.06 * required_cii_in_year, + ImoCiiBoundary.INFERIOR: 1.25 * required_cii_in_year + }, + year + ) + elif ship.ship_type == ShipType.TANKER: + return ShipDdVectorBoundaries( + ship.ship_type, + WeightClassification(0, float('inf')), + CapacityUnit.DWT, + { + ImoCiiBoundary.SUPERIOR: 0.82 * required_cii_in_year, + ImoCiiBoundary.LOWER: 0.93 * required_cii_in_year, + ImoCiiBoundary.UPPER: 1.08 * required_cii_in_year, + ImoCiiBoundary.INFERIOR: 1.28 * required_cii_in_year + }, + year + ) + elif ship.ship_type == ShipType.CONTAINER_SHIP: + return ShipDdVectorBoundaries( + ship.ship_type, + WeightClassification(0, float('inf')), + CapacityUnit.DWT, + { + ImoCiiBoundary.SUPERIOR: 0.83 * required_cii_in_year, + ImoCiiBoundary.LOWER: 0.94 * required_cii_in_year, + ImoCiiBoundary.UPPER: 1.07 * required_cii_in_year, + ImoCiiBoundary.INFERIOR: 1.19 * required_cii_in_year + }, + year + ) + elif ship.ship_type == ShipType.GENERAL_CARGO_SHIP: + return ShipDdVectorBoundaries( + ship.ship_type, + WeightClassification(0, float('inf')), + CapacityUnit.DWT, + { + ImoCiiBoundary.SUPERIOR: 0.83 * required_cii_in_year, + ImoCiiBoundary.LOWER: 0.94 * required_cii_in_year, + ImoCiiBoundary.UPPER: 1.06 * required_cii_in_year, + ImoCiiBoundary.INFERIOR: 1.19 * required_cii_in_year + }, + year + ) + elif ship.ship_type == ShipType.REFRIGERATED_CARGO_CARRIER: + return ShipDdVectorBoundaries( + ship.ship_type, + WeightClassification(0, float('inf')), + CapacityUnit.DWT, + { + ImoCiiBoundary.SUPERIOR: 0.78 * required_cii_in_year, + ImoCiiBoundary.LOWER: 0.91 * required_cii_in_year, + ImoCiiBoundary.UPPER: 1.07 * required_cii_in_year, + ImoCiiBoundary.INFERIOR: 1.20 * required_cii_in_year + }, + year + ) + elif ship.ship_type == ShipType.COMBINATION_CARRIER: + return ShipDdVectorBoundaries( + ship.ship_type, + WeightClassification(0, float('inf')), + CapacityUnit.DWT, + { + ImoCiiBoundary.SUPERIOR: 0.87 * required_cii_in_year, + ImoCiiBoundary.LOWER: 0.96 * required_cii_in_year, + ImoCiiBoundary.UPPER: 1.06 * required_cii_in_year, + ImoCiiBoundary.INFERIOR: 1.14 * required_cii_in_year + }, + year + ) + elif ship.ship_type == ShipType.LNG_CARRIER: + if ship.deadweight_tonnage >= 100000: + return ShipDdVectorBoundaries( + ship.ship_type, + WeightClassification(100000, float('inf')), + CapacityUnit.DWT, + { + ImoCiiBoundary.SUPERIOR: 0.89 * required_cii_in_year, + ImoCiiBoundary.LOWER: 0.98 * required_cii_in_year, + ImoCiiBoundary.UPPER: 1.06 * required_cii_in_year, + ImoCiiBoundary.INFERIOR: 1.13 * required_cii_in_year + }, + year + ) + else: + return ShipDdVectorBoundaries( + ship.ship_type, + WeightClassification(0, 100000 - 1), + CapacityUnit.DWT, + { + ImoCiiBoundary.SUPERIOR: 0.78 * required_cii_in_year, + ImoCiiBoundary.LOWER: 0.92 * required_cii_in_year, + ImoCiiBoundary.UPPER: 1.10 * required_cii_in_year, + ImoCiiBoundary.INFERIOR: 1.37 * required_cii_in_year + }, + year + ) + elif ship.ship_type == ShipType.RORO_CARGO_SHIP_VEHICLE_CARRIER: + return ShipDdVectorBoundaries( + ship.ship_type, + WeightClassification(0, float('inf')), + CapacityUnit.GT, + { + ImoCiiBoundary.SUPERIOR: 0.86 * required_cii_in_year, + ImoCiiBoundary.LOWER: 0.94 * required_cii_in_year, + ImoCiiBoundary.UPPER: 1.06 * required_cii_in_year, + ImoCiiBoundary.INFERIOR: 1.16 * required_cii_in_year + }, + year + ) + elif ship.ship_type == ShipType.RORO_CARGO_SHIP: + return ShipDdVectorBoundaries( + ship.ship_type, + WeightClassification(0, float('inf')), + CapacityUnit.GT, + { + ImoCiiBoundary.SUPERIOR: 0.76 * required_cii_in_year, + ImoCiiBoundary.LOWER: 0.89 * required_cii_in_year, + ImoCiiBoundary.UPPER: 1.08 * required_cii_in_year, + ImoCiiBoundary.INFERIOR: 1.27 * required_cii_in_year + }, + year + ) + elif ship.ship_type == ShipType.RORO_PASSENGER_SHIP: + return ShipDdVectorBoundaries( + ship.ship_type, + WeightClassification(0, float('inf')), + CapacityUnit.GT, + { + ImoCiiBoundary.SUPERIOR: 0.76 * required_cii_in_year, + ImoCiiBoundary.LOWER: 0.92 * required_cii_in_year, + ImoCiiBoundary.UPPER: 1.14 * required_cii_in_year, + ImoCiiBoundary.INFERIOR: 1.30 * required_cii_in_year + }, + year + ) + elif ship.ship_type == ShipType.RORO_PASSENGER_SHIP_HIGH_SPEED_SOLAS: + return ShipDdVectorBoundaries( + ship.ship_type, + WeightClassification(0, float('inf')), + CapacityUnit.GT, + { + ImoCiiBoundary.SUPERIOR: 0.76 * required_cii_in_year, + ImoCiiBoundary.LOWER: 0.92 * required_cii_in_year, + ImoCiiBoundary.UPPER: 1.14 * required_cii_in_year, + ImoCiiBoundary.INFERIOR: 1.30 * required_cii_in_year + }, + year + ) + elif ship.ship_type == ShipType.CRUISE_PASSENGER_SHIP: + return ShipDdVectorBoundaries( + ship.ship_type, + WeightClassification(0, float('inf')), + CapacityUnit.GT, + { + ImoCiiBoundary.SUPERIOR: 0.87 * required_cii_in_year, + ImoCiiBoundary.LOWER: 0.95 * required_cii_in_year, + ImoCiiBoundary.UPPER: 1.06 * required_cii_in_year, + ImoCiiBoundary.INFERIOR: 1.16 * required_cii_in_year + }, + year + ) + elif ship.ship_type == ShipType.UNKNOWN: + raise ValueError(f"Ship type '{ship.ship_type}' not supported") + else: + raise ValueError(f"Ship type '{ship.ship_type}' not supported") + + def _validate_ship_tonnage_valid(self, ship: Ship) -> None: + """ + Checks that the ship tonnage is valid for the ship type. + + Args: + - ship: The ship object to validate + + Raises: + - ValueError: If the ship's tonnage is invalid for its type + """ + dwt_ships = [ + ShipType.BULK_CARRIER, + ShipType.GAS_CARRIER, + ShipType.TANKER, + ShipType.CONTAINER_SHIP, + ShipType.GENERAL_CARGO_SHIP, + ShipType.REFRIGERATED_CARGO_CARRIER, + ShipType.COMBINATION_CARRIER, + ShipType.LNG_CARRIER + ] + + gt_ships = [ + ShipType.RORO_CARGO_SHIP_VEHICLE_CARRIER, + ShipType.RORO_CARGO_SHIP, + ShipType.RORO_PASSENGER_SHIP, + ShipType.RORO_PASSENGER_SHIP_HIGH_SPEED_SOLAS, + ShipType.CRUISE_PASSENGER_SHIP + ] + + if ship.ship_type in dwt_ships: + if ship.deadweight_tonnage <= 0: + raise ValueError( + f"Deadweight tonnage must be greater than 0 for ship type {ship.ship_type}. " + f"Was provided {ship.deadweight_tonnage}" + ) + elif ship.ship_type in gt_ships: + if ship.gross_tonnage <= 0: + raise ValueError( + f"Gross tonnage must be greater than 0 for ship type {ship.ship_type}. " + f"Was provided {ship.gross_tonnage}" + ) + elif ship.ship_type == ShipType.UNKNOWN: + raise ValueError(f"Ship type '{ship.ship_type}' not supported") + else: + raise ValueError(f"Ship type '{ship.ship_type}' not supported") diff --git a/python-impl/open_imo_cii_calculator/services/reduction_factor_utils.py b/python-impl/open_imo_cii_calculator/services/reduction_factor_utils.py new file mode 100644 index 0000000..d968e97 --- /dev/null +++ b/python-impl/open_imo_cii_calculator/services/reduction_factor_utils.py @@ -0,0 +1,51 @@ +""" +Utility functions for working with annual reduction factors according to MEPC.338(76) +""" + + +def get_annual_reduction_factor(year: int) -> float: + """ + Gets an annual reduction factor for a given year, according to MEPC.338(76) + + Args: + - year (int): The calendar year being analyzed + + Returns: + - float: The reduction factor + + Raises: + - ValueError: If a year outside of the range 2019-2030 (inclusive) is provided + """ + reduction_factors = { + 2019: 0.00, + 2020: 0.01, + 2021: 0.02, + 2022: 0.03, + 2023: 0.05, + 2024: 0.07, + 2025: 0.09, + 2026: 0.11, + 2027: 0.13, + 2028: 0.15, + 2029: 0.17, + 2030: 0.19 + } + + if year not in reduction_factors: + raise ValueError(f"Year {year} is not supported") + + return reduction_factors[year] + + +def apply_annual_reduction_factor(value: float, year: int) -> float: + """ + Apply annual reduction factor to a value + + Args: + - value (float): The value to apply the reduction factor to + - year (int): The calendar year being analyzed + + Returns: + - float: The value with the reduction factor applied + """ + return value * (1 - get_annual_reduction_factor(year)) \ No newline at end of file diff --git a/python-impl/open_imo_cii_calculator/services/ship_capacity_calculator_service.py b/python-impl/open_imo_cii_calculator/services/ship_capacity_calculator_service.py new file mode 100644 index 0000000..b8f1aec --- /dev/null +++ b/python-impl/open_imo_cii_calculator/services/ship_capacity_calculator_service.py @@ -0,0 +1,117 @@ +""" +Service for calculating ship capacity according to MEPC.353(78) guidelines +""" +from typing import Union, Optional +from open_imo_cii_calculator.models.ship_type import ShipType +from open_imo_cii_calculator.models.ship_models.ship import Ship + + +class ShipCapacityCalculatorService: + """ + Service for calculating ship capacity according to MEPC.353(78) guidelines + """ + + def get_ship_capacity(self, ship_or_type: Union[Ship, ShipType], deadweight_tonnage: Optional[float] = None, gross_tonnage: Optional[float] = None) -> float: + """ + Calculates the ship's capacity according to the MEPC.353(78) guidelines + + Args: + - ship_or_type (Union[Ship, ShipType]): The ship object or type of the ship + - deadweight_tonnage (Optional[float]): The deadweight tonnage of the ship + - gross_tonnage (Optional[float]): The gross tonnage of the ship + + Returns: + - float: The calculated ship capacity + """ + if isinstance(ship_or_type, Ship): + ship = ship_or_type + return self.get_ship_capacity(ship.ship_type, ship.deadweight_tonnage, ship.gross_tonnage) + + ship_type = ship_or_type + + self._validate_tonnage_params_set(ship_type, deadweight_tonnage, gross_tonnage) + + if ship_type == ShipType.BULK_CARRIER: + return 279000 if deadweight_tonnage >= 279000 else deadweight_tonnage + elif ship_type == ShipType.GAS_CARRIER: + return deadweight_tonnage + elif ship_type == ShipType.TANKER: + return deadweight_tonnage + elif ship_type == ShipType.CONTAINER_SHIP: + return deadweight_tonnage + elif ship_type == ShipType.GENERAL_CARGO_SHIP: + return deadweight_tonnage + elif ship_type == ShipType.REFRIGERATED_CARGO_CARRIER: + return deadweight_tonnage + elif ship_type == ShipType.COMBINATION_CARRIER: + return deadweight_tonnage + elif ship_type == ShipType.LNG_CARRIER: + return 65000 if deadweight_tonnage < 65000 else deadweight_tonnage + elif ship_type == ShipType.RORO_CARGO_SHIP_VEHICLE_CARRIER: + return 57700 if deadweight_tonnage >= 57700 else gross_tonnage + elif ship_type == ShipType.RORO_CARGO_SHIP: + return gross_tonnage + elif ship_type == ShipType.RORO_PASSENGER_SHIP: + return gross_tonnage + elif ship_type == ShipType.RORO_PASSENGER_SHIP_HIGH_SPEED_SOLAS: + return gross_tonnage + elif ship_type == ShipType.CRUISE_PASSENGER_SHIP: + return gross_tonnage + else: + raise ValueError(f"Unsupported ship type: {ship_type}") + + def _validate_tonnage_params_set(self, ship_type: ShipType, deadweight_tonnage: Optional[float], gross_tonnage: Optional[float]): + """ + Validates that the required tonnage parameters are set for the given ship type + + Args: + - ship_type (ShipType): The type of ship + - deadweight_tonnage (Optional[float]): The ship's deadweight tonnage + - gross_tonnage (Optional[float]): The ship's gross tonnage + + Raises: + - ValueError: If the required tonnage parameters are not set or invalid + """ + # Ship types that require deadweight tonnage + deadweight_ship_types = [ + ShipType.BULK_CARRIER, + ShipType.GAS_CARRIER, + ShipType.TANKER, + ShipType.CONTAINER_SHIP, + ShipType.GENERAL_CARGO_SHIP, + ShipType.REFRIGERATED_CARGO_CARRIER, + ShipType.COMBINATION_CARRIER, + ShipType.LNG_CARRIER + ] + + # Ship types that require gross tonnage + gross_tonnage_ship_types = [ + ShipType.RORO_CARGO_SHIP_VEHICLE_CARRIER, + ShipType.RORO_CARGO_SHIP, + ShipType.RORO_PASSENGER_SHIP, + ShipType.RORO_PASSENGER_SHIP_HIGH_SPEED_SOLAS, + ShipType.CRUISE_PASSENGER_SHIP + ] + + if ship_type in deadweight_ship_types: + self._validate_tonnage(deadweight_tonnage, "deadweight_tonnage", ship_type) + + if ship_type in gross_tonnage_ship_types: + self._validate_tonnage(gross_tonnage, "gross_tonnage", ship_type) + + def _validate_tonnage(self, tonnage: Optional[float], tonnage_name: str, ship_type: ShipType): + """ + Validates that the tonnage value is greater than 0 + + Args: + - tonnage (Optional[float]): The tonnage value to validate + - tonnage_name (str): The name of the tonnage parameter + - ship_type (ShipType): The type of ship + + Raises: + - ValueError: If the tonnage value is less than or equal to 0 + """ + if tonnage is None or tonnage <= 0: + raise ValueError(f"{tonnage_name} must be greater than 0 if ship_type is set to {ship_type}") + + return True \ No newline at end of file diff --git a/python-impl/open_imo_cii_calculator/services/ship_mass_of_co2_emissions_calculator_service.py b/python-impl/open_imo_cii_calculator/services/ship_mass_of_co2_emissions_calculator_service.py new file mode 100644 index 0000000..d5b2f79 --- /dev/null +++ b/python-impl/open_imo_cii_calculator/services/ship_mass_of_co2_emissions_calculator_service.py @@ -0,0 +1,137 @@ +""" +Service for calculating ship mass of CO2 emissions +""" +from typing import Dict + +from open_imo_cii_calculator.models.fuel_type import TypeOfFuel + + +class ShipMassOfCo2EmissionsCalculatorService: + """ + Service for calculating the mass of CO2 emissions for a ship + """ + + def get_mass_of_co2_emissions(self, fuel_type: TypeOfFuel, fuel_consumption_mass_in_grams: float) -> float: + """ + Gets the mass of CO2 emissions in grams (g) for a given fuel type and fuel consumption mass. + + Args: + - fuel_type (TypeOfFuel): The fuel type in use by the ship's engine + - fuel_consumption_mass_in_grams (float): The cumulative mass of consumed fuel across the calendar year in grams (g) + + Returns: + - float: The mass of CO2 emissions for a ship in a calendar year in grams (g) + + Raises: + - ValueError: If fuel_consumption_mass_in_grams is less than or equal to zero + - ValueError: If an unsupported fuel type is provided + """ + if fuel_consumption_mass_in_grams < 0: + raise ValueError("Fuel consumption mass must be a positive value") + + fuel_mass_conversion_factor = self.get_fuel_mass_conversion_factor(fuel_type) + mass_of_co2_emissions = fuel_consumption_mass_in_grams * fuel_mass_conversion_factor + + return mass_of_co2_emissions + + def get_fuel_mass_conversion_factor(self, fuel_type: TypeOfFuel) -> float: + """ + Gets a fuel type's mass conversion factor in accordance with MEPC.364(79) + + Args: + - fuel_type (TypeOfFuel): The fuel type to get the conversion factor for + + Returns: + - float: The fuel mass conversion factor for the given fuel type + + Raises: + - ValueError: If an unsupported fuel type is provided + + Note: + Emissions mass conversion factors are outlined in IMO MEPC.364(79) + https://wwwcdn.imo.org/localresources/en/KnowledgeCentre/IndexofIMOResolutions/MEPCDocuments/MEPC.364(79).pdf + """ + conversion_factors = { + TypeOfFuel.DIESEL_OR_GASOIL: 3.206, + TypeOfFuel.LIGHT_FUEL_OIL: 3.151, + TypeOfFuel.HEAVY_FUEL_OIL: 3.114, + TypeOfFuel.LIQUIFIED_PETROLEUM_PROPANE: 3.000, + TypeOfFuel.LIQUIFIED_PETROLEUM_BUTANE: 3.030, + TypeOfFuel.ETHANE: 2.927, + TypeOfFuel.LIQUIFIED_NATURAL_GAS: 2.750, + TypeOfFuel.METHANOL: 1.375, + TypeOfFuel.ETHANOL: 1.913 + } + + if fuel_type not in conversion_factors: + raise ValueError(f"Unsupported fuel type: {fuel_type}") + + return conversion_factors[fuel_type] + + def get_fuel_carbon_content(self, fuel_type: TypeOfFuel) -> float: + """ + Gets a fuel type's carbon content in accordance with MEPC.364(79) + + Args: + - fuel_type: The fuel type to get the carbon content for + + Returns: + - float: The carbon content for the given fuel type + + Raises: + - ValueError: If an unsupported fuel type is provided + + Note: + Carbon content data are outlined in IMO MEPC.364(79) + https://wwwcdn.imo.org/localresources/en/KnowledgeCentre/IndexofIMOResolutions/MEPCDocuments/MEPC.364(79).pdf + """ + carbon_contents = { + TypeOfFuel.DIESEL_OR_GASOIL: 0.8744, + TypeOfFuel.LIGHT_FUEL_OIL: 0.8594, + TypeOfFuel.HEAVY_FUEL_OIL: 0.8493, + TypeOfFuel.LIQUIFIED_PETROLEUM_PROPANE: 0.8182, + TypeOfFuel.LIQUIFIED_PETROLEUM_BUTANE: 0.8264, + TypeOfFuel.ETHANE: 0.7989, + TypeOfFuel.LIQUIFIED_NATURAL_GAS: 0.7500, + TypeOfFuel.METHANOL: 0.3750, + TypeOfFuel.ETHANOL: 0.5217 + } + + if fuel_type not in carbon_contents: + raise ValueError(f"Unsupported fuel type: {fuel_type}") + + return carbon_contents[fuel_type] + + def get_fuel_lower_calorific_value(self, fuel_type: TypeOfFuel) -> float: + """ + Gets the lower calorific value for a given fuel type in accordance with MEPC.364(79) + + Args: + - fuel_type: The fuel type to get the lower calorific value for + + Returns: + - float: The lower calorific value for the given fuel type + + Raises: + - ValueError: If an unsupported fuel type is provided + + Note: + Fuel calorific values are outlined in IMO MEPC.364(79) + https://wwwcdn.imo.org/localresources/en/KnowledgeCentre/IndexofIMOResolutions/MEPCDocuments/MEPC.364(79).pdf + """ + calorific_values = { + TypeOfFuel.DIESEL_OR_GASOIL: 42700, + TypeOfFuel.LIGHT_FUEL_OIL: 41200, + TypeOfFuel.HEAVY_FUEL_OIL: 40200, + TypeOfFuel.LIQUIFIED_PETROLEUM_PROPANE: 46300, + TypeOfFuel.LIQUIFIED_PETROLEUM_BUTANE: 45700, + TypeOfFuel.ETHANE: 46400, + TypeOfFuel.LIQUIFIED_NATURAL_GAS: 48000, + TypeOfFuel.METHANOL: 19900, + TypeOfFuel.ETHANOL: 26800 + } + + if fuel_type not in calorific_values: + raise ValueError(f"Unsupported fuel type: {fuel_type}") + + return calorific_values[fuel_type] \ No newline at end of file diff --git a/python-impl/open_imo_cii_calculator/services/ship_transport_work_calculator_service.py b/python-impl/open_imo_cii_calculator/services/ship_transport_work_calculator_service.py new file mode 100644 index 0000000..b2237f2 --- /dev/null +++ b/python-impl/open_imo_cii_calculator/services/ship_transport_work_calculator_service.py @@ -0,0 +1,29 @@ +""" +Service for calculating ship transport work +""" + + +class ShipTransportWorkCalculatorService: + """ + Service for calculating the transport work of a ship + """ + + def get_ship_transport_work(self, capacity: float, distance_travelled_in_nautical_miles: float) -> float: + """ + Gets a ship's transport work, which is the product of the ship's capacity and the distance travelled + in a calendar year expressed in nautical miles. + + Args: + - capacity (float): The ship's capacity (pre-calculated) + - distance_travelled_in_nautical_miles (float): The distance travelled by the ship in a calendar year in nautical miles + + Returns: + - float: The ship's transport work + """ + if capacity <= 0: + raise ValueError("Capacity must be a positive value") + + if distance_travelled_in_nautical_miles <= 0: + raise ValueError("Distance travelled must be a positive value") + + return capacity * distance_travelled_in_nautical_miles \ No newline at end of file diff --git a/python-impl/open_imo_cii_calculator/ship_carbon_intensity_calculator.py b/python-impl/open_imo_cii_calculator/ship_carbon_intensity_calculator.py new file mode 100644 index 0000000..9bf19d1 --- /dev/null +++ b/python-impl/open_imo_cii_calculator/ship_carbon_intensity_calculator.py @@ -0,0 +1,197 @@ +""" +Main calculator class for IMO CII calculations +""" +from typing import List, Iterable + +from open_imo_cii_calculator.models.ship_type import ShipType +from open_imo_cii_calculator.models.fuel_type import TypeOfFuel +from open_imo_cii_calculator.models.imo_cii_rating import ImoCiiRating +from open_imo_cii_calculator.models.imo_cii_boundary import ImoCiiBoundary +from open_imo_cii_calculator.models.ship_models.ship import Ship +from open_imo_cii_calculator.models.dto.calculation_result import CalculationResult +from open_imo_cii_calculator.models.dto.result_year import ResultYear +from open_imo_cii_calculator.models.dto.fuel_type_consumption import FuelTypeConsumption +from open_imo_cii_calculator.models.measurement_models.ship_dd_vector_boundaries import ShipDdVectorBoundaries + +from open_imo_cii_calculator.services.carbon_intensity_indicator_calculator_service import CarbonIntensityIndicatorCalculatorService +from open_imo_cii_calculator.services.rating_boundaries_service import RatingBoundariesService +from open_imo_cii_calculator.services.ship_capacity_calculator_service import ShipCapacityCalculatorService +from open_imo_cii_calculator.services.ship_mass_of_co2_emissions_calculator_service import ShipMassOfCo2EmissionsCalculatorService +from open_imo_cii_calculator.services.ship_transport_work_calculator_service import ShipTransportWorkCalculatorService + + +class ShipCarbonIntensityCalculator: + """ + Main calculator for determining a ship's Carbon Intensity Indicator (CII) rating + according to IMO's MEPC.354(78) guidelines. + """ + + def __init__(self): + """ + Initialize a new instance of the ShipCarbonIntensityCalculator + """ + self._ship_mass_of_co2_emissions_service = ShipMassOfCo2EmissionsCalculatorService() + self._ship_capacity_service = ShipCapacityCalculatorService() + self._ship_transport_work_service = ShipTransportWorkCalculatorService() + self._carbon_intensity_indicator_service = CarbonIntensityIndicatorCalculatorService() + self._rating_boundaries_service = RatingBoundariesService() + + def calculate_attained_cii_rating(self, + ship_type: ShipType, + gross_tonnage: float, + deadweight_tonnage: float, + distance_travelled: float, + fuel_type_consumptions: Iterable[FuelTypeConsumption], + target_year: int) -> CalculationResult: + """ + Calculate the attained CII rating for a ship for a given year + + This method accepts multiple fuel types. + + Args: + - ship_type (ShipType): The type of the ship + - gross_tonnage (float): The gross tonnage of the ship in long-tons + - deadweight_tonnage (float): The deadweight tonnage of the ship in long-tons + - distance_travelled (float): The distance travelled by the ship in a calendar year (nautical miles) + - fuel_type_consumptions (Iterable[FuelTypeConsumption]): List of fuel type consumption objects + - target_year (int): The year for which to calculate the attained CII + + Returns: + - CalculationResult: The result of the CII calculation for the ship + + Raises: + - ValueError: If any input is invalid or missing + """ + if not fuel_type_consumptions: + raise ValueError("fuel_type_consumptions must be provided") + + ship_co2_emissions = 0 + for consumption in fuel_type_consumptions: + ship_co2_emissions += self._ship_mass_of_co2_emissions_service.get_mass_of_co2_emissions( + consumption.fuel_type, consumption.fuel_consumption) + + ship_capacity = self._ship_capacity_service.get_ship_capacity( + ship_type, deadweight_tonnage, gross_tonnage) + + transport_work = self._ship_transport_work_service.get_ship_transport_work( + ship_capacity, distance_travelled) + + results = [] + for year in range(2019, 2031): + attained_cii_in_year = self._carbon_intensity_indicator_service.get_attained_carbon_intensity( + ship_co2_emissions, transport_work) + + required_cii_in_year = self._carbon_intensity_indicator_service.get_required_carbon_intensity( + ship_type, ship_capacity, year) + + vectors = self._rating_boundaries_service.get_boundaries( + Ship(ship_type, deadweight_tonnage, gross_tonnage), required_cii_in_year, year) + + rating = self._get_imo_cii_rating_from_vectors(vectors, attained_cii_in_year, year) + + results.append(ResultYear( + is_measured_year=(target_year == year), + year=year, + attained_cii=attained_cii_in_year, + required_cii=required_cii_in_year, + rating=rating, + vector_boundaries_for_year=vectors, + calculated_co2e_emissions=ship_co2_emissions, + calculated_ship_capacity=ship_capacity, + calculated_transport_work=transport_work + )) + + return CalculationResult(results) + + def calculate_attained_cii_rating_single_fuel(self, + ship_type: ShipType, + gross_tonnage: float, + deadweight_tonnage: float, + distance_travelled: float, + fuel_type: TypeOfFuel, + fuel_consumption: float, + target_year: int) -> CalculationResult: + """ + 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 + calculate_attained_cii_rating method. + + Args: + - ship_type (ShipType): The type of ship + - gross_tonnage (float): in long-tons + - deadweight_tonnage (float): in long-tons + - distance_travelled (float): distance travelled in nautical miles + - fuel_type (TypeOfFuel): The type of fuel + - fuel_consumption (float): quantity of fuel consumed in grams + - target_year (int): The calendar year being analyzed + + Returns: + - (CalculationResult): A CalculationResult containing details of the ship's carbon intensity rating + """ + ship_co2_emissions = self._ship_mass_of_co2_emissions_service.get_mass_of_co2_emissions( + fuel_type, fuel_consumption) + + ship_capacity = self._ship_capacity_service.get_ship_capacity( + ship_type, deadweight_tonnage, gross_tonnage) + + transport_work = self._ship_transport_work_service.get_ship_transport_work( + ship_capacity, distance_travelled) + + results = [] + for year in range(2019, 2031): + attained_cii_in_year = self._carbon_intensity_indicator_service.get_attained_carbon_intensity( + ship_co2_emissions, transport_work) + + required_cii_in_year = self._carbon_intensity_indicator_service.get_required_carbon_intensity( + ship_type, ship_capacity, year) + + vectors = self._rating_boundaries_service.get_boundaries( + Ship(ship_type, deadweight_tonnage, gross_tonnage), required_cii_in_year, year) + + rating = self._get_imo_cii_rating_from_vectors(vectors, attained_cii_in_year, year) + + results.append(ResultYear( + is_measured_year=(target_year == year), + year=year, + attained_cii=attained_cii_in_year, + required_cii=required_cii_in_year, + rating=rating, + vector_boundaries_for_year=vectors, + calculated_co2e_emissions=ship_co2_emissions, + calculated_ship_capacity=ship_capacity, + calculated_transport_work=transport_work + )) + + return CalculationResult(results) + + def _get_imo_cii_rating_from_vectors(self, + boundaries: ShipDdVectorBoundaries, + attained_cii_in_year: float, + year: int) -> ImoCiiRating: + """ + Determines the IMO CII rating from vector boundaries and attained CII + + Args: + - boundaries (ShipDdVectorBoundaries): The ship's boundary vectors for the given year + - attained_cii_in_year (float): The ship's attained CII in the given year + - year (int): The calendar year being analyzed + + Returns: + - (ImoCiiRating): The ship's IMO CII rating (A through E) + """ + if attained_cii_in_year < boundaries.boundary_dd_vectors[ImoCiiBoundary.SUPERIOR]: + # lower than the "superior" boundary + return ImoCiiRating.A + elif attained_cii_in_year < boundaries.boundary_dd_vectors[ImoCiiBoundary.LOWER]: + # lower than the "lower" boundary + return ImoCiiRating.B + elif attained_cii_in_year < boundaries.boundary_dd_vectors[ImoCiiBoundary.UPPER]: + # lower than the "upper" boundary + return ImoCiiRating.C + elif attained_cii_in_year < boundaries.boundary_dd_vectors[ImoCiiBoundary.INFERIOR]: + # lower than the "inferior" boundary + return ImoCiiRating.D + else: + # higher than the inferior boundary + return ImoCiiRating.E diff --git a/python-impl/poetry.lock b/python-impl/poetry.lock new file mode 100644 index 0000000..3a95bc7 --- /dev/null +++ b/python-impl/poetry.lock @@ -0,0 +1,167 @@ +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.8.0" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "coverage-7.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2931f66991175369859b5fd58529cd4b73582461877ecfd859b6549869287ffe"}, + {file = "coverage-7.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52a523153c568d2c0ef8826f6cc23031dc86cffb8c6aeab92c4ff776e7951b28"}, + {file = "coverage-7.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c8a5c139aae4c35cbd7cadca1df02ea8cf28a911534fc1b0456acb0b14234f3"}, + {file = "coverage-7.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a26c0c795c3e0b63ec7da6efded5f0bc856d7c0b24b2ac84b4d1d7bc578d676"}, + {file = "coverage-7.8.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:821f7bcbaa84318287115d54becb1915eece6918136c6f91045bb84e2f88739d"}, + {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a321c61477ff8ee705b8a5fed370b5710c56b3a52d17b983d9215861e37b642a"}, + {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ed2144b8a78f9d94d9515963ed273d620e07846acd5d4b0a642d4849e8d91a0c"}, + {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:042e7841a26498fff7a37d6fda770d17519982f5b7d8bf5278d140b67b61095f"}, + {file = "coverage-7.8.0-cp310-cp310-win32.whl", hash = "sha256:f9983d01d7705b2d1f7a95e10bbe4091fabc03a46881a256c2787637b087003f"}, + {file = "coverage-7.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:5a570cd9bd20b85d1a0d7b009aaf6c110b52b5755c17be6962f8ccd65d1dbd23"}, + {file = "coverage-7.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7ac22a0bb2c7c49f441f7a6d46c9c80d96e56f5a8bc6972529ed43c8b694e27"}, + {file = "coverage-7.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf13d564d310c156d1c8e53877baf2993fb3073b2fc9f69790ca6a732eb4bfea"}, + {file = "coverage-7.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5761c70c017c1b0d21b0815a920ffb94a670c8d5d409d9b38857874c21f70d7"}, + {file = "coverage-7.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ff52d790c7e1628241ffbcaeb33e07d14b007b6eb00a19320c7b8a7024c040"}, + {file = "coverage-7.8.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d39fc4817fd67b3915256af5dda75fd4ee10621a3d484524487e33416c6f3543"}, + {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b44674870709017e4b4036e3d0d6c17f06a0e6d4436422e0ad29b882c40697d2"}, + {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f99eb72bf27cbb167b636eb1726f590c00e1ad375002230607a844d9e9a2318"}, + {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b571bf5341ba8c6bc02e0baeaf3b061ab993bf372d982ae509807e7f112554e9"}, + {file = "coverage-7.8.0-cp311-cp311-win32.whl", hash = "sha256:e75a2ad7b647fd8046d58c3132d7eaf31b12d8a53c0e4b21fa9c4d23d6ee6d3c"}, + {file = "coverage-7.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3043ba1c88b2139126fc72cb48574b90e2e0546d4c78b5299317f61b7f718b78"}, + {file = "coverage-7.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bbb5cc845a0292e0c520656d19d7ce40e18d0e19b22cb3e0409135a575bf79fc"}, + {file = "coverage-7.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4dfd9a93db9e78666d178d4f08a5408aa3f2474ad4d0e0378ed5f2ef71640cb6"}, + {file = "coverage-7.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f017a61399f13aa6d1039f75cd467be388d157cd81f1a119b9d9a68ba6f2830d"}, + {file = "coverage-7.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0915742f4c82208ebf47a2b154a5334155ed9ef9fe6190674b8a46c2fb89cb05"}, + {file = "coverage-7.8.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a40fcf208e021eb14b0fac6bdb045c0e0cab53105f93ba0d03fd934c956143a"}, + {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a1f406a8e0995d654b2ad87c62caf6befa767885301f3b8f6f73e6f3c31ec3a6"}, + {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:77af0f6447a582fdc7de5e06fa3757a3ef87769fbb0fdbdeba78c23049140a47"}, + {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2d32f95922927186c6dbc8bc60df0d186b6edb828d299ab10898ef3f40052fe"}, + {file = "coverage-7.8.0-cp312-cp312-win32.whl", hash = "sha256:769773614e676f9d8e8a0980dd7740f09a6ea386d0f383db6821df07d0f08545"}, + {file = "coverage-7.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e5d2b9be5b0693cf21eb4ce0ec8d211efb43966f6657807f6859aab3814f946b"}, + {file = "coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd"}, + {file = "coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00"}, + {file = "coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64"}, + {file = "coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067"}, + {file = "coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008"}, + {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733"}, + {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323"}, + {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3"}, + {file = "coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d"}, + {file = "coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487"}, + {file = "coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25"}, + {file = "coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42"}, + {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502"}, + {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1"}, + {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4"}, + {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73"}, + {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a"}, + {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883"}, + {file = "coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada"}, + {file = "coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257"}, + {file = "coverage-7.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa260de59dfb143af06dcf30c2be0b200bed2a73737a8a59248fcb9fa601ef0f"}, + {file = "coverage-7.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:96121edfa4c2dfdda409877ea8608dd01de816a4dc4a0523356067b305e4e17a"}, + {file = "coverage-7.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b8af63b9afa1031c0ef05b217faa598f3069148eeee6bb24b79da9012423b82"}, + {file = "coverage-7.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89b1f4af0d4afe495cd4787a68e00f30f1d15939f550e869de90a86efa7e0814"}, + {file = "coverage-7.8.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94ec0be97723ae72d63d3aa41961a0b9a6f5a53ff599813c324548d18e3b9e8c"}, + {file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8a1d96e780bdb2d0cbb297325711701f7c0b6f89199a57f2049e90064c29f6bd"}, + {file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f1d8a2a57b47142b10374902777e798784abf400a004b14f1b0b9eaf1e528ba4"}, + {file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cf60dd2696b457b710dd40bf17ad269d5f5457b96442f7f85722bdb16fa6c899"}, + {file = "coverage-7.8.0-cp39-cp39-win32.whl", hash = "sha256:be945402e03de47ba1872cd5236395e0f4ad635526185a930735f66710e1bd3f"}, + {file = "coverage-7.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:90e7fbc6216ecaffa5a880cdc9c77b7418c1dcb166166b78dbc630d07f278cc3"}, + {file = "coverage-7.8.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:b8194fb8e50d556d5849753de991d390c5a1edeeba50f68e3a9253fbd8bf8ccd"}, + {file = "coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7"}, + {file = "coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501"}, +] + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "iniconfig" +version = "2.1.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.8" +files = [ + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, +] + +[[package]] +name = "packaging" +version = "25.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + +[[package]] +name = "pytest" +version = "8.3.5" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, + {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "6.1.1" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde"}, + {file = "pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a"}, +] + +[package.dependencies] +coverage = {version = ">=7.5", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] + +[metadata] +lock-version = "2.0" +python-versions = ">=3.12,<3.13" +content-hash = "e469140119a8651decfe5c13bd4f7107b07b89c8f03a830918e1be35135296a7" diff --git a/python-impl/pyproject.toml b/python-impl/pyproject.toml new file mode 100644 index 0000000..425fce7 --- /dev/null +++ b/python-impl/pyproject.toml @@ -0,0 +1,19 @@ +[tool.poetry] +name = "open-imo-cii-calculator" +version = "0.1.0" +description = "A python implementation of the Open IMO CII Calculator" +authors = ["Etive Mor Limited "] +readme = "README.md" +packages = [{include = "open_imo_cii_calculator"}] + +[tool.poetry.dependencies] +python = ">=3.12,<3.13" + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry.group.dev.dependencies] +pytest = "^8.3.5" +pytest-cov = "^6.1.1" + diff --git a/python-impl/tests/__init__.py b/python-impl/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python-impl/tests/conftest.py b/python-impl/tests/conftest.py new file mode 100644 index 0000000..36db5a9 --- /dev/null +++ b/python-impl/tests/conftest.py @@ -0,0 +1,28 @@ +import pytest +from open_imo_cii_calculator.models.ship_type import ShipType +from open_imo_cii_calculator.models.fuel_type import TypeOfFuel +from open_imo_cii_calculator.models.dto.fuel_type_consumption import FuelTypeConsumption + +@pytest.fixture +def sample_ship_data(): + """Fixture providing sample ship data""" + return { + 'ship_type': ShipType.RORO_PASSENGER_SHIP, + 'gross_tonnage': 25000, + 'deadweight_tonnage': 10000, + 'distance_travelled': 150000, + 'target_year': 2019 + } + +@pytest.fixture +def sample_fuel_consumption(): + """Fixture providing sample fuel consumption data""" + return [FuelTypeConsumption(TypeOfFuel.DIESEL_OR_GASOIL, 1.9e+10)] + +@pytest.fixture +def sample_multi_fuel_consumption(): + """Fixture providing sample multi-fuel consumption data""" + return [ + FuelTypeConsumption(TypeOfFuel.DIESEL_OR_GASOIL, 1.0e+10), + FuelTypeConsumption(TypeOfFuel.LIQUIFIED_NATURAL_GAS, 9.0e+9) + ] diff --git a/python-impl/tests/test_models/__init__.py b/python-impl/tests/test_models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python-impl/tests/test_models/test_capacity_unit.py b/python-impl/tests/test_models/test_capacity_unit.py new file mode 100644 index 0000000..5647362 --- /dev/null +++ b/python-impl/tests/test_models/test_capacity_unit.py @@ -0,0 +1,13 @@ +import pytest +from open_imo_cii_calculator.models.capacity_unit import CapacityUnit + +def test_capacity_unit_enum(): + """ + Test presence of all expected enum values in CapacityUnit. + + Verifies: + - All required capacity unit enum values are present and accessible in CapacityUnit. + - Ensures enum covers all supported capacity units for logic elsewhere in the codebase. + """ + assert hasattr(CapacityUnit, 'DWT') + assert hasattr(CapacityUnit, 'GT') diff --git a/python-impl/tests/test_models/test_dto/__init__.py b/python-impl/tests/test_models/test_dto/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python-impl/tests/test_models/test_dto/test_annual_consumption.py b/python-impl/tests/test_models/test_dto/test_annual_consumption.py new file mode 100644 index 0000000..05978a6 --- /dev/null +++ b/python-impl/tests/test_models/test_dto/test_annual_consumption.py @@ -0,0 +1,18 @@ +import pytest +from open_imo_cii_calculator.models.dto.annual_consumption import AnnualConsumption +from open_imo_cii_calculator.models.dto.fuel_type_consumption import FuelTypeConsumption +from open_imo_cii_calculator.models.fuel_type import TypeOfFuel + +def test_annual_consumption_init(): + """ + Test AnnualConsumption object creation and property assignment. + + Verifies: + - AnnualConsumption object is created with correct target year and fuel consumption list. + - Properties are assigned as expected during initialization. + """ + ftc = FuelTypeConsumption(fuel_type=TypeOfFuel.DIESEL_OR_GASOIL, fuel_consumption=1000) + ac = AnnualConsumption(target_year=2023, fuel_consumption=[ftc]) + assert ac.target_year == 2023 + assert isinstance(ac.fuel_consumption, list) + assert ac.fuel_consumption[0].fuel_type == TypeOfFuel.DIESEL_OR_GASOIL diff --git a/python-impl/tests/test_models/test_dto/test_calculation_result.py b/python-impl/tests/test_models/test_dto/test_calculation_result.py new file mode 100644 index 0000000..377c4b5 --- /dev/null +++ b/python-impl/tests/test_models/test_dto/test_calculation_result.py @@ -0,0 +1,16 @@ +import pytest +from open_imo_cii_calculator.models.dto.calculation_result import CalculationResult +from open_imo_cii_calculator.models.dto.result_year import ResultYear + +def test_calculation_result_init(): + """ + Test CalculationResult object creation and property assignment. + + Verifies: + - CalculationResult object is created with a list of ResultYear objects. + - Properties are assigned as expected during initialization. + """ + ry = ResultYear(is_measured_year=True, year=2023, attained_cii=1.0, required_cii=2.0, rating='A', vector_boundaries_for_year=None, calculated_co2e_emissions=100, calculated_ship_capacity=200, calculated_transport_work=300) + cr = CalculationResult(results=[ry]) + assert isinstance(cr.results, list) + assert cr.results[0].year == 2023 diff --git a/python-impl/tests/test_models/test_dto/test_fuel_type_consumption.py b/python-impl/tests/test_models/test_dto/test_fuel_type_consumption.py new file mode 100644 index 0000000..37d1630 --- /dev/null +++ b/python-impl/tests/test_models/test_dto/test_fuel_type_consumption.py @@ -0,0 +1,15 @@ +import pytest +from open_imo_cii_calculator.models.dto.fuel_type_consumption import FuelTypeConsumption +from open_imo_cii_calculator.models.fuel_type import TypeOfFuel + +def test_fuel_type_consumption_init(): + """ + Test FuelTypeConsumption object creation and property assignment. + + Verifies: + - FuelTypeConsumption object is created with correct fuel type and consumption value. + - Properties are assigned as expected during initialization. + """ + ftc = FuelTypeConsumption(fuel_type=TypeOfFuel.DIESEL_OR_GASOIL, fuel_consumption=1000) + assert ftc.fuel_type == TypeOfFuel.DIESEL_OR_GASOIL + assert ftc.fuel_consumption == 1000 diff --git a/python-impl/tests/test_models/test_dto/test_result_year.py b/python-impl/tests/test_models/test_dto/test_result_year.py new file mode 100644 index 0000000..c39f493 --- /dev/null +++ b/python-impl/tests/test_models/test_dto/test_result_year.py @@ -0,0 +1,42 @@ +import pytest +from open_imo_cii_calculator.models.dto.result_year import ResultYear + +def test_result_year_init(): + """ + Test ResultYear object creation and property assignment. + + Verifies: + - ResultYear object is created with correct year, measured flag, rating, and other properties. + - Properties are assigned as expected during initialization. + """ + ry = ResultYear(is_measured_year=True, year=2023, attained_cii=1.0, required_cii=2.0, rating='A', vector_boundaries_for_year=None, calculated_co2e_emissions=100, calculated_ship_capacity=200, calculated_transport_work=300) + assert ry.year == 2023 + assert ry.is_measured_year is True + assert ry.rating == 'A' + +def test_is_estimated_year(): + """ + Test property logic for is_estimated_year in ResultYear. + + Verifies: + - is_estimated_year returns True when is_measured_year is False, and False otherwise. + - Ensures property logic is correct for measured/estimated year distinction. + """ + ry = ResultYear(is_measured_year=False, year=2023, attained_cii=1.0, required_cii=2.0, rating='A', vector_boundaries_for_year=None, calculated_co2e_emissions=100, calculated_ship_capacity=200, calculated_transport_work=300) + assert ry.is_estimated_year is True + ry2 = ResultYear(is_measured_year=True, year=2023, attained_cii=1.0, required_cii=2.0, rating='A', vector_boundaries_for_year=None, calculated_co2e_emissions=100, calculated_ship_capacity=200, calculated_transport_work=300) + assert ry2.is_estimated_year is False + +def test_attained_required_ratio(): + """ + Test attained_required_ratio property logic in ResultYear, including zero/negative required. + + Verifies: + - attained_required_ratio returns correct ratio when required_cii is positive. + - Returns 0.0 when required_cii is zero to avoid division by zero. + - Ensures robust calculation logic for edge cases. + """ + ry = ResultYear(is_measured_year=True, year=2023, attained_cii=10.0, required_cii=2.0, rating='A', vector_boundaries_for_year=None, calculated_co2e_emissions=100, calculated_ship_capacity=200, calculated_transport_work=300) + assert ry.attained_required_ratio == 5.0 + ry2 = ResultYear(is_measured_year=True, year=2023, attained_cii=10.0, required_cii=0, rating='A', vector_boundaries_for_year=None, calculated_co2e_emissions=100, calculated_ship_capacity=200, calculated_transport_work=300) + assert ry2.attained_required_ratio == 0.0 diff --git a/python-impl/tests/test_models/test_fuel_type.py b/python-impl/tests/test_models/test_fuel_type.py new file mode 100644 index 0000000..a346e17 --- /dev/null +++ b/python-impl/tests/test_models/test_fuel_type.py @@ -0,0 +1,16 @@ +import pytest +from open_imo_cii_calculator.models.fuel_type import TypeOfFuel + +def test_fuel_type_enum(): + """ + Test presence of all expected enum values in TypeOfFuel. + + Verifies: + - All required fuel type enum values are present and accessible in TypeOfFuel. + - Ensures enum covers all supported fuel types for logic elsewhere in the codebase. + """ + assert hasattr(TypeOfFuel, 'DIESEL_OR_GASOIL') + assert hasattr(TypeOfFuel, 'LIGHT_FUEL_OIL') + assert hasattr(TypeOfFuel, 'HEAVY_FUEL_OIL') + assert hasattr(TypeOfFuel, 'LIQUIFIED_NATURAL_GAS') + assert hasattr(TypeOfFuel, 'METHANOL') diff --git a/python-impl/tests/test_models/test_imo_cii_boundary.py b/python-impl/tests/test_models/test_imo_cii_boundary.py new file mode 100644 index 0000000..d685178 --- /dev/null +++ b/python-impl/tests/test_models/test_imo_cii_boundary.py @@ -0,0 +1,15 @@ +import pytest +from open_imo_cii_calculator.models.imo_cii_boundary import ImoCiiBoundary + +def test_imo_cii_boundary_enum(): + """ + Test presence of all expected enum values in ImoCiiBoundary. + + Verifies: + - All required boundary enum values are present and accessible in ImoCiiBoundary. + - Ensures enum covers all supported boundaries for logic elsewhere in the codebase. + """ + assert hasattr(ImoCiiBoundary, 'SUPERIOR') + assert hasattr(ImoCiiBoundary, 'LOWER') + assert hasattr(ImoCiiBoundary, 'UPPER') + assert hasattr(ImoCiiBoundary, 'INFERIOR') diff --git a/python-impl/tests/test_models/test_imo_cii_rating.py b/python-impl/tests/test_models/test_imo_cii_rating.py new file mode 100644 index 0000000..64b3889 --- /dev/null +++ b/python-impl/tests/test_models/test_imo_cii_rating.py @@ -0,0 +1,16 @@ +import pytest +from open_imo_cii_calculator.models.imo_cii_rating import ImoCiiRating + +def test_imo_cii_rating_enum(): + """ + Test presence of all expected enum values in ImoCiiRating. + + Verifies: + - All required rating enum values (A-E) are present and accessible in ImoCiiRating. + - Ensures enum covers all supported ratings for logic elsewhere in the codebase. + """ + assert hasattr(ImoCiiRating, 'A') + assert hasattr(ImoCiiRating, 'B') + assert hasattr(ImoCiiRating, 'C') + assert hasattr(ImoCiiRating, 'D') + assert hasattr(ImoCiiRating, 'E') diff --git a/python-impl/tests/test_models/test_measurement_models/__init__.py b/python-impl/tests/test_models/test_measurement_models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python-impl/tests/test_models/test_measurement_models/test_ship_dd_vector_boundaries.py b/python-impl/tests/test_models/test_measurement_models/test_ship_dd_vector_boundaries.py new file mode 100644 index 0000000..1606858 --- /dev/null +++ b/python-impl/tests/test_models/test_measurement_models/test_ship_dd_vector_boundaries.py @@ -0,0 +1,25 @@ +import pytest +from open_imo_cii_calculator.models.measurement_models.ship_dd_vector_boundaries import ShipDdVectorBoundaries +from open_imo_cii_calculator.models.ship_type import ShipType +from open_imo_cii_calculator.models.capacity_unit import CapacityUnit +from open_imo_cii_calculator.models.imo_cii_boundary import ImoCiiBoundary +from open_imo_cii_calculator.models.measurement_models.weight_classification import WeightClassification + +def test_ship_dd_vector_boundaries_init(): + """ + Test initialization and property assignment for ShipDdVectorBoundaries. + + Verifies: + - ShipDdVectorBoundaries object is created with correct ship type, weight classification, capacity unit, boundaries, and year. + - boundary_dd_vectors is a dictionary containing expected boundaries. + """ + wc = WeightClassification(2000, 1000) + boundaries = ShipDdVectorBoundaries( + ship_type=ShipType.BULK_CARRIER, + weight_classification=wc, + capacity_unit=CapacityUnit.DWT, + boundary_dd_vectors={ImoCiiBoundary.SUPERIOR: 1, ImoCiiBoundary.LOWER: 2}, + year=2023 + ) + assert isinstance(boundaries.boundary_dd_vectors, dict) + assert ImoCiiBoundary.SUPERIOR in boundaries.boundary_dd_vectors diff --git a/python-impl/tests/test_models/test_measurement_models/test_weight_classification.py b/python-impl/tests/test_models/test_measurement_models/test_weight_classification.py new file mode 100644 index 0000000..d217e7f --- /dev/null +++ b/python-impl/tests/test_models/test_measurement_models/test_weight_classification.py @@ -0,0 +1,14 @@ +import pytest +from open_imo_cii_calculator.models.measurement_models.weight_classification import WeightClassification + +def test_weight_classification_init(): + """ + Test initialization and property assignment for WeightClassification. + + Verifies: + - WeightClassification object is created with correct upper and lower limits. + - Properties are assigned as expected during initialization. + """ + wc = WeightClassification(2000, 1000) + assert wc.upper_limit == 2000 + assert wc.lower_limit == 1000 diff --git a/python-impl/tests/test_models/test_ship_models/__init__.py b/python-impl/tests/test_models/test_ship_models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python-impl/tests/test_models/test_ship_models/test_ship.py b/python-impl/tests/test_models/test_ship_models/test_ship.py new file mode 100644 index 0000000..f89ea01 --- /dev/null +++ b/python-impl/tests/test_models/test_ship_models/test_ship.py @@ -0,0 +1,16 @@ +import pytest +from open_imo_cii_calculator.models.ship_models.ship import Ship +from open_imo_cii_calculator.models.ship_type import ShipType + +def test_ship_init(): + """ + Test Ship object creation and property assignment. + + Verifies: + - Ship object is created with correct ship type, deadweight tonnage, and gross tonnage. + - Properties are assigned as expected during initialization. + """ + ship = Ship(ShipType.BULK_CARRIER, 50000, 30000) + assert ship.ship_type == ShipType.BULK_CARRIER + assert ship.deadweight_tonnage == 50000 + assert ship.gross_tonnage == 30000 diff --git a/python-impl/tests/test_models/test_ship_type.py b/python-impl/tests/test_models/test_ship_type.py new file mode 100644 index 0000000..55c187c --- /dev/null +++ b/python-impl/tests/test_models/test_ship_type.py @@ -0,0 +1,22 @@ +import pytest +from open_imo_cii_calculator.models.ship_type import ShipType + +def test_ship_type_enum(): + """ + Test presence of all expected enum values in ShipType. + + Verifies: + - All required ship type enum values are present and accessible in ShipType. + - Ensures enum covers all supported ship types for logic elsewhere in the codebase. + """ + assert hasattr(ShipType, 'BULK_CARRIER') + assert hasattr(ShipType, 'TANKER') + assert hasattr(ShipType, 'CONTAINER_SHIP') + assert hasattr(ShipType, 'GENERAL_CARGO_SHIP') + assert hasattr(ShipType, 'REFRIGERATED_CARGO_CARRIER') + assert hasattr(ShipType, 'RORO_CARGO_SHIP') + assert hasattr(ShipType, 'RORO_PASSENGER_SHIP') + assert hasattr(ShipType, 'LNG_CARRIER') + assert hasattr(ShipType, 'GAS_CARRIER') + assert hasattr(ShipType, 'CRUISE_PASSENGER_SHIP') + assert hasattr(ShipType, 'UNKNOWN') diff --git a/python-impl/tests/test_services/__init__.py b/python-impl/tests/test_services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python-impl/tests/test_services/test_carbon_intensity_indicator_calculator_service.py b/python-impl/tests/test_services/test_carbon_intensity_indicator_calculator_service.py new file mode 100644 index 0000000..d56cca6 --- /dev/null +++ b/python-impl/tests/test_services/test_carbon_intensity_indicator_calculator_service.py @@ -0,0 +1,85 @@ +import pytest +from open_imo_cii_calculator.services.carbon_intensity_indicator_calculator_service import CarbonIntensityIndicatorCalculatorService, ValType +from open_imo_cii_calculator.models.ship_type import ShipType + +class TestCarbonIntensityIndicatorCalculatorService: + def test_get_attained_carbon_intensity(self): + """ + Test attained CII calculation and error handling. + + Verifies: + - Correct attained CII is calculated for valid input. + - Exception is raised for zero transport work. + - Exception is raised for negative values. + """ + service = CarbonIntensityIndicatorCalculatorService() + # Normal case + result = service.get_attained_carbon_intensity(1000, 100) + assert result > 0 + # Zero transport work should raise + with pytest.raises(Exception): + service.get_attained_carbon_intensity(1000, 0) + # Negative values should raise + with pytest.raises(Exception): + service.get_attained_carbon_intensity(-1000, 100) + + def test_get_required_carbon_intensity(self): + """ + Test required CII calculation and error handling. + + Verifies: + - Required CII is calculated for valid ship type, tonnage, and year. + - Exception is raised for invalid ship type or year. + """ + service = CarbonIntensityIndicatorCalculatorService() + # Example: bulk carrier, year 2023 + result = service.get_required_carbon_intensity(ShipType.BULK_CARRIER, 50000, 2023) + assert result > 0 + # Invalid ship type or year should raise + with pytest.raises(Exception): + service.get_required_carbon_intensity(None, 50000, 2023) + + def test_get_required_carbon_intensity_dict(self): + """ + Test required CII calculation for all years as a dictionary. + + Verifies: + - Returns a dictionary of required CII values for all years for a given ship type and tonnage. + - Ensures the result contains expected years as keys. + """ + service = CarbonIntensityIndicatorCalculatorService() + # Should return a dict for all years + result = service.get_required_carbon_intensity_dict(ShipType.BULK_CARRIER, 50000) + assert isinstance(result, dict) + assert 2023 in result + + def test_get_value_and_ship_type_handlers(self): + """ + Test ship type handler logic for value retrieval. + + Verifies: + - _get_value returns a float or int for supported ship types and value types. + - Ensures handler logic works for all supported ship types. + """ + service = CarbonIntensityIndicatorCalculatorService() + # Only test supported ship types + supported_types = [ + ShipType.BULK_CARRIER, + ShipType.GAS_CARRIER, + ShipType.TANKER, + ShipType.CONTAINER_SHIP, + ShipType.GENERAL_CARGO_SHIP, + ShipType.REFRIGERATED_CARGO_CARRIER, + ShipType.COMBINATION_CARRIER, + ShipType.LNG_CARRIER, + ShipType.RORO_CARGO_SHIP_VEHICLE_CARRIER, + ShipType.RORO_CARGO_SHIP, + ShipType.RORO_PASSENGER_SHIP, + ShipType.RORO_PASSENGER_SHIP_HIGH_SPEED_SOLAS, + ShipType.CRUISE_PASSENGER_SHIP + ] + for ship_type in supported_types: + result_a = service._get_value(ValType.A, ship_type, 50000) + result_c = service._get_value(ValType.C, ship_type, 50000) + assert isinstance(result_a, (float, int)) + assert isinstance(result_c, (float, int)) diff --git a/python-impl/tests/test_services/test_rating_boundaries_service.py b/python-impl/tests/test_services/test_rating_boundaries_service.py new file mode 100644 index 0000000..2ceea9e --- /dev/null +++ b/python-impl/tests/test_services/test_rating_boundaries_service.py @@ -0,0 +1,64 @@ +import pytest +from open_imo_cii_calculator.services.rating_boundaries_service import RatingBoundariesService +from open_imo_cii_calculator.models.ship_type import ShipType +from open_imo_cii_calculator.models.ship_models.ship import Ship + +class TestRatingBoundariesService: + def test_get_boundaries_all_types(self): + """ + Test correct boundary calculation for all ship types. + + Verifies: + - Boundaries are returned as expected for a valid ship type, year, and required CII. + - The result contains a 'boundary_dd_vectors' attribute as a dictionary. + """ + service = RatingBoundariesService() + # Example: test for a bulk carrier, year 2023, required_cii 10 + ship = Ship(ShipType.BULK_CARRIER, 50000, 30000) + boundaries = service.get_boundaries(ship, 10, 2023) + assert hasattr(boundaries, 'boundary_dd_vectors') + assert isinstance(boundaries.boundary_dd_vectors, dict) + + def test_get_boundaries_invalid_type(self): + """ + Test error handling for invalid ship type or parameters. + + Verifies: + - An exception is raised when an invalid ship type or negative parameters are provided. + - Ensures robust error handling for boundary calculation. + """ + service = RatingBoundariesService() + # Use an invalid ship type or params to trigger error handling + with pytest.raises(Exception): + ship = Ship(None, -1, -1) + service.get_boundaries(ship, -1, 2023) + + def test_validate_ship_tonnage_valid(self): + """ + Test validation logic for valid ship tonnage parameters. + + Verifies: + - No exception is raised for valid tonnage values. + - Confirms that the validation logic accepts correct input. + """ + service = RatingBoundariesService() + # Should not raise for valid tonnage + ship = Ship(ShipType.BULK_CARRIER, 50000, 30000) + try: + service._validate_ship_tonnage_valid(ship) + except Exception: + pytest.fail("Unexpected exception for valid tonnage") + + def test_validate_ship_tonnage_invalid(self): + """ + Test error handling for invalid ship tonnage parameters. + + Verifies: + - An exception is raised for negative or invalid tonnage values. + - Ensures validation logic rejects incorrect input. + """ + service = RatingBoundariesService() + # Should raise for invalid tonnage + ship = Ship(ShipType.BULK_CARRIER, -1, -1) + with pytest.raises(Exception): + service._validate_ship_tonnage_valid(ship) diff --git a/python-impl/tests/test_services/test_reduction_factor_utils.py b/python-impl/tests/test_services/test_reduction_factor_utils.py new file mode 100644 index 0000000..1f5c960 --- /dev/null +++ b/python-impl/tests/test_services/test_reduction_factor_utils.py @@ -0,0 +1,38 @@ +import pytest +from open_imo_cii_calculator.services.reduction_factor_utils import get_annual_reduction_factor, apply_annual_reduction_factor + +def test_get_annual_reduction_factor_all_years(): + """ + Test annual reduction factor for all valid years. + + Verifies: + - get_annual_reduction_factor returns a non-negative factor for each year in the valid range (2019-2030). + - Ensures reduction factors are defined for all expected years. + """ + for year in range(2019, 2031): + factor = get_annual_reduction_factor(year) + assert factor >= 0 + +def test_get_annual_reduction_factor_out_of_range(): + """ + Test error handling for out-of-range years in reduction factor calculation. + + Verifies: + - Exception is raised when requesting a reduction factor for an unsupported year (e.g., 1900). + - Ensures robust error handling for invalid year input. + """ + with pytest.raises(Exception): + get_annual_reduction_factor(1900) + +def test_apply_annual_reduction_factor(): + """ + Test application of annual reduction factor to a base value. + + Verifies: + - apply_annual_reduction_factor returns a value less than or equal to the base for all valid years. + - Ensures reduction is applied correctly for each year in the valid range. + """ + base = 100 + for year in range(2019, 2031): + reduced = apply_annual_reduction_factor(base, year) + assert reduced <= base diff --git a/python-impl/tests/test_services/test_ship_capacity_calculator_service.py b/python-impl/tests/test_services/test_ship_capacity_calculator_service.py new file mode 100644 index 0000000..74a1282 --- /dev/null +++ b/python-impl/tests/test_services/test_ship_capacity_calculator_service.py @@ -0,0 +1,75 @@ +import pytest +from open_imo_cii_calculator.services.ship_capacity_calculator_service import ShipCapacityCalculatorService +from open_imo_cii_calculator.models.ship_type import ShipType + +class TestShipCapacityCalculatorService: + def test_get_ship_capacity_all_types(self): + """ + Test capacity calculation for all ship types. + + Verifies: + - Capacity is calculated and returned for a valid ship type and tonnage values. + - Ensures positive capacity for valid input. + """ + service = ShipCapacityCalculatorService() + # Example: bulk carrier + capacity = service.get_ship_capacity(ShipType.BULK_CARRIER, 50000, 30000) + assert capacity > 0 + + def test_get_ship_capacity_invalid(self): + """ + Test error handling for invalid tonnage parameters in capacity calculation. + + Verifies: + - An exception is raised when negative tonnage values are provided. + - Ensures validation logic rejects invalid input. + """ + service = ShipCapacityCalculatorService() + with pytest.raises(Exception): + service.get_ship_capacity(ShipType.BULK_CARRIER, -1, -1) + + def test_validate_tonnage_params_set(self): + """ + Test validation logic for tonnage parameters being set. + + Verifies: + - No exception is raised for valid tonnage parameters. + - Ensures validation logic accepts correct input. + """ + service = ShipCapacityCalculatorService() + # Should not raise for valid params + try: + service._validate_tonnage_params_set(ShipType.BULK_CARRIER, 50000, 30000) + except Exception: + pytest.fail("Unexpected exception for valid params") + + def test_validate_tonnage_params_invalid(self): + """ + Test error handling for unset tonnage parameters. + + Verifies: + - An exception is raised when tonnage parameters are None. + - Ensures validation logic rejects missing input. + """ + service = ShipCapacityCalculatorService() + with pytest.raises(Exception): + service._validate_tonnage_params_set(ShipType.BULK_CARRIER, None, None) + + def test_validate_tonnage(self): + """ + Test validation logic for individual tonnage values. + + Verifies: + - No exception is raised for positive tonnage values. + - An exception is raised for zero or negative tonnage values. + - Ensures robust validation for tonnage input. + """ + service = ShipCapacityCalculatorService() + # Should not raise for tonnage > 0 + try: + service._validate_tonnage(1000, "deadweight_tonnage", ShipType.BULK_CARRIER) + except Exception: + pytest.fail("Unexpected exception for valid tonnage") + # Should raise for tonnage <= 0 + with pytest.raises(Exception): + service._validate_tonnage(0, "deadweight_tonnage", ShipType.BULK_CARRIER) diff --git a/python-impl/tests/test_services/test_ship_mass_of_co2_emissions_calculator_service.py b/python-impl/tests/test_services/test_ship_mass_of_co2_emissions_calculator_service.py new file mode 100644 index 0000000..e584a30 --- /dev/null +++ b/python-impl/tests/test_services/test_ship_mass_of_co2_emissions_calculator_service.py @@ -0,0 +1,38 @@ +import pytest +from open_imo_cii_calculator.services.ship_mass_of_co2_emissions_calculator_service import ShipMassOfCo2EmissionsCalculatorService +from open_imo_cii_calculator.models.fuel_type import TypeOfFuel + +class TestShipMassOfCo2EmissionsCalculatorService: + @pytest.mark.parametrize( + "fuel_type,fuel_consumption,expected_result", + [ + (TypeOfFuel.DIESEL_OR_GASOIL, 1000, 3206), + (TypeOfFuel.LIGHT_FUEL_OIL, 1000, 3151), + (TypeOfFuel.HEAVY_FUEL_OIL, 1000, 3114), + (TypeOfFuel.LIQUIFIED_NATURAL_GAS, 1000, 2750), + (TypeOfFuel.METHANOL, 1000, 1375), + ] + ) + def test_get_mass_of_co2_emissions(self, fuel_type, fuel_consumption, expected_result): + """ + Test CO2 emission calculations for various fuel types. + + Verifies: + - Correct calculation of CO2 emissions for different fuel types and consumption amounts. + - Uses parameterized tests to cover multiple scenarios. + """ + service = ShipMassOfCo2EmissionsCalculatorService() + result = service.get_mass_of_co2_emissions(fuel_type, fuel_consumption) + assert result == expected_result + + def test_negative_fuel_consumption_raises_error(self): + """ + Test that providing negative fuel consumption raises a ValueError. + + Verifies: + - That a ValueError is raised when fuel consumption is negative. + - This ensures input validation for fuel consumption. + """ + service = ShipMassOfCo2EmissionsCalculatorService() + with pytest.raises(ValueError): + service.get_mass_of_co2_emissions(TypeOfFuel.DIESEL_OR_GASOIL, -100) diff --git a/python-impl/tests/test_services/test_ship_mass_of_co2_emissions_calculator_service_extra.py b/python-impl/tests/test_services/test_ship_mass_of_co2_emissions_calculator_service_extra.py new file mode 100644 index 0000000..ccf8ca8 --- /dev/null +++ b/python-impl/tests/test_services/test_ship_mass_of_co2_emissions_calculator_service_extra.py @@ -0,0 +1,70 @@ +import pytest +from open_imo_cii_calculator.services.ship_mass_of_co2_emissions_calculator_service import ShipMassOfCo2EmissionsCalculatorService +from open_imo_cii_calculator.models.fuel_type import TypeOfFuel + +class TestShipMassOfCo2EmissionsCalculatorService: + @pytest.mark.parametrize( + "fuel_type,expected_factor", + [ + (TypeOfFuel.DIESEL_OR_GASOIL, 3.206), + (TypeOfFuel.LIGHT_FUEL_OIL, 3.151), + (TypeOfFuel.HEAVY_FUEL_OIL, 3.114), + (TypeOfFuel.LIQUIFIED_NATURAL_GAS, 2.75), + (TypeOfFuel.METHANOL, 1.375), + ] + ) + def test_get_fuel_mass_conversion_factor(self, fuel_type, expected_factor): + """ + Test the retrieval of fuel mass conversion factors for various fuel types. + + Verifies: + - Correct conversion factor is returned for each specified fuel type. + - Uses parameterized tests to cover different fuel types. + """ + service = ShipMassOfCo2EmissionsCalculatorService() + factor = service.get_fuel_mass_conversion_factor(fuel_type) + assert abs(factor - expected_factor) < 0.01 + + @pytest.mark.parametrize( + "fuel_type,expected_content", + [ + (TypeOfFuel.DIESEL_OR_GASOIL, 0.875), + (TypeOfFuel.LIGHT_FUEL_OIL, 0.86), + (TypeOfFuel.HEAVY_FUEL_OIL, 0.85), + (TypeOfFuel.LIQUIFIED_NATURAL_GAS, 0.75), + (TypeOfFuel.METHANOL, 0.375), + ] + ) + def test_get_fuel_carbon_content(self, fuel_type, expected_content): + """ + Test the retrieval of fuel carbon content for various fuel types. + + Verifies: + - Correct carbon content is returned for each specified fuel type. + - Uses parameterized tests to cover different fuel types. + """ + service = ShipMassOfCo2EmissionsCalculatorService() + content = service.get_fuel_carbon_content(fuel_type) + assert abs(content - expected_content) < 0.01 + + @pytest.mark.parametrize( + "fuel_type,expected_lcv", + [ + (TypeOfFuel.DIESEL_OR_GASOIL, 42700), + (TypeOfFuel.LIGHT_FUEL_OIL, 41200), + (TypeOfFuel.HEAVY_FUEL_OIL, 40200), + (TypeOfFuel.LIQUIFIED_NATURAL_GAS, 48000), + (TypeOfFuel.METHANOL, 19900), + ] + ) + def test_get_fuel_lower_calorific_value(self, fuel_type, expected_lcv): + """ + Test the retrieval of lower calorific values (LCV) for various fuel types. + + Verifies: + - Correct LCV is returned for each specified fuel type. + - Uses parameterized tests to cover different fuel types. + """ + service = ShipMassOfCo2EmissionsCalculatorService() + lcv = service.get_fuel_lower_calorific_value(fuel_type) + assert abs(lcv - expected_lcv) < 1 diff --git a/python-impl/tests/test_services/test_ship_transport_work_calculator_service.py b/python-impl/tests/test_services/test_ship_transport_work_calculator_service.py new file mode 100644 index 0000000..be9eb83 --- /dev/null +++ b/python-impl/tests/test_services/test_ship_transport_work_calculator_service.py @@ -0,0 +1,26 @@ +import pytest +from open_imo_cii_calculator.services.ship_transport_work_calculator_service import ShipTransportWorkCalculatorService + +class TestShipTransportWorkCalculatorService: + def test_get_ship_transport_work(self): + """ + Test transport work calculation and error handling. + + Verifies: + - Correct transport work is calculated for valid input (multiplication of distance and capacity). + - Exception is raised for zero or negative distance or capacity values. + - Ensures robust input validation for transport work calculation. + """ + service = ShipTransportWorkCalculatorService() + # Normal case + result = service.get_ship_transport_work(1000, 100) + assert result == 100000 + # Zero or negative values should raise + with pytest.raises(Exception): + service.get_ship_transport_work(0, 100) + with pytest.raises(Exception): + service.get_ship_transport_work(1000, 0) + with pytest.raises(Exception): + service.get_ship_transport_work(-1000, 100) + with pytest.raises(Exception): + service.get_ship_transport_work(1000, -100) diff --git a/python-impl/tests/test_ship_carbon_intensity_calculator.py b/python-impl/tests/test_ship_carbon_intensity_calculator.py new file mode 100644 index 0000000..1ec32f3 --- /dev/null +++ b/python-impl/tests/test_ship_carbon_intensity_calculator.py @@ -0,0 +1,37 @@ +import pytest +from open_imo_cii_calculator.ship_carbon_intensity_calculator import ShipCarbonIntensityCalculator +from open_imo_cii_calculator.models.ship_type import ShipType +from open_imo_cii_calculator.models.fuel_type import TypeOfFuel +from open_imo_cii_calculator.models.dto.fuel_type_consumption import FuelTypeConsumption + +class TestShipCarbonIntensityCalculator: + def test_init(self): + """ + Test the initialization of the ShipCarbonIntensityCalculator. + + Verifies: + - That the ShipCarbonIntensityCalculator object is created successfully. + """ + calculator = ShipCarbonIntensityCalculator() + assert calculator is not None + + def test_calculate_attained_cii_rating_single_fuel(self, sample_ship_data, sample_fuel_consumption): + """ + Test the calculate_attained_cii_rating method with a single fuel type. + + Verifies: + - That the method returns a result object. + - That the result object has a 'results' attribute. + - This test uses sample data for ship parameters and fuel consumption. + """ + calculator = ShipCarbonIntensityCalculator() + result = calculator.calculate_attained_cii_rating( + sample_ship_data['ship_type'], + sample_ship_data['gross_tonnage'], + sample_ship_data['deadweight_tonnage'], + sample_ship_data['distance_travelled'], + sample_fuel_consumption, + sample_ship_data['target_year'] + ) + assert result is not None + assert hasattr(result, 'results')