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