diff --git a/src/formulas/altitude.ts b/src/formulas/altitude.ts index 02fa72a..c865769 100644 --- a/src/formulas/altitude.ts +++ b/src/formulas/altitude.ts @@ -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 @@ -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 */ @@ -58,7 +71,9 @@ 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)) @@ -66,27 +81,51 @@ export function altitudeFromPressureDifference( // 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) diff --git a/tests/formulas/altitude.test.ts b/tests/formulas/altitude.test.ts index b8410c0..b42237f 100644 --- a/tests/formulas/altitude.test.ts +++ b/tests/formulas/altitude.test.ts @@ -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', () => {