Skip to content

Commit 35ce0f6

Browse files
authored
Merge pull request #20 from oyve/copilot/convert-liquid-to-snow-depth
Add snowfall equivalent calculations based on temperature
2 parents 7b42a2d + cf75d07 commit 35ce0f6

File tree

4 files changed

+247
-15
lines changed

4 files changed

+247
-15
lines changed

package-lock.json

Lines changed: 0 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,6 @@
2222
"import": "./dist/esm/formulas/altitude.js",
2323
"types": "./dist/types/formulas/altitude.d.ts"
2424
},
25-
"./diurnalRythm": {
26-
"require": "./dist/cjs/phenomena/diurnalRythm.cjs",
27-
"import": "./dist/esm/phenomena/diurnalRythm.js",
28-
"types": "./dist/types/phenomena/diurnalRythm.d.ts"
29-
},
3025
"./humidity": {
3126
"require": "./dist/cjs/formulas/humidity.cjs",
3227
"import": "./dist/esm/formulas/humidity.js",
@@ -47,21 +42,41 @@
4742
"import": "./dist/esm/formulas/wind.js",
4843
"types": "./dist/types/formulas/wind.d.ts"
4944
},
45+
"./PET": {
46+
"require": "./dist/cjs/indices/PET.cjs",
47+
"import": "./dist/esm/indices/PET.js",
48+
"types": "./dist/types/indices/PET.d.ts"
49+
},
50+
"./UTCI": {
51+
"require": "./dist/cjs/indices/UTCI.cjs",
52+
"import": "./dist/esm/indices/UTCI.js",
53+
"types": "./dist/types/indices/UTCI.d.ts"
54+
},
5055
"./heatIndex": {
51-
"require": "./dist/cjs/scales/heatIndex.cjs",
52-
"import": "./dist/esm/scales/heatIndex.js",
53-
"types": "./dist/types/scales/heatIndex.d.ts"
56+
"require": "./dist/cjs/indices/heatIndex.cjs",
57+
"import": "./dist/esm/indices/heatIndex.js",
58+
"types": "./dist/types/indices/heatIndex.d.ts"
5459
},
5560
"./humidex": {
5661
"require": "./dist/cjs/indices/humidex.cjs",
5762
"import": "./dist/esm/indices/humidex.js",
5863
"types": "./dist/types/indices/humidex.d.ts"
5964
},
65+
"./diurnalRythm": {
66+
"require": "./dist/cjs/phenomena/diurnalRythm.cjs",
67+
"import": "./dist/esm/phenomena/diurnalRythm.js",
68+
"types": "./dist/types/phenomena/diurnalRythm.d.ts"
69+
},
6070
"./fog": {
6171
"require": "./dist/cjs/phenomena/fog.cjs",
6272
"import": "./dist/esm/phenomena/fog.js",
6373
"types": "./dist/types/phenomena/fog.d.ts"
6474
},
75+
"./snow": {
76+
"require": "./dist/cjs/phenomena/snow.cjs",
77+
"import": "./dist/esm/phenomena/snow.js",
78+
"types": "./dist/types/phenomena/snow.d.ts"
79+
},
6580
"./beaufort": {
6681
"require": "./dist/cjs/scales/beaufort.cjs",
6782
"import": "./dist/esm/scales/beaufort.js",
@@ -140,4 +155,4 @@
140155
"regression": "^2.0.1",
141156
"suncalc": "^1.9.0"
142157
}
143-
}
158+
}

