Psychrometrics, with the “r”
Psychrometrics, not psychometrics. The “r” matters. One is the study of moist air; the other is the measurement of mental faculties. The work below has nothing to do with people's brains.
What it's actually about: how air and water vapour behave together. You've experienced it without naming it. Step out of a hot shower and the bathroom mirror fogs up because warm humid air hits a colder surface and the water condenses. Walk outside on a warm muggy day and feel worse than you would on an equally warm dry day, because high humidity stops sweat from evaporating off your skin. Cold humid air doesn't feel as bad as warm humid air, because cold air just can't hold much moisture in absolute terms even at 100% relative humidity.
The useful principle, the one that makes the maths worth doing, is this: given any two independent properties of moist air(plus the ambient pressure), you can calculate every other. Have dry-bulb temperature and relative humidity from a sensor? Dew point falls out. So does enthalpy, wet-bulb, humidity ratio, density, specific heat. Every property an HVAC engineer wants is one short calculation away from the two readings most buildings already have.
You can see this for yourself below. Set a dry-bulb temperature and a relative humidity (the most common pair from building sensors), and the other properties are computed in your browser as you change them:
Pick a dry-bulb temperature and relative humidity. Everything else is computed in the browser by the actual library.
Ambient pressure assumed to be standard atmospheric (101.325 kPa).
There's an analogue version of this that engineers still reach for: the psychrometric chart. Plot a point from any two known properties and you can read the rest off the surrounding axes. Charts also do something the calculator doesn't: they make psychrometric processes visible. A line drawn between two state points shows what the air goes through when it's heated, cooled, mixed with another stream, or humidified, and the shape of that line distinguishes one kind of process from another.
Two Engines, One Contract
The reason this work happened at all has to do with how CoolPlanet's calculation engine is structured. There isn't one calculation engine. There are two: a server-side engine in Axon (running on SkySpark), and a client-side engine in JavaScript.
For that arrangement to be useful, both engines have to be functionally interchangeable. A user-defined calculation should evaluate to the same value regardless of where it runs. When they drift, you get a class of bugs that's genuinely awful to track down: each side, in isolation, looks correct, but the dashboard shows one number and an export shows another, and there's no obvious wrong-doer.
Most of the time, keeping the two engines aligned just means being careful when adding functions: anything new lands in both places, or it lands in neither. Psychrometric functions were a hole in that pattern. They existed server-side in Axon (NREL's psychrometricsExt) but had no equivalent client-side. HVAC-leaning users wanting to do dew point or enthalpy analysis on time-series sensor data ended up exporting to Excel. The gap was a real workflow tax, and closing it meant getting the same set of functions running in JavaScript.
Port, Don't Depend
When I started looking at this, the obvious option was to depend on an existing JavaScript psychrometrics package. PsychroLib is the closest thing to a de facto standard, with a JavaScript distribution among others. I decided not to use it.
The reason was the same equivalence contract. The server-side engine runs psychrometricsExt, and any other implementation, no matter how reputable, is a separate codebase with its own equations, its own coefficients, and its own Newton-Raphson tolerances. Even if the published outputs match to the fourth decimal place across most of the range, there will be inputs near the edges (low temperatures crossing the saturation regime, extreme humidity, the corners of the wet-bulb solver) where the two implementations disagree at the fifth decimal. That's exactly the kind of drift we needed to avoid. Porting the canonical Axon implementation directly was the cheapest way to keep both engines bit-for-bit aligned by construction.
Bonus arguments helped. The library is small: a dozen pure functions, well-bounded equations, no statefulness. Once written it never really needs to change, because the underlying equations don't. And one fewer dependency is one fewer thing to track.
The provenance chain runs Eric Kozubal's VBA spreadsheet → NREL Axon → my TypeScript. The equations themselves are canonical to the ASHRAE Fundamentals Handbook (2013), SI Edition, which is the authoritative reference HVAC engineers reach for. I worked from the Axon source directly, with the ASHRAE handbook open as a sanity check.
Here's where my prior career did some quiet work. I was a building services engineer before I moved into software, which means I'd already studied this material. I knew what humidity ratio is, why the saturation pressure equation switches regimes at 0°C (because water becomes ice and the empirical coefficients change), which input pairs are valid, and what the output values should roughly look like for any sane set of conditions. There was no domain ramp-up to do. I could read the Axon code, recognise what each function was doing, and sanity-check outputs by feel. Wrong values were obviously wrong.
The Library
The output is a small TypeScript library of pure functions, mirroring the Axon library's API verbatim. Same names, same signatures, same return types. That's deliberate: it keeps the mental model symmetric across server and client, so reasoning about a calculation in one context maps directly to the other. The calculator earlier on this page is running this library directly in your browser.
Most of the functions are short. The interesting ones are where the equations have boundary behaviour, like saturation pressure switching between an over-ice and over-water regime at 0°C:
export const saturationPressure = (t: number): number => {
const tK = celsiusToKelvin(t);
// Coefficients, ASHRAE Fundamentals Handbook (2013), SI Edition, p. 1.2
const c01 = -5.6745359e3;
// ... (coefficients elided)
if (t < 0) {
// Over ice: Eq. (5)
return (
Math.exp(
c01 / tK + c02 + c03 * tK + c04 * tK ** 2 +
c05 * tK ** 3 + c06 * tK ** 4 + c07 * Math.log(tK)
) / 1000
);
} else {
// Over liquid water: Eq. (6)
return (
Math.exp(
c08 / tK + c09 + c10 * tK + c11 * tK ** 2 +
c12 * tK ** 3 + c13 * Math.log(tK)
) / 1000
);
}
};The empirical coefficients aren't something a software engineer should be deriving from first principles. They come from decades of ASHRAE research, and the right thing to do is to copy them faithfully and reference the equation numbers in the comments so the next person can audit the port against the handbook.
The other interesting pattern is composition. Most of the higher-level properties are built from the lower-level ones:
export const relativeHumidity = (
tDB: number,
tWB: number,
p: number = 101.325
): number => {
const w = humidityRatio(tDB, tWB, p);
const pWS = saturationPressure(tDB);
const pW = partialPressure(p, w);
// Eq. (24), p. 1.8
return (pW / pWS) * 100;
};Each step has a physical meaning. Humidity ratio from the two temperatures, saturation pressure at the dry-bulb, actual partial pressure of water vapour, then the ratio of the two pressures as a percentage. Small functions, clear interfaces, the structure of the equations and the structure of the code line up.
Where It's Used Today
The library has been deployed in production as part of the client-side calculation engine since 2022. Where it most obviously earns its keep is in replacing an Excel-driven workflow: HVAC-leaning users who previously had to export time-series data and run dew point or enthalpy calculations in a spreadsheet can now do that work in-app, against live data, alongside everything else they're looking at.