diff --git a/Algorithms/Gas.cs b/Algorithms/Gas.cs
deleted file mode 100644
index 7b01ba6..0000000
--- a/Algorithms/Gas.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-namespace Algorithms;
-
-public class Gas
-{
- public string Name { get; }
- public float O2 { get; }
- public float He { get; }
- public float N2 { get; }
- public int TankSize { get; }
- public int Pressure { get; }
-
- public Gas(string name, float o2, float he, int tankSize, int pressure)
- {
- if (o2 <= 0) { throw new ArgumentException($"Invalid gas composition: O2 cannot be 0, was {o2}"); }
- if (o2 + he > 1) { throw new ArgumentException($"Invalid gas composition: O2 + He must not exceed 1.0, was {o2 + he}"); }
- if (tankSize <= 0) { throw new ArgumentException($"Invalid tank size: Tank size must be greater than 0, was {tankSize}"); }
- if (pressure <= 0) { throw new ArgumentException($"Invalid pressure: Pressure must be greater than 0, was {pressure}"); }
-
- Name = name;
- O2 = o2;
- He = he;
- N2 = 1 - (o2 + he);
- TankSize = tankSize;
- Pressure = pressure;
- }
-}
\ No newline at end of file
diff --git a/CLI/Program.cs b/CLI/Program.cs
deleted file mode 100644
index e5dff12..0000000
--- a/CLI/Program.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-// See https://aka.ms/new-console-template for more information
-
-Console.WriteLine("Hello, World!");
\ No newline at end of file
diff --git a/docs/UMLDiagrams/ZHL-16Diagrams/Step1.puml b/docs/UMLDiagrams/ZHL-16Diagrams/Step1.puml
new file mode 100644
index 0000000..ff7842f
--- /dev/null
+++ b/docs/UMLDiagrams/ZHL-16Diagrams/Step1.puml
@@ -0,0 +1,15 @@
+@startuml Step1_Initialise_Tissues
+title Step 1 – Initialise the Model (Initialise_Tissues Function)
+
+start
+:Call Initialise_Tissues();
+repeat
+ :For tissue i (1 to 16):
+ :Set Pn[i] = 0.79 * (ATM - WATERVAPOUR);
+ :Set Ph[i] = 0;
+ :Set Pt[i] = Pn[i] + Ph[i];
+repeat while (more tissues remain)
+:Return tissue arrays (Pn, Ph, Pt);
+stop
+
+@enduml
diff --git a/docs/UMLDiagrams/ZHL-16Diagrams/Step2.puml b/docs/UMLDiagrams/ZHL-16Diagrams/Step2.puml
new file mode 100644
index 0000000..383a33b
--- /dev/null
+++ b/docs/UMLDiagrams/ZHL-16Diagrams/Step2.puml
@@ -0,0 +1,43 @@
+@startuml Step2_Descent_Gas_Loadings
+title Step 2 – Descent Gas Loading (Descent_Gas_Loadings Function)
+
+start
+:Input Parameters;
+note right
+Depth, DESCENT_RATE, fN2, fHe, WATERVAPOUR
+end note
+
+:Calculate descent time t = Depth / DESCENT_RATE;
+
+repeat
+ :For each tissue i (1 to 16);
+
+ partition "For Nitrogen" {
+ note right
+ Use Schreiner Equation:
+ P = Pio + R*(t - 1/k) - (Pio - Po - R/k)*exp(-k*t)
+ end note
+ :Set Po = Pn[i];
+ :Set Pio = (Depth - WATERVAPOUR) * fN2;
+ :Set R = DESCENT_RATE * fN2;
+ :Calculate k = ln(2) / (nitrogen half-time[i]);
+ :Update Pn[i] = Pio + R*(t - 1/k) - (Pio - Po - R/k)*exp(-k*t);
+ }
+
+ partition "For Helium"
+ note right
+ Use Schreiner Equation:
+ P = Pio + R*(t - 1/k) - (Pio - Po - R/k)*exp(-k*t)
+ end note
+ :Set Po = Ph[i];
+ :Set Pio = (Depth - WATERVAPOUR) * fHe;
+ :Set R = DESCENT_RATE * fHe;
+ :Calculate k = ln(2) / (helium half-time[i]);
+ :Update Ph[i] = Pio + R*(t - 1/k) - (Pio - Po - R/k)*exp(-k*t);
+ }
+repeat while (more tissues remain)
+
+:Return updated tissue arrays (Pn, Ph);
+stop
+
+@enduml
diff --git a/docs/UMLDiagrams/ZHL-16Diagrams/Step3.puml b/docs/UMLDiagrams/ZHL-16Diagrams/Step3.puml
new file mode 100644
index 0000000..03baee6
--- /dev/null
+++ b/docs/UMLDiagrams/ZHL-16Diagrams/Step3.puml
@@ -0,0 +1,33 @@
+@startuml Step3_Dive_Loadings
+title Step 3 – Dive Loadings (Dive_Loadings Function)
+
+start
+
+repeat :For each tissue i (1 to 16):
+ partition "For Nitrogen" {
+ note right
+ Use Instantaneous Equation:
+ P = Po + (Pi - Po)*(1 - 2^(-t/half-time))
+ end note
+ :Set Po = Pn[i];
+ :Calculate Pi = (Depth - WATERVAPOUR) * fN2;
+ :Set t = Bottom Time;
+ :Update Pn[i] = Po + (Pi - Po) * (1 - 2^(-t / (nitrogen half-time[i])));
+ }
+
+ partition "For Helium" {
+ note right
+ Use Instantaneous Equation:
+ P = Po + (Pi - Po)*(1 - 2^(-t/half-time))
+ end note
+ :Set Po = Ph[i];
+ :Calculate Pi = (Depth - WATERVAPOUR) * fHe;
+ :Set t = Bottom Time;
+ :Update Ph[i] = Po + (Pi - Po) * (1 - 2^(-t / (helium half-time[i])));
+ }
+repeat while (more tissues remain)
+
+:Return updated tissue arrays (Pn, Ph);
+stop
+
+@enduml
diff --git a/docs/UMLDiagrams/ZHL-16Diagrams/Step3a.puml b/docs/UMLDiagrams/ZHL-16Diagrams/Step3a.puml
new file mode 100644
index 0000000..ac32373
--- /dev/null
+++ b/docs/UMLDiagrams/ZHL-16Diagrams/Step3a.puml
@@ -0,0 +1,12 @@
+@startuml
+start
+repeat :For each Tissue;
+:Calculate A = ((an2 * Pn) + (ahe * Ph)) / (Pn + Ph);
+:Calculate B = ((bn2 * Pn) + (bhe * Ph)) / (Pn + Ph);
+:Compute TissueCeiling = ((Pn + Ph) - A) * B;
+:Store TissueCeiling value;
+repeat while (more tissues remain?)
+ :Find the deepest (highest) TissueCeiling;
+ :Set Critical Ascent Ceiling = Highest Value;
+stop
+@enduml
diff --git a/docs/UMLDiagrams/ZHL-16Diagrams/Step4.puml b/docs/UMLDiagrams/ZHL-16Diagrams/Step4.puml
new file mode 100644
index 0000000..d22ff5c
--- /dev/null
+++ b/docs/UMLDiagrams/ZHL-16Diagrams/Step4.puml
@@ -0,0 +1,24 @@
+@startuml
+start
+
+:Set currentStop = FirstStop;
+:Set LastStop = 3msw or the desired last stop depth;
+
+repeat
+ :Get DecoGas for current stop;
+ :Set StopLength = 0;
+
+ repeat
+ :CalculateAtDepth|
+ :Depth = currentStop;
+ :Bottom Time = 1 min;
+ :GasMix = DecoGas;
+ :Increment StopLength by 1;
+ repeat while (AscentCeiling != (currentStop - 3msw))
+
+ :Total stop time = StopLength minutes;
+ :Move up to next stop depth (currentStop = currentStop - 3msw);
+
+
+stop
+@enduml
diff --git a/src/.github/workflows/qodana_code_quality.yml b/src/.github/workflows/qodana_code_quality.yml
new file mode 100644
index 0000000..5f23040
--- /dev/null
+++ b/src/.github/workflows/qodana_code_quality.yml
@@ -0,0 +1,40 @@
+#-------------------------------------------------------------------------------#
+# Discover additional configuration options in our documentation #
+# https://www.jetbrains.com/help/qodana/github.html #
+#-------------------------------------------------------------------------------#
+
+name: Qodana
+on:
+ workflow_dispatch:
+ pull_request:
+ push:
+ branches:
+ - dev
+ - master
+
+jobs:
+ qodana:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ pull-requests: write
+ checks: write
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event.pull_request.head.sha }}
+ fetch-depth: 0
+ - name: 'Qodana Scan'
+ uses: JetBrains/qodana-action@v2025.2
+ env:
+ QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}
+ with:
+ # When pr-mode is set to true, Qodana analyzes only the files that have been changed
+ pr-mode: false
+ use-caches: true
+ post-pr-comment: true
+ use-annotations: true
+ # Upload Qodana results (SARIF, other artifacts, logs) as an artifact to the job
+ upload-result: false
+ # quick-fixes available in Ultimate and Ultimate Plus plans
+ push-fixes: 'none'
\ No newline at end of file
diff --git a/Algorithms/Algorithms.csproj b/src/DecoBox.Algorithms/DecoBox.Algorithms.csproj
similarity index 81%
rename from Algorithms/Algorithms.csproj
rename to src/DecoBox.Algorithms/DecoBox.Algorithms.csproj
index 17b910f..1c2aaf3 100644
--- a/Algorithms/Algorithms.csproj
+++ b/src/DecoBox.Algorithms/DecoBox.Algorithms.csproj
@@ -4,6 +4,8 @@
net9.0
enable
enable
+ Algorithms
+
diff --git a/src/DecoBox.Algorithms/DiveLevel.cs b/src/DecoBox.Algorithms/DiveLevel.cs
new file mode 100644
index 0000000..bb8af6d
--- /dev/null
+++ b/src/DecoBox.Algorithms/DiveLevel.cs
@@ -0,0 +1,66 @@
+namespace Algorithms;
+
+///
+/// Represents a level in a dive profile.
+///
+public class DiveLevel
+{
+ ///
+ /// Represents the depth of the dive level in bar.
+ ///
+ public float Depth;
+
+ ///
+ /// Represents the time spent at this dive level in minutes.
+ ///
+ public int Time;
+
+ ///
+ /// Represents the gas used at this dive level. Uses a Gas object.
+ ///
+ public Gas LevelGas;
+
+ ///
+ /// Initializes a new instance of the class with the specified parameters
+ ///
+ /// Depth of the dive level in bar.
+ /// Time spent at depth in metres.
+ /// The gas used in the dive level. See .
+ ///
+ /// Thrown when any of the following are true:
+ ///
+ /// - Depth is less than or equal to 0 bar.
+ /// - Time is less than or equal to 0 minutes.
+ ///
+ ///
+ /// Thrown when no gas is supplied.
+ public DiveLevel(float depth, int time, Gas levelGas)
+ {
+ if (depth <= 0f) { throw new ArgumentException("Error: Depth must be greater than 0 bar."); }
+ if (time <= 0) { throw new ArgumentException("Error: Time must be at least 1 minute."); }
+ ArgumentNullException.ThrowIfNull(levelGas);
+
+ Depth = depth;
+ Time = time;
+ LevelGas = levelGas;
+ }
+
+ ///
+ /// Returns a string representation of the DiveLevel.
+ ///
+ /// A human-readable representation of the dive level
+ public override string ToString()
+ {
+ return $"Depth: {Depth} bar, Time: {Time} min, Gas: {LevelGas.Name}";
+ }
+
+ ///
+ /// Converts the depth from bar to meters.
+ ///
+ /// The depth of the DiveLevel instance in metres.
+ public double GetDepthInMeters()
+ {
+ return (Depth - 1) * 10;
+ }
+}
+
diff --git a/src/DecoBox.Algorithms/Gas.cs b/src/DecoBox.Algorithms/Gas.cs
new file mode 100644
index 0000000..5972e60
--- /dev/null
+++ b/src/DecoBox.Algorithms/Gas.cs
@@ -0,0 +1,77 @@
+namespace Algorithms;
+
+///
+/// Rrepresents a breathing gas mixture used in diving.
+///
+///
+/// The gas is defined by it's oxygen () and helium () fractions. The nitrogen () is calcuated from the remaining percent.
+///
+public class Gas
+{
+ ///
+ /// The name of the tank for identification purposes.
+ ///
+ public string Name { get; }
+
+ ///
+ /// The percentage of oxygen in the gas mixture. Expressed as a fraction (e.g., 0.21 for 21%).
+ ///
+ public float O2 { get; }
+ ///
+ /// The percentage of helium in the gas mixture. Expressed as a fraction (e.g., 0.35 for 35%).
+ public float He { get; }
+ ///
+ /// The percentage of nitrogen in the gas mixture. Calculated as the remainder after accounting for oxygen and helium. Expressed as a fraction (e.g., 0.79 for 79%).
+ ///
+ public float N2 { get; }
+ ///
+ /// The tank size in liters.
+ ///
+ public int TankSize { get; }
+ ///
+ /// The pressure of the gas in the tank, measured in bar.
+ ///
+ public int Pressure { get; }
+
+ ///
+ /// Initializes a new instance of the class with the specified parameters.
+ ///
+ /// Name of the tank for ID purposes.
+ /// The percentage of oxygen expressed as a fraction.
+ /// The percentage of helium expressed as a fraction.
+ /// The size of the tank in litres.
+ /// The pressure of the tank in bar.
+ ///
+ /// Thrown when any of the following are true:
+ ///
+ /// - O2 is less than or equal to 0.
+ /// - The sum of O2 and He exceeds 1.0.
+ /// - Tank size is less than or equal to 0.
+ /// - Pressure is less than or equal to 0.
+ ///
+ ///
+ public Gas(string name, float o2, float he, int tankSize, int pressure)
+ {
+ if (o2 <= 0) { throw new ArgumentException($"Invalid gas composition: O2 cannot be 0, was {o2}"); }
+ if (o2 + he > 1) { throw new ArgumentException($"Invalid gas composition: O2 + He must not exceed 1.0, was {o2 + he}"); }
+ if (tankSize <= 0) { throw new ArgumentException($"Invalid tank size: Tank size must be greater than 0, was {tankSize}"); }
+ if (pressure <= 0) { throw new ArgumentException($"Invalid pressure: Pressure must be greater than 0, was {pressure}"); }
+
+ Name = name;
+ O2 = o2;
+ He = he;
+ N2 = 1 - (o2 + he);
+ TankSize = tankSize;
+ Pressure = pressure;
+ }
+
+ ///
+ /// Returns the maximum operating depth (MOD) for the gas at a given partial pressure of oxygen (PO2).
+ ///
+ /// The partial pressure of the tank (e.g., 1.4)
+ /// A float of the maximum operating depth of the tank.
+ public float CalculateMOD(float PO2)
+ {
+ return ((PO2 / O2) - 1) * 10;
+ }
+}
\ No newline at end of file
diff --git a/src/DecoBox.Algorithms/ZHL-16/Compartment.cs b/src/DecoBox.Algorithms/ZHL-16/Compartment.cs
new file mode 100644
index 0000000..4ff2f3a
--- /dev/null
+++ b/src/DecoBox.Algorithms/ZHL-16/Compartment.cs
@@ -0,0 +1,113 @@
+namespace Algorithms.ZHL_16;
+
+///
+/// Represents a single ZHL-16 tissue compartment used for decompression
+/// calculations. Each compartment stores half-times and model coefficients
+/// for nitrogen (N2) and helium (He), as well as the current partial
+/// pressures used during calculations.
+///
+public record Compartment
+{
+ ///
+ /// The nitrogen (N2) half-time for this compartment (minutes).
+ ///
+ public double HalfTimeN2 { get; set; }
+
+ ///
+ /// The helium (He) half-time for this compartment (minutes).
+ ///
+ public double HalfTimeHe { get; set; }
+
+ ///
+ /// ZHL model coefficient a for nitrogen for this compartment.
+ ///
+ public double A_N2 { get; set; }
+
+ ///
+ /// ZHL model coefficient b for nitrogen for this compartment.
+ ///
+ public double B_N2 { get; set; }
+
+ ///
+ /// ZHL model coefficient a for helium for this compartment.
+ ///
+ public double A_He { get; set; }
+
+ ///
+ /// ZHL model coefficient b for helium for this compartment.
+ ///
+ public double B_He { get; set; }
+
+ ///
+ /// Current partial pressure of nitrogen in this compartment (bar).
+ /// This value is updated by the decompression algorithm during simulation.
+ ///
+ public double P_N2 { get; set; }
+
+ ///
+ /// Current partial pressure of helium in this compartment (bar).
+ ///
+ public double P_He { get; set; }
+
+ ///
+ /// Current total inert gas pressure (N2 + He) in this compartment (bar).
+ ///
+ public double P_Total { get; set; }
+
+
+ ///
+ /// Creates a new compartment with the specified half-times and ZHL
+ /// coefficients. The partial pressures are initialized to their default
+ /// values (0.0) and should be set by the calling code or algorithm.
+ ///
+ /// Nitrogen half-time in minutes.
+ /// Helium half-time in minutes.
+ /// ZHL a coefficient for nitrogen.
+ /// ZHL b coefficient for nitrogen.
+ /// ZHL a coefficient for helium.
+ /// ZHL b coefficient for helium.
+ public Compartment(double halfTimeN2, double halfTimeHe, double aN2, double bN2, double aHe, double bHe)
+ {
+ HalfTimeN2 = halfTimeN2;
+ HalfTimeHe = halfTimeHe;
+ this.A_N2 = aN2;
+ this.B_N2 = bN2;
+ this.A_He = aHe;
+ this.B_He = bHe;
+ }
+
+ ///
+ /// Internal constructor that also sets partial pressures. This is useful
+ /// when creating a compartment with a pre-existing gas state.
+ ///
+ /// Nitrogen half-time in minutes.
+ /// Helium half-time in minutes.
+ /// ZHL a coefficient for nitrogen.
+ /// ZHL b coefficient for nitrogen.
+ /// ZHL a coefficient for helium.
+ /// ZHL b coefficient for helium.
+ /// Initial nitrogen partial pressure (bar).
+ /// Initial helium partial pressure (bar).
+ /// Initial total inert gas pressure (bar).
+ private Compartment(double halfTimeN2, double halfTimeHe, double aN2, double bN2, double aHe, double bHe, double pn2, double pHe, double pTotal)
+ {
+ HalfTimeN2 = halfTimeN2;
+ HalfTimeHe = halfTimeHe;
+ this.A_N2 = aN2;
+ this.B_N2 = bN2;
+ this.A_He = aHe;
+ this.B_He = bHe;
+ P_N2 = pn2;
+ P_He = pHe;
+ P_Total = pTotal;
+ }
+
+ ///
+ /// Returns a human-readable representation of the compartment including
+ /// the half-times, coefficients, and current partial pressures.
+ ///
+ public override string ToString()
+ {
+ return $"Compartment( HalfTimeN2: {HalfTimeN2}, HalfTimeHe: {HalfTimeHe}, A_N2: {A_N2}, B_N2: {B_N2}, A_He: {A_He}, B_He: {B_He}, P_N2: {P_N2}, P_He: {P_He}, P_Total: {P_Total} )";
+ }
+}
\ No newline at end of file
diff --git a/src/DecoBox.Algorithms/ZHL-16/Zhl16.cs b/src/DecoBox.Algorithms/ZHL-16/Zhl16.cs
new file mode 100644
index 0000000..f310ced
--- /dev/null
+++ b/src/DecoBox.Algorithms/ZHL-16/Zhl16.cs
@@ -0,0 +1,209 @@
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Algorithms.ZHL_16;
+
+///
+/// A representation of the ZHL-16 decompression algorithm.
+///
+///
+/// Important notes:
+///
+/// - All pressures, depths etc. are expressed in bar to simplify calculations (1 bar ≈ 10 metres of seawater).
+///
+///
+///
+public class Zhl16
+{
+ ///
+ /// Name of the algorithm for identification purposes.
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// A list of the dive levels for the dive profile.
+ ///
+ public List DiveLevels { get; set; }
+
+ ///
+ /// A list of the gases used in the dive.
+ ///
+ public List GasList { get; set; }
+
+ ///
+ /// The compartments used in the algorithm.
+ ///
+ public List CompartmentList { get; set; }
+
+ ///
+ /// Atmospheric pressure in bar. Default is 1 bar for sea level.
+ ///
+ public float Atm;
+
+ ///
+ /// Pressure of water vapour in bar. Default is 0.06267 bar (approx. 47 mmHg at 37°C).
+ ///
+ private float Watervapour;
+
+ ///
+ /// Initialises a new instance of the class with the specified parameters.
+ ///
+ /// Name of the ZHL16 instance for identification purposes.
+ /// A list of the class used for determining which levels to calculate.
+ /// A list of the class used for determining the gases to be used during the dive.
+ /// The pressure of water vapour in bar. Default to 0.06267.
+ /// The pressure of the diver at the surface. Default is sea level (1 bar).
+ /// Thrown when any of the following are true:
+ ///
+ /// - The name is null or empty.
+ /// - Atmospheric pressure is less than or equal to 0 bar.
+ /// - Water vapour pressure is negative.
+ ///
+ ///
+ /// Thrown when any of the following are true:
+ ///
+ /// - No dive levels are supplied.
+ /// - No gases are supplied.
+ ///
+ ///
+ public Zhl16(string name, List diveLevels, List gasList, float watervapour = 0.06267f, float atm = 1)
+ {
+ if (string.IsNullOrWhiteSpace(name)) { throw new ArgumentException("Algorithm name cannot be null or empty.", nameof(name)); }
+ if (atm <= 0) { throw new ArgumentException("Atmospheric pressure must be greater than 0 bar."); }
+ if (watervapour < 0) { throw new ArgumentException("Water vapour pressure cannot be negative."); }
+
+ if (diveLevels.Count == 0)
+ {
+ throw new WarningException("DiveLevels is empty.");
+ }
+
+ if (gasList.Count == 0)
+ {
+ throw new WarningException("GasList is empty.");
+ }
+
+ Name = name;
+ DiveLevels = diveLevels;
+ GasList = gasList;
+ Atm = atm;
+ Watervapour = watervapour;
+ CompartmentList = ImportCompartments(true); // TODO: Remove me!
+ }
+
+ ///
+ /// Initialises the compartments to starting values at the surface. Must be completed before any calculations.
+ ///
+ public void Initialise()
+ {
+ foreach (Compartment compartment in CompartmentList)
+ {
+ compartment.P_N2 = 0.79 * (Atm - Watervapour);
+ compartment.P_He = 0.0;
+ compartment.P_Total = compartment.P_N2 + compartment.P_He;
+ }
+ }
+
+ ///
+ /// Calculates the tissue compartment pressures during descent.
+ ///
+ ///
+ /// Descent calculations use Schreiner's equation to determine the inert gas pressures in each compartment during descent.
+ /// All pressures are expressed in bar, and descent rate is in metres per minute (converted to bar per minute internally). - TODO: This needs to be changed in order to be consistent.
+ ///
+ /// The dive level to descend to.
+ /// The gas used during the descent.
+ /// Descent rate in metres a minute.
+ public void CalculateDescent(DiveLevel diveLevel, Gas gas, float descentRate)
+ {
+ var descentTime = diveLevel.Depth / (descentRate / 10.0f);
+
+ foreach (Compartment compartment in CompartmentList)
+ {
+ // Nitrogen calculation
+ var Po = compartment.P_N2;
+ var Pio = (diveLevel.Depth - Watervapour) * gas.N2;
+ var R = (descentRate / 10) * gas.N2;
+ var k = Math.Log(2) / compartment.HalfTimeN2;
+
+ compartment.P_N2 = Pio + R * (descentTime - (1 / k)) - (Pio - Po - R / k) * Math.Exp(-k * descentTime);
+
+ // Helium calculation
+ Po = compartment.P_He;
+ Pio = (diveLevel.Depth - Watervapour) * gas.He;
+ R = (descentRate / 10) * gas.He;
+ k = Math.Log(2) / compartment.HalfTimeHe;
+ compartment.P_He = Pio + R * (descentTime - (1 / k)) - (Pio - Po - R / k) * Math.Exp(-k * descentTime);
+
+ compartment.P_Total = compartment.P_N2 + compartment.P_He;
+ }
+ }
+
+ public void CalculateAtDepth()
+ {
+ throw new NotImplementedException();
+ }
+
+ public double CalculateAscentCeiling()
+ {
+ throw new NotImplementedException();
+ }
+
+ public double CalculateDecompression()
+ {
+ throw new NotImplementedException();
+ }
+
+ ///
+ /// Imports compartment data from a CSV file or uses embedded data if specified.
+ ///
+ /// Flag to determine if the default ZHL-16C compartments should be used.
+ /// A list of compartments.
+ private static List ImportCompartments(bool useEmbedded = false)
+ {
+ if (!useEmbedded)
+ {
+ const string filePath = "compartments.csv";
+ var compartments = new List();
+
+ using var reader = new StreamReader(filePath);
+ reader.ReadLine();
+
+ while (!reader.EndOfStream)
+ {
+ var line = reader.ReadLine();
+ var values = line.Split(',');
+
+ compartments.Add(new Compartment(
+ double.Parse(values[0]),
+ double.Parse(values[1]),
+ double.Parse(values[2]),
+ double.Parse(values[3]),
+ double.Parse(values[4]),
+ double.Parse(values[5])));
+ }
+
+ return compartments;
+ }
+
+ // Backup embedded compartment data
+ return new List
+ {
+ new Compartment(5.0, 1.1696, 0.5578, 1.88, 1.6189, 0.4770),
+ new Compartment(8.0, 1.0000, 0.6514, 3.02, 1.3830, 0.5747),
+ new Compartment(12.5, 0.8618, 0.7222, 4.72, 1.1919, 0.6527),
+ new Compartment(18.5, 0.7562, 0.7825, 6.99, 1.0458, 0.7223),
+ new Compartment(27.0, 0.6200, 0.8126, 10.21, 0.9220, 0.7582),
+ new Compartment(38.3, 0.5043, 0.8434, 14.48, 0.8205, 0.7957),
+ new Compartment(54.3, 0.4410, 0.8693, 20.53, 0.7305, 0.8279),
+ new Compartment(77.0, 0.4000, 0.8910, 29.11, 0.6502, 0.8553),
+ new Compartment(109.0, 0.3750, 0.9092, 41.20, 0.5950, 0.8757),
+ new Compartment(146.0, 0.3500, 0.9222, 55.19, 0.5545, 0.8903),
+ new Compartment(187.0, 0.3295, 0.9319, 70.69, 0.5333, 0.8997),
+ new Compartment(239.0, 0.3065, 0.9403, 90.34, 0.5189, 0.9073),
+ new Compartment(305.0, 0.2835, 0.9477,115.29, 0.5181, 0.9122),
+ new Compartment(390.0, 0.2610, 0.9544,147.42, 0.5176, 0.9171),
+ new Compartment(498.0, 0.2480, 0.9602,188.24, 0.5172, 0.9217),
+ new Compartment(635.0, 0.2327, 0.9653,240.03, 0.5119, 0.9267)
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/DecoBox.Algorithms/ZHL-16/compartments.csv b/src/DecoBox.Algorithms/ZHL-16/compartments.csv
new file mode 100644
index 0000000..4727f05
--- /dev/null
+++ b/src/DecoBox.Algorithms/ZHL-16/compartments.csv
@@ -0,0 +1,17 @@
+hN2,hHe,aN2,bN2,aHe,bHe
+5.0,1.88,1.1696,0.5578,1.6189,0.477
+8.0,3.02,1.0,0.6514,1.383,0.5747
+12.5,4.72,0.8618,0.7222,1.1919,0.6527
+18.5,6.99,0.7562,0.7825,1.0458,0.7223
+27.0,10.21,0.62,0.8126,0.922,0.7582
+38.3,14.48,0.5043,0.8434,0.8205,0.7957
+54.3,20.53,0.441,0.8693,0.7305,0.8279
+77.0,29.11,0.4,0.891,0.6502,0.8553
+109.0,41.2,0.375,0.9092,0.595,0.8757
+146.0,55.19,0.35,0.9222,0.5545,0.8903
+187.0,70.69,0.3295,0.9319,0.5333,0.8997
+239.0,90.34,0.3065,0.9403,0.5189,0.9073
+305.0,115.29,0.2835,0.9477,0.5181,0.9122
+390.0,147.42,0.261,0.9544,0.5176,0.9171
+498.0,188.24,0.248,0.9602,0.5172,0.9217
+635.0,240.03,0.2327,0.9653,0.5119,0.9267
diff --git a/CLI/CLI.csproj b/src/DecoBox.CLI/DecoBox.CLI.csproj
similarity index 61%
rename from CLI/CLI.csproj
rename to src/DecoBox.CLI/DecoBox.CLI.csproj
index 85b4959..0351052 100644
--- a/CLI/CLI.csproj
+++ b/src/DecoBox.CLI/DecoBox.CLI.csproj
@@ -5,6 +5,11 @@
net9.0
enable
enable
+ CLI
+
+
+
+
diff --git a/src/DecoBox.CLI/Program.cs b/src/DecoBox.CLI/Program.cs
new file mode 100644
index 0000000..7b94668
--- /dev/null
+++ b/src/DecoBox.CLI/Program.cs
@@ -0,0 +1,18 @@
+using System;
+using Algorithms;
+using Algorithms.ZHL_16;
+
+namespace CLI;
+
+internal class Program
+{
+ public static void Main(string[] args)
+ {
+ Zhl16 zhl16 = new Zhl16("ZHL-16C", new List(), new List());
+
+ foreach (Compartment compartment in zhl16.CompartmentList)
+ {
+ Console.WriteLine(compartment);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/DecoBox.Tests/CompartmentTest.cs b/src/DecoBox.Tests/CompartmentTest.cs
new file mode 100644
index 0000000..ef969ec
--- /dev/null
+++ b/src/DecoBox.Tests/CompartmentTest.cs
@@ -0,0 +1,82 @@
+using Algorithms.ZHL_16;
+
+namespace Tests
+{
+ [TestClass]
+ public class CompartmentTests
+ {
+ [TestMethod]
+ public void Constructor_ShouldInitializeAllFields()
+ {
+ var c = new Compartment(5.0, 10.0, 1.0, 0.5, 1.1, 0.6);
+
+ Assert.AreEqual(5.0, c.HalfTimeN2);
+ Assert.AreEqual(10.0, c.HalfTimeHe);
+ Assert.AreEqual(1.0, c.A_N2);
+ Assert.AreEqual(0.5, c.B_N2);
+ Assert.AreEqual(1.1, c.A_He);
+ Assert.AreEqual(0.6, c.B_He);
+ Assert.AreEqual(0, c.P_N2);
+ Assert.AreEqual(0, c.P_He);
+ Assert.AreEqual(0, c.P_Total);
+ }
+
+ [TestMethod]
+ public void ToString_ShouldIncludeAllFields()
+ {
+ var c = new Compartment(5, 10, 1, 0.5, 1.1, 0.6)
+ {
+ P_N2 = 0.79,
+ P_He = 0.0,
+ P_Total = 0.79
+ };
+
+ var s = c.ToString();
+
+ StringAssert.Contains(s, "HalfTimeN2: 5");
+ StringAssert.Contains(s, "P_Total: 0.79");
+ }
+
+ [TestMethod]
+ public void Records_WithSameValues_ShouldBeEqual()
+ {
+ var c1 = new Compartment(5, 10, 1, 0.5, 1.1, 0.6);
+ var c2 = new Compartment(5, 10, 1, 0.5, 1.1, 0.6);
+
+ Assert.AreEqual(c1, c2);
+ Assert.IsTrue(c1 == c2);
+ }
+
+ [TestMethod]
+ public void Records_WithDifferentValues_ShouldNotBeEqual()
+ {
+ var c1 = new Compartment(5, 10, 1, 0.5, 1.1, 0.6);
+ var c2 = new Compartment(6, 10, 1, 0.5, 1.1, 0.6);
+
+ Assert.AreNotEqual(c1, c2);
+ }
+
+ [TestMethod]
+ public void WithExpression_ShouldCopyAndModify()
+ {
+ var c1 = new Compartment(5, 10, 1, 0.5, 1.1, 0.6) { P_N2 = 0.8 };
+ var c2 = c1 with { P_N2 = 0.9 };
+
+ Assert.AreEqual(0.8, c1.P_N2);
+ Assert.AreEqual(0.9, c2.P_N2);
+ Assert.AreNotSame(c1, c2);
+ }
+
+ [TestMethod]
+ public void AllowEdgeValues_ShouldHandleNaNAndInfinity()
+ {
+ var c = new Compartment(5, 10, 1, 0.5, 1.1, 0.6);
+
+ c.P_N2 = double.NaN;
+ c.P_He = double.PositiveInfinity;
+
+ Assert.IsTrue(double.IsNaN(c.P_N2));
+ Assert.IsTrue(double.IsPositiveInfinity(c.P_He));
+ }
+ }
+}
diff --git a/Tests/Tests.csproj b/src/DecoBox.Tests/DecoBox.Tests.csproj
similarity index 81%
rename from Tests/Tests.csproj
rename to src/DecoBox.Tests/DecoBox.Tests.csproj
index 0cdbf2e..0630dd0 100644
--- a/Tests/Tests.csproj
+++ b/src/DecoBox.Tests/DecoBox.Tests.csproj
@@ -5,6 +5,7 @@
latest
enable
enable
+ Tests
@@ -17,7 +18,7 @@
-
+
diff --git a/src/DecoBox.Tests/DiveLevelTest.cs b/src/DecoBox.Tests/DiveLevelTest.cs
new file mode 100644
index 0000000..8cfc98c
--- /dev/null
+++ b/src/DecoBox.Tests/DiveLevelTest.cs
@@ -0,0 +1,55 @@
+using Algorithms;
+
+namespace Tests
+{
+ [TestClass]
+ public class DiveLevelTests
+ {
+ private static Gas testGas = new Gas("Air", 0.21f, 0.0f, 24, 232);
+
+ [TestMethod]
+ public void DiveLevelTest_Initialize()
+ {
+ var level = new DiveLevel(3f, 10, testGas);
+
+ Assert.AreEqual(3f, level.Depth);
+ Assert.AreEqual(10, level.Time);
+ Assert.AreEqual(testGas, level.LevelGas);
+ }
+
+ [TestMethod]
+ public void DiveLevelTest_ToMetres()
+ {
+ var level = new DiveLevel(3f, 10, testGas);
+
+ Assert.AreEqual(3f, level.Depth);
+ Assert.AreEqual(20f, level.GetDepthInMeters());
+ }
+
+ [TestMethod]
+ public void DiveLevelTest_InvalidDepthThrows()
+ {
+ Assert.ThrowsException(() =>
+ {
+ var level = new DiveLevel(0f, 10, testGas);
+ });
+ }
+
+ [TestMethod]
+ public void DiveLevelTest_InvalidTimeThrows()
+ {
+ Assert.ThrowsException(() =>
+ {
+ var level = new DiveLevel(3f, 0, testGas);
+ });
+ }
+
+ public void DiveLevelTest_NullGasThrows()
+ {
+ Assert.ThrowsException(() =>
+ {
+ var level = new DiveLevel(3f, 10, null);
+ });
+ }
+ }
+}
diff --git a/Tests/GasTest.cs b/src/DecoBox.Tests/GasTest.cs
similarity index 78%
rename from Tests/GasTest.cs
rename to src/DecoBox.Tests/GasTest.cs
index d119658..eec23a7 100644
--- a/Tests/GasTest.cs
+++ b/src/DecoBox.Tests/GasTest.cs
@@ -5,16 +5,8 @@ namespace Tests;
[TestClass]
public sealed class GasTest
{
- private static Gas backGas;
- private static Gas decoGas;
- private Gas invalidGas;
-
- [ClassInitialize]
- public static void GasTest_ClassInitialize(TestContext testContext)
- {
- backGas = new Gas("21/35", 0.21f, 0.35f, 24, 232);
- decoGas = new Gas("Nx 50", 0.5f, 0f, 11, 232);
- }
+ private Gas backGas = new Gas("21/35", 0.21f, 0.35f, 24, 232);
+ private Gas decoGas = new Gas("Nx 50", 0.5f, 0f, 11, 232);
[TestMethod]
public void GasTest_TestO2()
@@ -30,6 +22,13 @@ public void GasTest_TestHe()
Assert.AreEqual(0f, decoGas.He, 1e-6f);
}
+ [TestMethod]
+ public void GasTest_TestN2()
+ {
+ Assert.AreEqual(0.44f, backGas.N2, 1e-6f);
+ Assert.AreEqual(0.5f, decoGas.N2, 1e-6f);
+ }
+
[TestMethod]
public void GasTest_NameTankPressure()
{
@@ -83,4 +82,11 @@ public void GasTest_InvalidO2Throws()
var gas = new Gas("InvalidGas", 0f, 0.2f, 12, 220);
});
}
+
+ [TestMethod]
+ public void GasTest_CalculateMOD()
+ {
+ Assert.AreEqual(56.7f, backGas.CalculateMOD(1.4f), 0.1f);
+ Assert.AreEqual(22f, decoGas.CalculateMOD(1.6f), 0.1f);
+ }
}
\ No newline at end of file
diff --git a/Tests/MSTestSettings.cs b/src/DecoBox.Tests/MSTestSettings.cs
similarity index 100%
rename from Tests/MSTestSettings.cs
rename to src/DecoBox.Tests/MSTestSettings.cs
diff --git a/src/DecoBox.Tests/ZHL16Test.cs b/src/DecoBox.Tests/ZHL16Test.cs
new file mode 100644
index 0000000..bad59dd
--- /dev/null
+++ b/src/DecoBox.Tests/ZHL16Test.cs
@@ -0,0 +1,91 @@
+using System.ComponentModel;
+using Algorithms;
+using Algorithms.ZHL_16;
+
+namespace Tests;
+
+[TestClass]
+public class Zhl16Test
+{
+ private static List testGases = new List();
+ private static List testDiveLevels = new List();
+
+ [ClassInitialize]
+ public static void ClassInit(TestContext testContext)
+ {
+ testGases.Add(new Gas("18/45", 0.18f, 0.45f, 24, 232));
+ testDiveLevels.Add(new DiveLevel(6f, 20, testGases[0]));
+ }
+
+ [TestMethod]
+ public void Zhl16Test_ConstructorTest()
+ {
+ Zhl16 _zhl16Model = new Zhl16("ZHL-16C", testDiveLevels, testGases);
+ Assert.AreEqual("ZHL-16C", _zhl16Model.Name);
+ Assert.AreEqual(16, _zhl16Model.CompartmentList.Count);
+ }
+
+ [TestMethod]
+ public void Zhl16Test_InvalidNameThrows()
+ {
+ Assert.ThrowsException(() =>
+ {
+ Zhl16 _zhl16Model = new Zhl16(null, testDiveLevels, testGases);
+ });
+ }
+
+ [TestMethod]
+ public void Zhl16Test_InvalidAtmThrows()
+ {
+ Assert.ThrowsException(() =>
+ {
+ Zhl16 _zhl16Model = new Zhl16("ZHL-16C", testDiveLevels, testGases, atm: 0.0f);
+ });
+ }
+
+ [TestMethod]
+ public void Zhl16Test_InvalidWaterVapourPressureThrows()
+ {
+ Assert.ThrowsException(() =>
+ {
+ Zhl16 _zhl16Model = new Zhl16("ZHL-16C", testDiveLevels, testGases, watervapour: -1.0f);
+ });
+ }
+
+ [TestMethod]
+ public void Zhl16Test_EmptyDiveLevelsThrows()
+ {
+ Assert.ThrowsException(() =>
+ {
+ Zhl16 _zhl16Model = new Zhl16("ZHL-16C", new List(), testGases);
+ });
+ }
+
+ [TestMethod]
+ public void Zhl16Test_EmptyGasListThrows()
+ {
+ Assert.ThrowsException(() =>
+ {
+ Zhl16 _zhl16Model = new Zhl16("ZHL-16C", testDiveLevels, new List());
+ });
+ }
+
+ [TestMethod]
+ public void Zhl16_Initalisation_Test()
+ {
+ Zhl16 _zhl16Model = new Zhl16("ZHL-16C", testDiveLevels, testGases);
+ _zhl16Model.Initialise();
+ Assert.AreEqual(0.740490f, _zhl16Model.CompartmentList[0].P_Total, 1e-6f);
+ }
+
+ [TestMethod]
+ public void Zhl16_CalculateDescent_Test()
+ {
+ Zhl16 _zhl16Model = new Zhl16("ZHL-16C", testDiveLevels, testGases);
+
+ _zhl16Model.Initialise();
+ _zhl16Model.CalculateDescent(_zhl16Model.DiveLevels[0], _zhl16Model.GasList[0], 18.0f);
+
+ Assert.AreEqual(5.545671f, _zhl16Model.CompartmentList[0].P_Total, 1e-6f);
+ }
+}
\ No newline at end of file
diff --git a/DecoBox.sln b/src/DecoBox.sln
similarity index 89%
rename from DecoBox.sln
rename to src/DecoBox.sln
index ccb1064..8b048dc 100644
--- a/DecoBox.sln
+++ b/src/DecoBox.sln
@@ -3,15 +3,15 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{48075E88-D613-4C7B-AA1A-2052B81916B2}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DecoBox.Tests", "DecoBox.Tests\DecoBox.Tests.csproj", "{48075E88-D613-4C7B-AA1A-2052B81916B2}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{78B56521-84C6-4860-9AE9-AB6BCF3510A3}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{AAED59C6-69EF-4EAC-ADD0-937C32E9D5BB}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Algorithms", "Algorithms\Algorithms.csproj", "{059084DE-906B-4B82-B34E-DAF0BBC2EFE1}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DecoBox.Algorithms", "DecoBox.Algorithms\DecoBox.Algorithms.csproj", "{059084DE-906B-4B82-B34E-DAF0BBC2EFE1}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CLI", "CLI\CLI.csproj", "{3B0C98A3-AA1B-4519-AA09-8EF74E306326}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DecoBox.CLI", "DecoBox.CLI\DecoBox.CLI.csproj", "{3B0C98A3-AA1B-4519-AA09-8EF74E306326}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
diff --git a/src/qodana.yaml b/src/qodana.yaml
new file mode 100644
index 0000000..541fb41
--- /dev/null
+++ b/src/qodana.yaml
@@ -0,0 +1,46 @@
+#-------------------------------------------------------------------------------#
+# Qodana analysis is configured by qodana.yaml file #
+# https://www.jetbrains.com/help/qodana/qodana-yaml.html #
+#-------------------------------------------------------------------------------#
+
+#################################################################################
+# WARNING: Do not store sensitive information in this file, #
+# as its contents will be included in the Qodana report. #
+#################################################################################
+version: "1.0"
+
+#Specify IDE code to run analysis without container (Applied in CI/CD pipeline)
+ide: QDNET
+
+#Specify inspection profile for code analysis
+profile:
+ name: qodana.starter
+
+#Enable inspections
+#include:
+# - name:
+
+#Disable inspections
+#exclude:
+# - name:
+# paths:
+# -
+
+#Execute shell command before Qodana execution (Applied in CI/CD pipeline)
+#bootstrap: sh ./prepare-qodana.sh
+
+#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
+#plugins:
+# - id: #(plugin id can be found at https://plugins.jetbrains.com)
+
+# Quality gate. Will fail the CI/CD pipeline if any condition is not met
+# severityThresholds - configures maximum thresholds for different problem severities
+# testCoverageThresholds - configures minimum code coverage on a whole project and newly added code
+# Code Coverage is available in Ultimate and Ultimate Plus plans
+#failureConditions:
+# severityThresholds:
+# any: 15
+# critical: 5
+# testCoverageThresholds:
+# fresh: 70
+# total: 50