src/phenomena/snow.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* Calculate the snow-to-liquid ratio based on temperature.
3+
* The ratio varies with temperature, with colder temperatures producing fluffier, less dense snow.
4+
* Uses linear interpolation for smooth transition between temperature regimes.
5+
*
6+
* @param {number} temperature - Air temperature in Kelvin
7+
* @returns {number} Snow-to-liquid ratio (dimensionless)
8+
*/
9+
export function snowToLiquidRatio(temperature: number): number {
10+
// Convert to Celsius
11+
const tempC = temperature - 273.15;
12+
13+
// Use a linear approximation for smooth transition
14+
// Linearly interpolates from 5:1 at 0°C to 30:1 at -20°C
15+
16+
if (tempC >= 0) {
17+
// Above freezing
18+
return 5;
19+
} else if (tempC > -20) {
20+
// Linear interpolation for -20°C to 0°C
21+
// Slope = (30 - 5) / (-20 - 0) = 25 / -20 = -1.25
22+
// ratio = 5 - 1.25 * tempC
23+
return 5 - 1.25 * tempC;
24+
} else {
25+
// Very cold (-20°C and below)
26+
return 30;
27+
}
28+
}
29+
30+
/**
31+
* Calculate snow depth from liquid precipitation based on temperature.
32+
* Converts liquid precipitation (e.g., rainfall equivalent) to estimated snow depth.
33+
*
34+
* @param {number} liquidPrecipitation - Liquid precipitation depth in millimeters
35+
* @param {number} temperature - Air temperature in Kelvin
36+
* @returns {number} Estimated snow depth in millimeters
37+
*/
38+
export function snowfallEquivalent(liquidPrecipitation: number, temperature: number): number {
39+
const ratio = snowToLiquidRatio(temperature);
40+
return liquidPrecipitation * ratio;
41+
}
42+
43+
44+
45+
/**
46+
* Calculate liquid precipitation from snow depth (reverse calculation).
47+
* Useful for estimating water content of snowfall.
48+
*
49+
* @param {number} snowDepth - Snow depth in millimeters
50+
* @param {number} temperature - Air temperature in Kelvin
51+
* @returns {number} Estimated liquid equivalent in millimeters
52+
*/
53+
export function snowToLiquidEquivalent(snowDepth: number, temperature: number): number {
54+
const ratio = snowToLiquidRatio(temperature);
55+
return snowDepth / ratio;
56+
}

