Dew Point Calculation in Java

Introduction

The core functionality of the Ersa library is all about calculating the dew point from temperature and humidity data. The dew point is used to assess environmental risks, like mold and corrosion, as well as to predict material preservation (e.g. in archives and art collections). The Ersa solution collects data from LoRa connected sensors and processes this data to provide insight into environmental conditions.

What is a dew point?

A real gas closely resembles an ideal gas above a certain temperature: its critical temperature. If we compress a gas below its critical temperature it will start to condense at a certain point. Beyond that point the pressure will not change until the gas is fully condensed into liquid. Within this range the liquid and the gas phase are at equilibrium. We speak of a saturated vapor and a corresponding saturation pressure, which is constant at constant temperature.

So the saturation pressure is volume independent and increases with temperature. When the saturation pressure of a liquid equals the surrounding air pressure, the liquid has reached its boiling point.

The air that surrounds it contains water vapor. The number of grams of water per cubic meter of air is the absolute air humidity (AH). The water contributes to the air pressure with its vapor pressure (pV). When the vapor pressure reaches the saturation pressure (pS) the air has reached its maximum absolute humidity. The maximum absolute humidity also increases with temperature.

The relative humidity is the ratio of the absolute air humidity at pressure pV to the maximum absolute air humidity at pS:

RH = AH / AHmax = pV / pS

As the saturation pressure decreases with temperature, the relative humidity increases when the air cools down. At a certain temperature it reaches 100%. This temperature is the dew point (Tdew), where AH equals AHmax. Below the dew point, the vapor condenses into liquid water.

Calculating the dew point

Starting from our temperature and humidity sensor data, we will now calculate the dew point. Credits for this approach go to Wolfgang Kühn who made a dew point calculator in JavaScript.

We need some formulas to calculate the saturation pressure at a given temperature. Then we calculate the actual vapor pressure from the temperature and relative humidity. Finally we need to find the temperature at which this vapor pressure becomes the saturation pressure. That temperature is the dew point.

All code is available in the Ersa project: https://github.com/Pygmalion69/Ersa

Saturation pressure

The International Association for the Properties of Water and Steam has published a formula to calculate the saturation pressure. Let’s implement it like this:

Dew.java

    /* Water saturation vapor pressure coefficients */
    private static final double N1 = 0.11670521452767e4;
    private static final double N6 = 0.14915108613530e2;
    private static final double N2 = -0.72421316703206e6;
    private static final double N7 = -0.48232657361591e4;
    private static final double N3 = -0.17073846940092e2;
    private static final double N8 = 0.40511340542057e6;
    private static final double N4 = 0.12020824702470e5;
    private static final double N9 = -0.23855557567849;
    private static final double N5 = -0.32325550322333e7;
    private static final double N10 = 0.65017534844798e3;
 
  /**
     * Saturation Vapor Pressure formula for range 273..678 K. 
     *
     * @param temperature temperature
     * @return saturation vapor pressure
     */
    public double pvsWater(double temperature) {
        double th = temperature + N9 / (temperature - N10);
        double a = (th + N1) * th + N2;
        double b = (N3 * th + N4) * th + N5;
        double c = (N6 * th + N7) * th + N8;
 
        double p = 2 * c / (-b + Math.sqrt(b * b - 4 * a * c));
        p *= p;
        p *= p;
        return p * 1e6;
    }

What if the water is frozen? Bob Hardy has published a formula for the saturation pressure of ice:

Dew.java

    /* Ice saturation vapor pressure coefficients */
    private static final double K0 = -5.8666426e3;
    private static final double K1 = 2.232870244e1;
    private static final double K2 = 1.39387003e-2;
    private static final double K3 = -3.4262402e-5;
    private static final double K4 = 2.7040955e-8;
    private static final double K5 = 6.7063522e-1;
 
     /**
     * Saturation Vapor Pressure formula for range -100..0 Deg. C. 
     *
     * @param temperature temperature
     * @return saturation vapor pressure
     */
    public double pvsIce(double temperature) {
        double lnP = K0 / temperature + K1 + (K2 + (K3 + (K4 * temperature))
                * temperature) * temperature + K5 * Math.log(temperature);
        return Math.exp(lnP);
    }

Now we can have a saturation pressure method for water in its solid and liquid state within the valid temperature range:

