Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 57 additions & 18 deletions src/formulas/altitude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,24 @@ export function freezingLevelAltitude(
* returns the altitude at which the observed pressure occurs.
*
* For dry air conditions, the formula uses the specific gas constant for dry air (287.05 J/(kg·K)).
* For moist air conditions (when relativeHumidity is provided), the formula uses the virtual
* For moist air conditions (when humidity parameters are provided), the formula uses the virtual
* temperature approach which accounts for the lower density of moist air compared to dry air.
* This correction typically results in slightly higher altitude estimates for the same pressure
* difference, as moist air is less dense than dry air at the same temperature and pressure.
*
* The function supports three modes for humidity:
* 1. No humidity: Dry air calculation (no humidity parameters provided)
* 2. Single humidity: Uses the same relative humidity for both reference and observed altitudes
* 3. Separate humidities: Uses different humidity values for reference and observed altitudes,
* which provides more accurate results when humidity varies significantly between the two levels
*
* @param {number} referencePressure - Reference pressure in Pascals (Pa) at the reference altitude.
* @param {number} observedPressure - Observed pressure in Pascals (Pa) at the unknown altitude.
* @param {number} referenceAltitude - Altitude in meters (m) where the reference pressure was measured. Defaults to 0 (sea level).
* @param {number} temperature - Average temperature in Kelvin (K) between the two altitudes. Defaults to 288.15 K (15°C).
* @param {number} [relativeHumidity] - Optional relative humidity in percentage (0-100%). When provided, the calculation uses virtual temperature to account for moist air effects.
* @param {number} [relativeHumidity] - Optional relative humidity in percentage (0-100%). When provided without referenceHumidity/observedHumidity, this value is used for both altitudes.
* @param {number} [referenceHumidity] - Optional relative humidity in percentage (0-100%) at the reference altitude. When provided along with observedHumidity, allows for more accurate calculations when humidity varies with altitude.
* @param {number} [observedHumidity] - Optional relative humidity in percentage (0-100%) at the observed altitude. When provided along with referenceHumidity, allows for more accurate calculations when humidity varies with altitude.
* @returns {number} The final altitude in meters (m) where the observed pressure occurs.
*
* @example
Expand All @@ -46,10 +54,15 @@ export function freezingLevelAltitude(
* console.log(altitudeDry); // ~1011 m
*
* @example
* // Calculate altitude for moist air (60% relative humidity)
* // Calculate altitude for moist air (60% relative humidity at both levels)
* const altitudeMoist = altitudeFromPressureDifference(101325, 89874, 0, 288.15, 60);
* console.log(altitudeMoist); // ~1021 m (slightly higher due to lower density of moist air)
*
* @example
* // Calculate altitude with different humidity at reference (80%) and observed (40%) altitudes
* const altitudeVarying = altitudeFromPressureDifference(101325, 89874, 0, 288.15, undefined, 80, 40);
* console.log(altitudeVarying); // More accurate when humidity varies with altitude
*
* @see https://en.wikipedia.org/wiki/Hypsometric_equation
* @see https://en.wikipedia.org/wiki/Virtual_temperature
*/
Expand All @@ -58,35 +71,61 @@ export function altitudeFromPressureDifference(
observedPressure: number,
referenceAltitude: number = 0,
temperature: number = c.STANDARD_MEAN_TEMPERATURE_KELVIN,
relativeHumidity?: number
relativeHumidity?: number,
referenceHumidity?: number,
observedHumidity?: number
): number {
const g = c.DRY_AIR_CONSTANTS.gravity; // Gravitational acceleration (m/s²)
const R = c.DRY_AIR_CONSTANTS.gasConstant; // Specific gas constant for dry air (J/(kg·K))

// Determine the effective temperature to use in the calculation
let effectiveTemperature = temperature;

if (relativeHumidity !== undefined) {
// Determine which humidity values to use
// Priority: referenceHumidity/observedHumidity pair > relativeHumidity > no humidity (dry air)
const hasSeparateHumidities = referenceHumidity !== undefined && observedHumidity !== undefined;
const hasSingleHumidity = relativeHumidity !== undefined;

if (hasSeparateHumidities || hasSingleHumidity) {
// For moist air, use the virtual temperature approach
// Virtual temperature accounts for the effect of water vapor on air density
// by treating moist air as dry air at a slightly higher temperature

// Calculate average pressure for mixing ratio calculation
// Using geometric mean because pressure varies exponentially with altitude,
// so geometric mean better represents the average pressure in the layer
const avgPressure = Math.sqrt(referencePressure * observedPressure);

// Calculate saturation vapor pressure at the given temperature
const svp = saturationVaporPressure(temperature);

// Calculate actual vapor pressure from relative humidity
const avp = actualVaporPressure(svp, relativeHumidity);

// Calculate mixing ratio in g/kg
const mixRatio = calcMixingRatio(avp, avgPressure);

// Use virtual temperature which accounts for moist air
effectiveTemperature = virtualTemperature(temperature, mixRatio);
if (hasSeparateHumidities) {
// Use separate humidity values for reference and observed altitudes
// Calculate virtual temperature for each level and use their average

// Reference level calculations
const refAvp = actualVaporPressure(svp, referenceHumidity!);
const refMixRatio = calcMixingRatio(refAvp, referencePressure);
const refVirtualTemp = virtualTemperature(temperature, refMixRatio);

// Observed level calculations
const obsAvp = actualVaporPressure(svp, observedHumidity!);
const obsMixRatio = calcMixingRatio(obsAvp, observedPressure);
const obsVirtualTemp = virtualTemperature(temperature, obsMixRatio);

// Use the average of virtual temperatures for the layer
effectiveTemperature = (refVirtualTemp + obsVirtualTemp) / 2;
} else {
// Use single humidity value (backward compatible behavior)
// Calculate average pressure for mixing ratio calculation
// Using geometric mean because pressure varies exponentially with altitude,
// so geometric mean better represents the average pressure in the layer
const avgPressure = Math.sqrt(referencePressure * observedPressure);

// Calculate actual vapor pressure from relative humidity
const avp = actualVaporPressure(svp, relativeHumidity!);

// Calculate mixing ratio in g/kg
const mixRatio = calcMixingRatio(avp, avgPressure);

// Use virtual temperature which accounts for moist air
effectiveTemperature = virtualTemperature(temperature, mixRatio);
}
}

// Using the hypsometric formula: h = (R * T / g) * ln(P1 / P2)
Expand Down
95 changes: 95 additions & 0 deletions tests/formulas/altitude.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,101 @@ describe('altitudeFromPressureDifference', () => {
// Dry result is ~5305 m, moist result should be ~5333 m
expect(moistResult).toBeCloseTo(5333, 0);
});

// Tests for separate referenceHumidity and observedHumidity parameters
it('should calculate altitude with separate reference and observed humidity values', () => {
// When humidity differs between reference and observed altitudes
const result = altitudeFromPressureDifference(101325, 89874, 0, 288.15, undefined, 80, 40);

// The result should be a valid positive altitude
expect(result).toBeGreaterThan(1000);
expect(result).toBeLessThan(1100);
});

it('should give higher altitude when reference humidity is higher than observed humidity', () => {
// Higher humidity at sea level (80%) decreasing to lower humidity at altitude (40%)
const highToLowHumidity = altitudeFromPressureDifference(101325, 89874, 0, 288.15, undefined, 80, 40);
// Lower humidity at sea level (40%) increasing to higher humidity at altitude (80%)
const lowToHighHumidity = altitudeFromPressureDifference(101325, 89874, 0, 288.15, undefined, 40, 80);

// Both should give similar results since we're averaging the virtual temperatures
// The difference should be small because the average humidity is the same
expect(Math.abs(highToLowHumidity - lowToHighHumidity)).toBeLessThan(1);
});

it('should produce different result than single humidity when humidities differ significantly', () => {
// Single humidity at 60% (the average of 80% and 40%)
const singleHumidityResult = altitudeFromPressureDifference(101325, 89874, 0, 288.15, 60);
// Separate humidities at 80% reference and 40% observed
const separateHumidityResult = altitudeFromPressureDifference(101325, 89874, 0, 288.15, undefined, 80, 40);

// Results should be slightly different due to different calculation methods
// (geometric mean pressure vs individual pressure for mixing ratio)
expect(separateHumidityResult).toBeDefined();
expect(singleHumidityResult).toBeDefined();
// Both should be in a similar range but not necessarily identical
expect(Math.abs(singleHumidityResult - separateHumidityResult)).toBeLessThan(5);
});

it('should give same result when reference and observed humidity are equal to single humidity', () => {
// All humidities at 60%
const singleHumidityResult = altitudeFromPressureDifference(101325, 89874, 0, 288.15, 60);
const separateEqualHumidityResult = altitudeFromPressureDifference(101325, 89874, 0, 288.15, undefined, 60, 60);

// Results should be very close when using equal separate humidities vs single humidity
expect(separateEqualHumidityResult).toBeCloseTo(singleHumidityResult, 0);
});

it('should use dry air calculation when only one of reference or observed humidity is provided', () => {
// Only referenceHumidity provided (observedHumidity is undefined)
const onlyReference = altitudeFromPressureDifference(101325, 89874, 0, 288.15, undefined, 60, undefined);
// Only observedHumidity provided (referenceHumidity is undefined)
const onlyObserved = altitudeFromPressureDifference(101325, 89874, 0, 288.15, undefined, undefined, 60);
// Dry air (no humidity)
const dryResult = altitudeFromPressureDifference(101325, 89874, 0, 288.15);

// When only one humidity is provided and relativeHumidity is not set, should use dry air
expect(onlyReference).toBeCloseTo(dryResult, 2);
expect(onlyObserved).toBeCloseTo(dryResult, 2);
});

it('should prioritize separate humidity parameters over single relativeHumidity', () => {
// Provide all three humidity parameters
// relativeHumidity=50%, referenceHumidity=80%, observedHumidity=40%
const result = altitudeFromPressureDifference(101325, 89874, 0, 288.15, 50, 80, 40);
const separateOnly = altitudeFromPressureDifference(101325, 89874, 0, 288.15, undefined, 80, 40);
const singleOnly = altitudeFromPressureDifference(101325, 89874, 0, 288.15, 50);

// Should use separate humidities when both are provided, ignoring relativeHumidity
expect(result).toBeCloseTo(separateOnly, 2);
// And result should differ from single humidity result
expect(Math.abs(result - singleOnly)).toBeGreaterThan(0);
});

it('should show appropriate humidity effect with varying humidity at high temperatures', () => {
// At high temperatures, humidity effect is more pronounced
// Reference at 100% humidity, observed at 20% humidity (average 60%)
const varyingHumidity = altitudeFromPressureDifference(101325, 89874, 0, 303.15, undefined, 100, 20);
// Single humidity at 60% (same average)
const avgHumidity = altitudeFromPressureDifference(101325, 89874, 0, 303.15, 60);
// Dry air
const dryResult = altitudeFromPressureDifference(101325, 89874, 0, 303.15);

// Both humid calculations should be higher than dry
expect(varyingHumidity).toBeGreaterThan(dryResult);
expect(avgHumidity).toBeGreaterThan(dryResult);
});

it('should handle extreme humidity differences', () => {
// 100% humidity at reference, 0% at observed
const extremeResult = altitudeFromPressureDifference(101325, 89874, 0, 288.15, undefined, 100, 0);
// 50% at both (same average)
const avgResult = altitudeFromPressureDifference(101325, 89874, 0, 288.15, 50);

// Both should produce valid results
expect(extremeResult).toBeGreaterThan(1000);
expect(avgResult).toBeGreaterThan(1000);
});
});

describe('cloudBaseHeight', () => {
Expand Down