tests/phenomena/snow.test.ts

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import {
2+
snowToLiquidRatio,
3+
snowfallEquivalent,
4+
snowToLiquidEquivalent
5+
} from '../../src/phenomena/snow';
6+
7+
describe('snowToLiquidRatio', () => {
8+
it('returns 5 for temperatures at or above freezing', () => {
9+
// At freezing point (0°C = 273.15K)
10+
expect(snowToLiquidRatio(273.15)).toBe(5);
11+
// Above freezing
12+
expect(snowToLiquidRatio(275)).toBe(5);
13+
expect(snowToLiquidRatio(280)).toBe(5);
14+
});
15+
16+
it('returns 30 for very cold temperatures', () => {
17+
expect(snowToLiquidRatio(253.15)).toBeCloseTo(30, 2); // -20°C exactly
18+
expect(snowToLiquidRatio(243.15)).toBe(30); // -30°C
19+
});
20+
21+
it('provides continuous values in the transition range', () => {
22+
// -2°C should be 7.5
23+
const ratio1 = snowToLiquidRatio(271.15);
24+
expect(ratio1).toBeCloseTo(7.5, 1);
25+
26+
// -10°C should be 17.5
27+
const ratio2 = snowToLiquidRatio(263.15);
28+
expect(ratio2).toBeCloseTo(17.5, 1);
29+
30+
// -20°C boundary (exactly 30, with floating point tolerance)
31+
const ratio3 = snowToLiquidRatio(253.15);
32+
expect(ratio3).toBeCloseTo(30, 2);
33+
});
34+
35+
it('increases ratio as temperature decreases', () => {
36+
const ratio1 = snowToLiquidRatio(271.15); // -2°C
37+
const ratio2 = snowToLiquidRatio(268.15); // -5°C
38+
const ratio3 = snowToLiquidRatio(263.15); // -10°C
39+
const ratio4 = snowToLiquidRatio(258.15); // -15°C
40+
41+
expect(ratio2).toBeGreaterThan(ratio1);
42+
expect(ratio3).toBeGreaterThan(ratio2);
43+
expect(ratio4).toBeGreaterThan(ratio3);
44+
});
45+
});
46+
47+
describe('snowfallEquivalent', () => {
48+
it('calculates snow depth from liquid precipitation at various temperatures', () => {
49+
// 10mm liquid at 0°C (ratio 5:1) = 50mm snow
50+
expect(snowfallEquivalent(10, 273.15)).toBe(50);
51+
52+
// 10mm liquid at -3°C (ratio ~8.75:1)
53+
const snow1 = snowfallEquivalent(10, 270.15);
54+
expect(snow1).toBeCloseTo(87.5, 1);
55+
56+
// 10mm liquid at -8°C (ratio ~15:1)
57+
const snow2 = snowfallEquivalent(10, 265.15);
58+
expect(snow2).toBeCloseTo(150, 1);
59+
60+
// 10mm liquid at -12°C (ratio ~20:1)
61+
const snow3 = snowfallEquivalent(10, 261.15);
62+
expect(snow3).toBeCloseTo(200, 1);
63+
64+
// 10mm liquid at -20°C (ratio 30:1) = 300mm snow
65+
const snow4 = snowfallEquivalent(10, 253.15);
66+
expect(snow4).toBeCloseTo(300, 1);
67+
});
68+
69+
it('returns 0 for 0mm liquid precipitation', () => {
70+
expect(snowfallEquivalent(0, 270)).toBe(0);
71+
expect(snowfallEquivalent(0, 260)).toBe(0);
72+
});
73+
74+
it('handles small amounts of precipitation', () => {
75+
// 1mm liquid at -10°C (ratio ~17.5:1)
76+
const snow1 = snowfallEquivalent(1, 263.15);
77+
expect(snow1).toBeCloseTo(17.5, 1);
78+
79+
// 0.5mm liquid at -8°C (ratio ~15:1)
80+
const snow2 = snowfallEquivalent(0.5, 265.15);
81+
expect(snow2).toBeCloseTo(7.5, 1);
82+
});
83+
84+
it('handles large amounts of precipitation', () => {
85+
// 100mm liquid at -10°C (ratio ~17.5:1)
86+
const snow = snowfallEquivalent(100, 263.15);
87+
expect(snow).toBeCloseTo(1750, 1);
88+
});
89+
90+
it('provides smoother transitions than step function', () => {
91+
// The continuous version should give different values for close temperatures
92+
const snow1 = snowfallEquivalent(10, 267.15); // -6°C
93+
const snow2 = snowfallEquivalent(10, 266.15); // -7°C
94+
95+
// Both should be positive and snow2 should be >= snow1
96+
expect(snow1).toBeGreaterThan(0);
97+
expect(snow2).toBeGreaterThanOrEqual(snow1);
98+
});
99+
});
100+
101+
102+
describe('snowToLiquidEquivalent', () => {
103+
it('converts snow depth to liquid precipitation', () => {
104+
// 50mm snow at 0°C (ratio 5:1) = 10mm liquid
105+
expect(snowToLiquidEquivalent(50, 273.15)).toBe(10);
106+
107+
// 150mm snow at -8°C (ratio ~15:1) = ~10mm liquid
108+
const liquid1 = snowToLiquidEquivalent(150, 265.15);
109+
expect(liquid1).toBeCloseTo(10, 1);
110+
111+
// 300mm snow at -20°C (ratio 30:1) = 10mm liquid
112+
const liquid2 = snowToLiquidEquivalent(300, 253.15);
113+
expect(liquid2).toBeCloseTo(10, 1);
114+
});
115+
116+
it('is the inverse of snowfallEquivalent', () => {
117+
const liquid = 25;
118+
const temp = 265.15; // -8°C
119+
120+
// Convert liquid to snow and back
121+
const snow = snowfallEquivalent(liquid, temp);
122+
const liquidBack = snowToLiquidEquivalent(snow, temp);
123+
124+
expect(liquidBack).toBeCloseTo(liquid, 10);
125+
});
126+
127+
it('returns 0 for 0mm snow depth', () => {
128+
expect(snowToLiquidEquivalent(0, 270)).toBe(0);
129+
expect(snowToLiquidEquivalent(0, 260)).toBe(0);
130+
});
131+
132+
it('handles fractional values', () => {
133+
// At -10°C, ratio is ~17.5:1
134+
// So 87.5mm snow should give ~5mm liquid
135+
const liquid = snowToLiquidEquivalent(87.5, 263.15);
136+
expect(liquid).toBeCloseTo(5, 1);
137+
});
138+
});
139+
140+
describe('Integration tests', () => {
141+
it('round trip conversion preserves values', () => {
142+
const temperatures = [273.15, 270.15, 265.15, 260.15, 253.15];
143+
const liquidAmounts = [1, 5, 10, 25, 50];
144+
145+
temperatures.forEach(temp => {
146+
liquidAmounts.forEach(liquid => {
147+
const snow = snowfallEquivalent(liquid, temp);
148+
const liquidBack = snowToLiquidEquivalent(snow, temp);
149+
expect(liquidBack).toBeCloseTo(liquid, 10);
150+
});
151+
});
152+
});
153+
154+
it('handles realistic weather scenarios', () => {
155+
// Light snow: 2mm liquid at -5°C (ratio ~11.25:1)
156+
const lightSnow = snowfallEquivalent(2, 268.15);
157+
expect(lightSnow).toBeCloseTo(22.5, 1);
158+
159+
// Moderate snow: 10mm liquid at -10°C (ratio ~17.5:1)
160+
const moderateSnow = snowfallEquivalent(10, 263.15);
161+
expect(moderateSnow).toBeCloseTo(175, 1);
162+
163+
// Heavy snow: 25mm liquid at -15°C (ratio ~23.75:1)
164+
const heavySnow = snowfallEquivalent(25, 258.15);
165+
expect(heavySnow).toBeCloseTo(593.75, 1);
166+
});
167+
});

0 commit comments

Comments
 (0)