From 1f746a87462f0caff27449613f27c79cd68f9f7f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Jun 2025 17:30:41 +0000 Subject: [PATCH 1/4] Initial plan for issue From 33e942c030591fdf6e1e69fb98c079fcd7b166f5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Jun 2025 17:44:49 +0000 Subject: [PATCH 2/4] Implement basic time-to-fly calculation in Tissue and BuhlmannZHL16C classes Co-authored-by: dylan-smith <1508559+dylan-smith@users.noreply.github.com> --- .../BuhlmannZHL16C.spec.ts | 95 +++++++++++++++++++ .../dive-planner-service/BuhlmannZHL16C.ts | 9 ++ .../app/dive-planner-service/Tissue.spec.ts | 25 +++++ src/src/app/dive-planner-service/Tissue.ts | 27 ++++++ 4 files changed, 156 insertions(+) create mode 100644 src/src/app/dive-planner-service/BuhlmannZHL16C.spec.ts create mode 100644 src/src/app/dive-planner-service/Tissue.spec.ts diff --git a/src/src/app/dive-planner-service/BuhlmannZHL16C.spec.ts b/src/src/app/dive-planner-service/BuhlmannZHL16C.spec.ts new file mode 100644 index 00000000..bc266cc2 --- /dev/null +++ b/src/src/app/dive-planner-service/BuhlmannZHL16C.spec.ts @@ -0,0 +1,95 @@ +import { BuhlmannZHL16C } from './BuhlmannZHL16C'; +import { BreathingGas } from './BreathingGas'; +import { DiveSettingsService } from './DiveSettings.service'; +import { DiveSegment } from './DiveSegment'; + +describe('BuhlmannZHL16C', () => { + let diveSettings: DiveSettingsService; + let air: BreathingGas; + let model: BuhlmannZHL16C; + + beforeEach(() => { + diveSettings = new DiveSettingsService(); + // Initialize StandardGases to avoid the find error + BreathingGas.StandardGases = []; + air = BreathingGas.create(21, 0, 79, diveSettings); + model = new BuhlmannZHL16C(); + }); + + it('should calculate time to fly for surface conditions', () => { + // At surface with no dive, should be 0 time to fly + const timeToFly = model.getTimeToFly(); + expect(timeToFly).toBe(0); + }); + + it('should calculate time to fly after a dive', () => { + // Simulate a dive to 30m for 30 minutes + const segment = new DiveSegment(0, 1800, 'Test', 'Test dive', 30, 30, air, 'test', diveSettings); + model.calculateForSegment(segment); + + // Should have some time to fly after nitrogen loading + const timeToFly = model.getTimeToFly(); + expect(timeToFly).toBeDefined(); + expect(timeToFly).toBeGreaterThan(0); + + // Should be reasonable time (less than 24 hours for a moderate dive) + expect(timeToFly).toBeLessThan(24 * 3600); + }); + + it('should calculate longer time to fly for deeper/longer dives', () => { + // Simulate two different dives + const shallowDive = new DiveSegment(0, 1800, 'Shallow', 'Shallow dive', 20, 20, air, 'test', diveSettings); + const deepDive = new DiveSegment(0, 1800, 'Deep', 'Deep dive', 40, 40, air, 'test', diveSettings); + + const shallowModel = new BuhlmannZHL16C(); + const deepModel = new BuhlmannZHL16C(); + + shallowModel.calculateForSegment(shallowDive); + deepModel.calculateForSegment(deepDive); + + const shallowTimeToFly = shallowModel.getTimeToFly(); + const deepTimeToFly = deepModel.getTimeToFly(); + + expect(shallowTimeToFly).toBeDefined(); + expect(deepTimeToFly).toBeDefined(); + + if (shallowTimeToFly !== undefined && deepTimeToFly !== undefined) { + expect(deepTimeToFly).toBeGreaterThan(shallowTimeToFly); + } + }); + + it('should use custom flying threshold', () => { + // Simulate a dive to 30m for 30 minutes + const segment = new DiveSegment(0, 1800, 'Test', 'Test dive', 30, 30, air, 'test', diveSettings); + model.calculateForSegment(segment); + + // Compare different thresholds + const conservativeTime = model.getTimeToFly(0.80); // More conservative + const standardTime = model.getTimeToFly(0.869); // Standard (110% of ambient) + const liberalTime = model.getTimeToFly(0.95); // More liberal + + expect(conservativeTime).toBeDefined(); + expect(standardTime).toBeDefined(); + expect(liberalTime).toBeDefined(); + + if (conservativeTime !== undefined && standardTime !== undefined && liberalTime !== undefined) { + expect(conservativeTime).toBeGreaterThan(standardTime); + expect(standardTime).toBeGreaterThan(liberalTime); + } + }); + + it('should return controlling tissue time', () => { + // Simulate a dive that loads multiple tissues + const segment = new DiveSegment(0, 3600, 'Test', 'Long dive', 25, 25, air, 'test', diveSettings); + model.calculateForSegment(segment); + + const timeToFly = model.getTimeToFly(); + + // The result should be reasonable for a long shallow dive + expect(timeToFly).toBeDefined(); + expect(timeToFly).toBeGreaterThan(0); + + // Should be controlled by slower tissues for longer dives + expect(timeToFly).toBeGreaterThan(3600); // More than 1 hour for a 1-hour dive + }); +}); \ No newline at end of file diff --git a/src/src/app/dive-planner-service/BuhlmannZHL16C.ts b/src/src/app/dive-planner-service/BuhlmannZHL16C.ts index 5765fee7..ace926c6 100644 --- a/src/src/app/dive-planner-service/BuhlmannZHL16C.ts +++ b/src/src/app/dive-planner-service/BuhlmannZHL16C.ts @@ -62,4 +62,13 @@ export class BuhlmannZHL16C { getTissuePHe(time: number, tissue: number): number { return this.tissues[tissue - 1].getPHe(time); } + + getTimeToFly(flyingN2Threshold: number = 0.869): number | undefined { + const tissueTimes = this.tissues.map(t => t.getTimeToFly(flyingN2Threshold)); + const validTimes = tissueTimes.filter(x => x !== undefined) as number[]; + + if (validTimes.length === 0) return undefined; + + return Math.max(...validTimes); + } } diff --git a/src/src/app/dive-planner-service/Tissue.spec.ts b/src/src/app/dive-planner-service/Tissue.spec.ts new file mode 100644 index 00000000..a495cbb0 --- /dev/null +++ b/src/src/app/dive-planner-service/Tissue.spec.ts @@ -0,0 +1,25 @@ +import { Tissue } from './Tissue'; +import { BreathingGas } from './BreathingGas'; +import { DiveSettingsService } from './DiveSettings.service'; +import { DiveSegment } from './DiveSegment'; + +describe('Tissue Time to Fly', () => { + let diveSettings: DiveSettingsService; + let air: BreathingGas; + + beforeEach(() => { + diveSettings = new DiveSettingsService(); + // Initialize StandardGases to avoid the find error + BreathingGas.StandardGases = []; + air = BreathingGas.create(21, 0, 79, diveSettings); + }); + + it('should return 0 for surface conditions', () => { + // Test tissue compartment 1 (fastest) - 5 minute N2 half-life + const tissue = new Tissue(1, 5, 1.1696, 0.5578, 1.51, 1.7474, 0.4245); + + // At surface, should be 0 time to fly + const timeToFly = tissue.getTimeToFly(); + expect(timeToFly).toBe(0); + }); +}); \ No newline at end of file diff --git a/src/src/app/dive-planner-service/Tissue.ts b/src/src/app/dive-planner-service/Tissue.ts index 5fdd1d74..855f7158 100644 --- a/src/src/app/dive-planner-service/Tissue.ts +++ b/src/src/app/dive-planner-service/Tissue.ts @@ -101,6 +101,33 @@ export class Tissue { return Math.floor(time); } + getTimeToFly(flyingN2Threshold: number = 0.869): number | undefined { + const currentTime = this.tissueByTime.size - 1; + const currentPN2 = this.getPN2(currentTime); + const currentPHe = this.getPHe(currentTime); + + // If we're already at safe levels, no waiting time needed + if (currentPN2 <= flyingN2Threshold && currentPHe <= this.ENVIRONMENT_PHE + 0.001) { + return 0; + } + + // Surface gas pressures + const surfacePN2 = this.ENVIRONMENT_PN2; + const surfacePHe = this.ENVIRONMENT_PHE; + + // Simple iterative approach - check every minute + for (let time = 1; time <= this.MAX_NDL; time++) { + const newPN2 = currentPN2 + this.getPN2DeltaByTime(currentPN2, surfacePN2, time); + const newPHe = currentPHe + this.getPHeDeltaByTime(currentPHe, surfacePHe, time); + + if (newPN2 <= flyingN2Threshold && newPHe <= this.ENVIRONMENT_PHE + 0.001) { + return time; + } + } + + return undefined; // If it takes longer than MAX_NDL + } + getPN2(time: number): number { return this.getTissueByTime(time).PN2; } From 7d8491be400ef0c631ffb2e33bdc9327aa6ef867 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Jun 2025 17:49:38 +0000 Subject: [PATCH 3/4] Integrate time-to-fly calculation into DivePlannerService and DiveProfile Co-authored-by: dylan-smith <1508559+dylan-smith@users.noreply.github.com> --- .../DivePlannerService.spec.ts | 15 +++++++++++++++ .../dive-planner-service/DivePlannerService.ts | 4 ++++ src/src/app/dive-planner-service/DiveProfile.ts | 4 ++++ 3 files changed, 23 insertions(+) diff --git a/src/src/app/dive-planner-service/DivePlannerService.spec.ts b/src/src/app/dive-planner-service/DivePlannerService.spec.ts index e4dc315c..1b37a682 100644 --- a/src/src/app/dive-planner-service/DivePlannerService.spec.ts +++ b/src/src/app/dive-planner-service/DivePlannerService.spec.ts @@ -42,6 +42,10 @@ describe('DivePlannerService', () => { expect(svc.getHypoxicError().duration).toBe(0); expect(svc.getMaxDepth()).toBe(0); expect(svc.getPO2Error().duration).toBe(0); + + // Test time to fly at surface - should be 0 + const timeToFly = svc.getTimeToFly(); + expect(timeToFly).toBe(0); }); it('30m for 25 mins on nitrox 32', () => { @@ -78,6 +82,12 @@ describe('DivePlannerService', () => { expect(svc.getMaxDepth()).toBe(30); expect(svc.getPO2Error().duration).toBe(0); + // Test time to fly - should be reasonable for a recreational dive + const timeToFly = svc.getTimeToFly(); + expect(timeToFly).toBeDefined(); + expect(timeToFly).toBeGreaterThan(0); + expect(timeToFly).toBeLessThan(24 * 3600); // Less than 24 hours + expect(svc.getNoDecoLimit(25, air, 0)).toBe(518); expect(svc.getOptimalDecoGas(25).oxygen).toBe(45); expect(svc.getOptimalDecoGas(25).helium).toBe(0); @@ -138,6 +148,11 @@ describe('DivePlannerService', () => { expect(svc.getHypoxicError().duration).toBe(6); expect(svc.getMaxDepth()).toBe(100); expect(svc.getPO2Error().duration).toBe(708); + + // Test time to fly - should be significant after a deep technical dive + const timeToFly = svc.getTimeToFly(); + expect(timeToFly).toBeDefined(); + expect(timeToFly).toBeGreaterThan(0); }); it('NDL accounts for on-gassing during ascent', () => { diff --git a/src/src/app/dive-planner-service/DivePlannerService.ts b/src/src/app/dive-planner-service/DivePlannerService.ts index f053949b..c7a3cb57 100644 --- a/src/src/app/dive-planner-service/DivePlannerService.ts +++ b/src/src/app/dive-planner-service/DivePlannerService.ts @@ -148,6 +148,10 @@ export class DivePlannerService { return this.diveProfile.getENDError(); } + getTimeToFly(flyingN2Threshold: number = 0.869): number | undefined { + return this.diveProfile.getTimeToFly(flyingN2Threshold); + } + getPO2WarningMessage(pO2: number): string | undefined { if (pO2 > this.settings.workingPO2Maximum && pO2 <= this.settings.decoPO2Maximum) return `Oxygen partial pressure should only go above ${this.settings.workingPO2Maximum} during deco stops`; diff --git a/src/src/app/dive-planner-service/DiveProfile.ts b/src/src/app/dive-planner-service/DiveProfile.ts index d203309a..6bf6fe49 100644 --- a/src/src/app/dive-planner-service/DiveProfile.ts +++ b/src/src/app/dive-planner-service/DiveProfile.ts @@ -241,6 +241,10 @@ export class DiveProfile { return { end: maxEND, duration }; } + getTimeToFly(flyingN2Threshold: number = 0.869): number | undefined { + return this.algo.getTimeToFly(flyingN2Threshold); + } + getMaxDepth(): number { return Math.max(...this.segments.map(x => x.EndDepth)); } From 3dd81e0abab8cbce3a21c2f40bdba39e3ca444e7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Jun 2025 17:54:17 +0000 Subject: [PATCH 4/4] Add time-to-fly display to dive summary UI component Co-authored-by: dylan-smith <1508559+dylan-smith@users.noreply.github.com> --- .../dive-summary/dive-summary.component.html | 4 ++ .../dive-summary.component.spec.ts | 58 +++++++++++++++++++ .../dive-summary/dive-summary.component.ts | 23 ++++++++ 3 files changed, 85 insertions(+) diff --git a/src/src/app/dive-summary/dive-summary.component.html b/src/src/app/dive-summary/dive-summary.component.html index 87b16292..f2ceb9ca 100644 --- a/src/src/app/dive-summary/dive-summary.component.html +++ b/src/src/app/dive-summary/dive-summary.component.html @@ -11,4 +11,8 @@ Average Depth: {{ divePlanner.getAverageDepth() | number: '1.0-0' }}m +