Dew.java

     /**
     * Compute Saturation Vapor Pressure for
     * Temperature.MIN < temperature[K] < Temperature.MAX
     *
     * @param temperature temperature
     * @return saturation vapor pressure
     * @see eu.sergehelfrich.ersa.Temperature
     */
   public double pvs(double temperature) throws IllegalArgumentException {
        if (temperature < Temperature.MIN || temperature > Temperature.MAX) {
            throw new IllegalArgumentException("Temperature out of range!");
        } else if (temperature < Temperature.CELSIUS_OFFSET) {
            return pvsIce(temperature);
        } else {
            return pvsWater(temperature);
        }
    }

Solving the saturation vapor pressure function

We can now calculate the saturation vapor pressure, but we also need to go the other way around. Once we have found the saturation vapor pressure at the dew point, we want to find the corresponding temperature. We’ll use Newton’s Method to solve f(x) = y for x. Basically we start with a first guess: x0. We establish the tangent line at (x0, f(x0)) and find our next value, x1, at the x intersection of this line. On several iterations we approach the x we’re looking for. It does not always work, but it will in our case.

A clear explanation of Newton’s Method is given by Aaron Burton in Newton’s Method and Fractals.

The pvs() method holds our function. We’ll create a Solver class and pass this method to it. The Solver will establish our x, that is the temperature for a given saturation vapor pressure.

Passing a method as an argument

An easy way to do this is by defining a Single Method Interface (SMI). Instances of this interface hold our function and can be passed around as arguments.

FunctionCallable.java

public interface FunctionCallable {
 
    /**
     *
     * @param x x
     * @return y y
     * @throws eu.sergehelfrich.ersa.solver.SolverException Solver does not converge
     */
    public double function(double x) throws SolverException;
 
}

This allows us to implement a FunctionCallable, like so:

Dew.java

     FunctionCallable functionCallable = new FunctionCallable() {
 
            @Override
            public double function(double x) throws SolverException, IllegalArgumentException {
                return pvs(x);
            }};

Solver

So here’s the Solver. Its solve() method takes our FunctionCallable, the y value and the initial guess, x0, as arguments.

Solver.java

public class Solver {
 
    /**
     * Newton's Method to solve f(x)=y for x with an initial guess of x0.
     *
     * @param functionCallable f(x)=y
     * @param y y
     * @param x0 x0
     * @return x x
     * @throws eu.sergehelfrich.ersa.solver.SolverException Solver does not converge
     */
    public double solve(FunctionCallable functionCallable, double y, double x0) throws SolverException, IllegalArgumentException {
 
        double x = x0;
        double xNew;
        double maxCount = 10;
        double count = 0;
        while (true) {
            if (count > maxCount) {
                throw new SolverException("Solver does not converge!");
            }
            double dx = x / 1000.0;
            double z = functionCallable.function(x);
            xNew = x + dx * (y - z) / (functionCallable.function(x + dx) - z);
            if (Math.abs((xNew - x) / xNew) < 0.0001) {
                return xNew;
            }
            x = xNew;
            count++;
        }
    }
}

It throws a custom exception when the function cannot be solved by this method.

SolverException.java

public class SolverException extends Exception {
 
    /**
     *
     * @param message message
     */
    public SolverException(String message) {
        super(message);
    }
 
}

Establishing the dew point

We’ll use the actual temperature as our initial guess. The actual vapor pressure is defined by:

pV = RH × pS

This is passed as the y value, as we want to know at what temperature this pressure becomes the saturation vapor pressure. That’s the dew point!

Dew.java

    private final Solver solver = new Solver();
 
    /**
     * Compute the dew point for given relative humidity[%] and temperature[K].
     * @param relativeHumidity relative humidity (%)
     * @param temperature temperature (K)
     * @return dew point (K)
     * @throws eu.sergehelfrich.ersa.solver.SolverException Solver does not converge
     */
    public double dewPoint(double relativeHumidity, double temperature) throws SolverException, IllegalArgumentException {
        FunctionCallable functionCallable = new FunctionCallable() {
            @Override
            public double function(double x) throws SolverException, IllegalArgumentException {
                return pvs(x);
            }};      
        return solver.solve(functionCallable, relativeHumidity / 100.0 * pvs(temperature), temperature);            
    }

As contemporary Java programmers we definitely want to clean up this clutter and use a lambda expression. Here’s our final dew point method:

Dew.java

 public double dewPoint(double relativeHumidity, double temperature) throws SolverException, IllegalArgumentException {        
        return solver.solve((double x) -> pvs(x), relativeHumidity / 100.0 * pvs(temperature), temperature);
    }