@verifyhash/psychrometrics 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +185 -0
- package/index.js +233 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 <OWNER-NAME PLACEHOLDER>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# psychrometrics
|
|
2
|
+
|
|
3
|
+
<!-- publish-prep -->
|
|
4
|
+
> **TODO (owner): pick the final npm name/scope before publishing.**
|
|
5
|
+
> The npm package name is **not** finalized by this project. Convention used here:
|
|
6
|
+
> `package.json` keeps the working slug `psychrometrics` as a placeholder — replace it
|
|
7
|
+
> (and the `npm install` line below) with the real published name/scope before
|
|
8
|
+
> running `npm publish`.
|
|
9
|
+
|
|
10
|
+
A tiny, **zero-dependency** Node library for humidity and moist-air math:
|
|
11
|
+
saturation vapour pressure, dew point, relative and absolute humidity, the NOAA
|
|
12
|
+
heat index, and Stull's wet-bulb temperature.
|
|
13
|
+
|
|
14
|
+
Every export is a **pure function**: no I/O, no network, no daemon, no
|
|
15
|
+
dependencies, and no mutation of its arguments. The same inputs always give the
|
|
16
|
+
same output. Everything is in SI-friendly units — degrees Celsius, percent RH,
|
|
17
|
+
hectopascals (hPa == millibars), and grams per cubic metre.
|
|
18
|
+
|
|
19
|
+
## Who it's for
|
|
20
|
+
|
|
21
|
+
Developers building **HVAC**, **weather**, or **agriculture** tools who need
|
|
22
|
+
reliable moist-air numbers without pulling in a large scientific package or
|
|
23
|
+
hitting an API:
|
|
24
|
+
|
|
25
|
+
- an HVAC/indoor-climate app converting between temperature, RH, and dew point;
|
|
26
|
+
- a weather dashboard showing "feels like" (heat index) and wet-bulb stress;
|
|
27
|
+
- an agriculture / greenhouse model tracking condensation risk (dew point) and
|
|
28
|
+
vapour density (absolute humidity).
|
|
29
|
+
|
|
30
|
+
## Install / use
|
|
31
|
+
|
|
32
|
+
Copy the folder in, or `require` it directly — there is nothing to install.
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
const psy = require('./psychrometrics'); // path to this folder
|
|
36
|
+
|
|
37
|
+
psy.dewPoint(30, 50); // → 18.45 (°C)
|
|
38
|
+
psy.saturationVaporPressure(20); // → 23.33 (hPa)
|
|
39
|
+
psy.heatIndex(32, 70); // → 40.4 (°C, "feels like")
|
|
40
|
+
psy.absoluteHumidity(20, 50); // → 8.62 (g/m³)
|
|
41
|
+
psy.wetBulb(20, 50); // → 13.70 (°C)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Run the tests
|
|
45
|
+
|
|
46
|
+
One command, offline, no framework:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
node test/psychrometrics.test.js
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
It exits `0` on success and prints `N passed, 0 failed`. (Or `npm test`.)
|
|
53
|
+
|
|
54
|
+
## API
|
|
55
|
+
|
|
56
|
+
All temperatures are **°C**, all relative humidity is **percent (0..100)**,
|
|
57
|
+
vapour pressure is **hPa**, absolute humidity is **g/m³**.
|
|
58
|
+
|
|
59
|
+
### `saturationVaporPressure(tempC) → hPa`
|
|
60
|
+
|
|
61
|
+
Saturation vapour pressure of water over a flat liquid surface.
|
|
62
|
+
|
|
63
|
+
- **Formula:** Magnus/Tetens form, `es = 6.1094 · exp(17.625·T / (243.04 + T))`,
|
|
64
|
+
using the Alduchov & Eskridge (1996) "AERK" coefficients (*J. Appl. Meteorol.*
|
|
65
|
+
35, 601–609).
|
|
66
|
+
- **Accuracy:** better than **0.4 %** over roughly **−40 °C … +50 °C**.
|
|
67
|
+
- **Caveats:** over ice (below 0 °C) the true saturation pressure is slightly
|
|
68
|
+
lower than this over-liquid value; if you need frost-point work, use an
|
|
69
|
+
over-ice coefficient set. Not valid far outside the fitted range.
|
|
70
|
+
- Reference: `es(0 °C) = 6.1094 hPa`, `es(20 °C) ≈ 23.3 hPa`.
|
|
71
|
+
|
|
72
|
+
### `dewPoint(tempC, rhPct) → °C`
|
|
73
|
+
|
|
74
|
+
Temperature to which the air must cool (at constant pressure and water content)
|
|
75
|
+
to reach saturation.
|
|
76
|
+
|
|
77
|
+
- **Formula:** analytic Magnus inverse of `saturationVaporPressure`, with the
|
|
78
|
+
same AERK coefficients, so `dewPoint` and `relativeHumidity` are mutually
|
|
79
|
+
consistent.
|
|
80
|
+
- **Domain:** `rhPct` in `[0, 100]`. At `100 %` the dew point equals the air
|
|
81
|
+
temperature; at `0 %` it is `-Infinity` (dry air never saturates on cooling).
|
|
82
|
+
- **Accuracy:** inherits the < 0.4 % vapour-pressure fit; dew-point error is
|
|
83
|
+
typically a few hundredths of a degree versus an exact inversion.
|
|
84
|
+
- Reference: `dewPoint(30, 50) ≈ 18.4 °C`.
|
|
85
|
+
|
|
86
|
+
### `relativeHumidity(tempC, dewPointC) → %`
|
|
87
|
+
|
|
88
|
+
Relative humidity from air temperature and dew point, as the ratio of
|
|
89
|
+
saturation vapour pressure at the dew point to that at the air temperature.
|
|
90
|
+
|
|
91
|
+
- **Formula:** `100 · es(dewPointC) / es(tempC)`.
|
|
92
|
+
- **Round-trip:** `relativeHumidity(T, dewPoint(T, rh)) ≈ rh` to machine
|
|
93
|
+
precision (verified in the tests).
|
|
94
|
+
- **Caveat:** returns **> 100** when `dewPointC > tempC` (supersaturation);
|
|
95
|
+
that is intentional, not clamped, so callers can detect bad/edge inputs.
|
|
96
|
+
|
|
97
|
+
### `absoluteHumidity(tempC, rhPct) → g/m³`
|
|
98
|
+
|
|
99
|
+
Mass of water vapour per cubic metre of air (vapour density).
|
|
100
|
+
|
|
101
|
+
- **Formula:** ideal-gas law on the vapour partial pressure,
|
|
102
|
+
`AH = e / (Rv · T_K)` with `e = es(T)·RH/100` in Pa, `Rv = 461.5 J/(kg·K)`,
|
|
103
|
+
`T_K = T + 273.15`, result converted to g/m³.
|
|
104
|
+
- **Domain:** `rhPct` in `[0, 100]`. Linear in RH at fixed temperature.
|
|
105
|
+
- **Caveat:** treats vapour as an ideal gas (excellent at ambient conditions;
|
|
106
|
+
small errors near boiling). Independent of barometric pressure, since it is a
|
|
107
|
+
density, not a mixing ratio.
|
|
108
|
+
- Reference: `absoluteHumidity(20, 100) ≈ 17.3 g/m³` (saturated 20 °C air).
|
|
109
|
+
|
|
110
|
+
### `heatIndex(tempC, rhPct) → °C`
|
|
111
|
+
|
|
112
|
+
Apparent "feels-like" temperature from heat and humidity.
|
|
113
|
+
|
|
114
|
+
- **Formula:** NOAA/NWS **Rothfusz regression**. Computed internally in °F and
|
|
115
|
+
returned in °C. Matching the NWS reference implementation, it first tries the
|
|
116
|
+
simpler Steadman average; if that stays below **80 °F (≈ 27 °C)** the heat
|
|
117
|
+
index is essentially the air temperature and that value is returned.
|
|
118
|
+
Otherwise the full 9-term polynomial is applied, followed by the two
|
|
119
|
+
documented corrections — a **low-RH** subtraction (RH < 13 %, 80–112 °F) and a
|
|
120
|
+
**high-RH** addition (RH > 85 %, 80–87 °F).
|
|
121
|
+
- **Valid range:** the regression is fit for **hot, humid** conditions, roughly
|
|
122
|
+
**T ≳ 27 °C** and **RH ≳ 40 %**. Outside that the returned value approaches the
|
|
123
|
+
air temperature and should be read as "no meaningful heat stress," not as a
|
|
124
|
+
precise apparent temperature. Heat index also assumes shade and light wind;
|
|
125
|
+
direct sun can add up to ~8 °C.
|
|
126
|
+
- Reference: `heatIndex(32, 70) ≈ 40 °C` (well above the 32 °C air temperature).
|
|
127
|
+
|
|
128
|
+
### `wetBulb(tempC, rhPct) → °C`
|
|
129
|
+
|
|
130
|
+
Wet-bulb temperature: the lowest temperature reachable by evaporative cooling at
|
|
131
|
+
the given humidity — a key metric for heat stress and cooling-tower design.
|
|
132
|
+
|
|
133
|
+
- **Formula:** Stull (2011), *"Wet-Bulb Temperature from Relative Humidity and
|
|
134
|
+
Air Temperature"*, **J. Appl. Meteorol. Climatol. 50, 2267–2269**, an
|
|
135
|
+
empirical closed-form fit (no iteration).
|
|
136
|
+
- **Valid range:** standard **sea-level pressure (1013.25 hPa)**, roughly
|
|
137
|
+
**−20 °C ≤ T ≤ 50 °C** and **5 % ≤ RH ≤ 99 %**, where it agrees with an exact
|
|
138
|
+
psychrometric solver to about **±1 °C**. Error grows at very low RH / very low
|
|
139
|
+
temperature.
|
|
140
|
+
- **Caveat:** **not** altitude-corrected — at high elevation (lower pressure)
|
|
141
|
+
the true wet bulb is lower than this returns.
|
|
142
|
+
- Reference: `wetBulb(20, 50) ≈ 13.7 °C` (Stull's worked example).
|
|
143
|
+
|
|
144
|
+
## Domain guards
|
|
145
|
+
|
|
146
|
+
Inputs are validated so unit mistakes fail loudly instead of silently
|
|
147
|
+
producing garbage:
|
|
148
|
+
|
|
149
|
+
- Every numeric argument must be a **finite number** or a `TypeError` is thrown.
|
|
150
|
+
- `rhPct` must be within **`[0, 100]` percent** or a `RangeError` is thrown. The
|
|
151
|
+
library **throws rather than clamps** — a caller who accidentally passes a
|
|
152
|
+
`0..1` fraction (or a percentage over 100) finds out immediately. The test
|
|
153
|
+
suite covers this behaviour.
|
|
154
|
+
|
|
155
|
+
## Honest limits
|
|
156
|
+
|
|
157
|
+
- These are **standard engineering approximations**, not a full equation-of-state
|
|
158
|
+
psychrometric solver. For most HVAC/weather/agriculture work they are well
|
|
159
|
+
within measurement noise; for research-grade work near the edges of the stated
|
|
160
|
+
ranges, use a dedicated reference implementation.
|
|
161
|
+
- Saturation is computed **over liquid water**; sub-freezing frost-point work
|
|
162
|
+
needs an over-ice variant.
|
|
163
|
+
- `heatIndex` and `wetBulb` are empirical regressions valid in the ranges noted
|
|
164
|
+
above; outside them they degrade gracefully but are not authoritative.
|
|
165
|
+
|
|
166
|
+
## License
|
|
167
|
+
|
|
168
|
+
MIT.
|
|
169
|
+
|
|
170
|
+
## Install
|
|
171
|
+
|
|
172
|
+
> Placeholder name: the `psychrometrics` below is the working slug, not a finalized
|
|
173
|
+
> npm name — see the owner TODO near the top of this README.
|
|
174
|
+
|
|
175
|
+
```sh
|
|
176
|
+
npm install psychrometrics
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
```js
|
|
180
|
+
const psy = require('psychrometrics');
|
|
181
|
+
|
|
182
|
+
psy.dewPoint(30, 50); // 18.45 (°C)
|
|
183
|
+
psy.heatIndex(32, 70); // 40.4 (°C, "feels like")
|
|
184
|
+
psy.wetBulb(20, 50); // 13.70 (°C)
|
|
185
|
+
```
|
package/index.js
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* psychrometrics — zero-dependency humidity & moist-air math.
|
|
5
|
+
*
|
|
6
|
+
* Every export is a PURE function: no I/O, no network, no globals mutated, no
|
|
7
|
+
* arguments mutated, no dependencies. Given the same inputs it always returns
|
|
8
|
+
* the same output.
|
|
9
|
+
*
|
|
10
|
+
* The core saturation-vapour-pressure relation and its dew-point inverse use
|
|
11
|
+
* the Magnus form with the Alduchov & Eskridge (1996) "AERK" coefficients
|
|
12
|
+
* (a = 17.625, b = 243.04 °C, e0 = 6.1094 hPa), which fit water-vapour
|
|
13
|
+
* saturation to better than 0.4 % over roughly -40 °C .. +50 °C. Heat index
|
|
14
|
+
* follows the NOAA/NWS Rothfusz regression; wet-bulb follows Stull (2011).
|
|
15
|
+
* Sources are cited per-function below and in the README.
|
|
16
|
+
*
|
|
17
|
+
* UNITS (consistent throughout)
|
|
18
|
+
* tempC, dewPointC air / dew-point temperature in degrees Celsius
|
|
19
|
+
* rhPct relative humidity in percent, 0 .. 100
|
|
20
|
+
* pressure hectopascals (hPa, == millibars)
|
|
21
|
+
* absolute humidity grams of water vapour per cubic metre (g/m^3)
|
|
22
|
+
*
|
|
23
|
+
* DOMAIN GUARDS
|
|
24
|
+
* All inputs must be finite numbers or a TypeError is thrown. Relative
|
|
25
|
+
* humidity must be within [0, 100] or a RangeError is thrown (we throw rather
|
|
26
|
+
* than silently clamp so a caller's unit bug — e.g. passing a 0..1 fraction —
|
|
27
|
+
* surfaces immediately). See validateRh below.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Magnus / Alduchov-Eskridge (1996) coefficients over liquid water.
|
|
32
|
+
// es(T) = E0 * exp(A * T / (B + T)), T in °C, es in hPa.
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
const A = 17.625;
|
|
35
|
+
const B = 243.04; // °C
|
|
36
|
+
const E0 = 6.1094; // hPa, saturation vapour pressure at 0 °C
|
|
37
|
+
|
|
38
|
+
// Specific gas constant for water vapour, J/(kg·K).
|
|
39
|
+
const RV = 461.5;
|
|
40
|
+
const KELVIN = 273.15;
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Input validation helpers
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
function num(name, v) {
|
|
46
|
+
if (typeof v !== 'number' || !Number.isFinite(v)) {
|
|
47
|
+
throw new TypeError(`${name} must be a finite number, got ${v}`);
|
|
48
|
+
}
|
|
49
|
+
return v;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function validateRh(rhPct) {
|
|
53
|
+
num('rhPct', rhPct);
|
|
54
|
+
if (rhPct < 0 || rhPct > 100) {
|
|
55
|
+
throw new RangeError(
|
|
56
|
+
`rhPct must be in [0, 100] percent, got ${rhPct} ` +
|
|
57
|
+
'(note: this API uses percent, not a 0..1 fraction)'
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
return rhPct;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// 1. Saturation vapour pressure — Magnus/Tetens form (hPa)
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
/**
|
|
67
|
+
* Saturation vapour pressure of water in air, in hectopascals (hPa == mbar).
|
|
68
|
+
* Magnus formula with Alduchov & Eskridge (1996) coefficients over liquid
|
|
69
|
+
* water. Accurate to < 0.4 % across ~ -40 °C .. +50 °C.
|
|
70
|
+
* es(20 °C) ≈ 23.34 hPa; es(0 °C) = 6.1094 hPa.
|
|
71
|
+
* @param {number} tempC air temperature, °C
|
|
72
|
+
* @returns {number} saturation vapour pressure, hPa
|
|
73
|
+
*/
|
|
74
|
+
function saturationVaporPressure(tempC) {
|
|
75
|
+
num('tempC', tempC);
|
|
76
|
+
return E0 * Math.exp((A * tempC) / (B + tempC));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// 2. Dew point — Magnus inverse (°C)
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
/**
|
|
83
|
+
* Dew-point temperature: the temperature to which air must cool (at constant
|
|
84
|
+
* pressure and water content) to reach saturation. Magnus inverse.
|
|
85
|
+
* dewPoint(30, 50) ≈ 18.45 °C.
|
|
86
|
+
* @param {number} tempC air temperature, °C
|
|
87
|
+
* @param {number} rhPct relative humidity, percent 0..100
|
|
88
|
+
* @returns {number} dew-point temperature, °C
|
|
89
|
+
*/
|
|
90
|
+
function dewPoint(tempC, rhPct) {
|
|
91
|
+
num('tempC', tempC);
|
|
92
|
+
validateRh(rhPct);
|
|
93
|
+
// RH of 0 has no finite dew point (dry air never saturates on cooling).
|
|
94
|
+
if (rhPct === 0) return -Infinity;
|
|
95
|
+
const alpha = Math.log(rhPct / 100) + (A * tempC) / (B + tempC);
|
|
96
|
+
return (B * alpha) / (A - alpha);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// 3. Relative humidity from temperature and dew point (%)
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
/**
|
|
103
|
+
* Relative humidity (%) from air temperature and dew point, as the ratio of
|
|
104
|
+
* saturation vapour pressure at the dew point to that at the air temperature.
|
|
105
|
+
* Inverse-consistent with dewPoint(): relativeHumidity(T, dewPoint(T, rh)) ≈ rh.
|
|
106
|
+
* @param {number} tempC air temperature, °C
|
|
107
|
+
* @param {number} dewPointC dew-point temperature, °C
|
|
108
|
+
* @returns {number} relative humidity, percent (may exceed 100 if dewPointC > tempC)
|
|
109
|
+
*/
|
|
110
|
+
function relativeHumidity(tempC, dewPointC) {
|
|
111
|
+
num('tempC', tempC);
|
|
112
|
+
num('dewPointC', dewPointC);
|
|
113
|
+
return (
|
|
114
|
+
100 *
|
|
115
|
+
(saturationVaporPressure(dewPointC) / saturationVaporPressure(tempC))
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// 4. Absolute humidity — mass of vapour per unit volume (g/m^3)
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
/**
|
|
123
|
+
* Absolute humidity: mass of water vapour per cubic metre of air, in g/m^3.
|
|
124
|
+
* Derived from the ideal-gas law for the vapour partial pressure:
|
|
125
|
+
* AH = e / (Rv · T_K), e = es(T)·RH/100 (Pa), Rv = 461.5 J/(kg·K).
|
|
126
|
+
* At 20 °C / 50 % RH ≈ 8.6 g/m^3; saturated 20 °C air holds ≈ 17.3 g/m^3.
|
|
127
|
+
* @param {number} tempC air temperature, °C
|
|
128
|
+
* @param {number} rhPct relative humidity, percent 0..100
|
|
129
|
+
* @returns {number} absolute humidity, g/m^3
|
|
130
|
+
*/
|
|
131
|
+
function absoluteHumidity(tempC, rhPct) {
|
|
132
|
+
num('tempC', tempC);
|
|
133
|
+
validateRh(rhPct);
|
|
134
|
+
const ePa = saturationVaporPressure(tempC) * (rhPct / 100) * 100; // hPa -> Pa
|
|
135
|
+
const kgPerM3 = ePa / (RV * (tempC + KELVIN));
|
|
136
|
+
return kgPerM3 * 1000; // kg -> g
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// 5. Heat index — NOAA/NWS Rothfusz regression (returns °C)
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
/**
|
|
143
|
+
* Apparent temperature ("feels like") from heat and humidity, via the NOAA/NWS
|
|
144
|
+
* Rothfusz regression. Computed internally in °F, returned in °C.
|
|
145
|
+
*
|
|
146
|
+
* Method (matching the NWS reference implementation):
|
|
147
|
+
* 1. Try the simple Steadman average first. If it stays below 80 °F, that
|
|
148
|
+
* value is the heat index (the full regression is only fit for hot,
|
|
149
|
+
* humid conditions).
|
|
150
|
+
* 2. Otherwise apply the full Rothfusz polynomial, then the two documented
|
|
151
|
+
* corrections: a low-RH subtraction (RH < 13 %, 80..112 °F) and a high-RH
|
|
152
|
+
* addition (RH > 85 %, 80..87 °F).
|
|
153
|
+
*
|
|
154
|
+
* VALID RANGE: meaningful mainly for T ≳ 27 °C (80 °F) and RH ≳ 40 %. Below
|
|
155
|
+
* that the heat index ≈ air temperature and this returns the Steadman value.
|
|
156
|
+
* @param {number} tempC air temperature, °C
|
|
157
|
+
* @param {number} rhPct relative humidity, percent 0..100
|
|
158
|
+
* @returns {number} heat index (apparent temperature), °C
|
|
159
|
+
*/
|
|
160
|
+
function heatIndex(tempC, rhPct) {
|
|
161
|
+
num('tempC', tempC);
|
|
162
|
+
validateRh(rhPct);
|
|
163
|
+
|
|
164
|
+
const T = (tempC * 9) / 5 + 32; // °F
|
|
165
|
+
const R = rhPct;
|
|
166
|
+
|
|
167
|
+
// Steadman "simple" formula, averaged with the dry-bulb temperature.
|
|
168
|
+
const simple = 0.5 * (T + 61.0 + (T - 68.0) * 1.2 + R * 0.094);
|
|
169
|
+
const avg = (simple + T) / 2;
|
|
170
|
+
|
|
171
|
+
let hiF;
|
|
172
|
+
if (avg < 80) {
|
|
173
|
+
hiF = avg;
|
|
174
|
+
} else {
|
|
175
|
+
hiF =
|
|
176
|
+
-42.379 +
|
|
177
|
+
2.04901523 * T +
|
|
178
|
+
10.14333127 * R -
|
|
179
|
+
0.22475541 * T * R -
|
|
180
|
+
0.00683783 * T * T -
|
|
181
|
+
0.05481717 * R * R +
|
|
182
|
+
0.00122874 * T * T * R +
|
|
183
|
+
0.00085282 * T * R * R -
|
|
184
|
+
0.00000199 * T * T * R * R;
|
|
185
|
+
|
|
186
|
+
if (R < 13 && T >= 80 && T <= 112) {
|
|
187
|
+
hiF -= ((13 - R) / 4) * Math.sqrt((17 - Math.abs(T - 95)) / 17);
|
|
188
|
+
} else if (R > 85 && T >= 80 && T <= 87) {
|
|
189
|
+
hiF += ((R - 85) / 10) * ((87 - T) / 5);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return ((hiF - 32) * 5) / 9; // °F -> °C
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
// 6. Wet-bulb temperature — Stull (2011) approximation (°C)
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
/**
|
|
200
|
+
* Wet-bulb temperature via Stull's (2011) empirical fit,
|
|
201
|
+
* "Wet-Bulb Temperature from Relative Humidity and Air Temperature",
|
|
202
|
+
* J. Appl. Meteorol. Climatol. 50, 2267–2269.
|
|
203
|
+
* Valid at standard sea-level pressure (1013.25 hPa) for roughly
|
|
204
|
+
* -20 °C ≤ T ≤ 50 °C and 5 % ≤ RH ≤ 99 %,
|
|
205
|
+
* where it agrees with an exact psychrometric solver to about ±1 °C (larger
|
|
206
|
+
* error at very low RH / very low T). Not corrected for altitude.
|
|
207
|
+
* wetBulb(20, 50) ≈ 13.7 °C.
|
|
208
|
+
* @param {number} tempC air temperature, °C
|
|
209
|
+
* @param {number} rhPct relative humidity, percent 0..100
|
|
210
|
+
* @returns {number} wet-bulb temperature, °C
|
|
211
|
+
*/
|
|
212
|
+
function wetBulb(tempC, rhPct) {
|
|
213
|
+
num('tempC', tempC);
|
|
214
|
+
validateRh(rhPct);
|
|
215
|
+
const T = tempC;
|
|
216
|
+
const R = rhPct;
|
|
217
|
+
return (
|
|
218
|
+
T * Math.atan(0.151977 * Math.sqrt(R + 8.313659)) +
|
|
219
|
+
Math.atan(T + R) -
|
|
220
|
+
Math.atan(R - 1.676331) +
|
|
221
|
+
0.00391838 * Math.pow(R, 1.5) * Math.atan(0.023101 * R) -
|
|
222
|
+
4.686035
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
module.exports = {
|
|
227
|
+
saturationVaporPressure,
|
|
228
|
+
dewPoint,
|
|
229
|
+
relativeHumidity,
|
|
230
|
+
absoluteHumidity,
|
|
231
|
+
heatIndex,
|
|
232
|
+
wetBulb,
|
|
233
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@verifyhash/psychrometrics",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Zero-dependency humidity & moist-air math: saturation vapour pressure, dew point, relative & absolute humidity, NOAA heat index, and Stull wet-bulb temperature.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "node test/psychrometrics.test.js"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"psychrometrics",
|
|
12
|
+
"humidity",
|
|
13
|
+
"dew-point",
|
|
14
|
+
"relative-humidity",
|
|
15
|
+
"absolute-humidity",
|
|
16
|
+
"vapor-pressure",
|
|
17
|
+
"heat-index",
|
|
18
|
+
"wet-bulb",
|
|
19
|
+
"magnus",
|
|
20
|
+
"hvac",
|
|
21
|
+
"meteorology",
|
|
22
|
+
"zero-dependency"
|
|
23
|
+
],
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"files": [
|
|
26
|
+
"index.js",
|
|
27
|
+
"README.md"
|
|
28
|
+
],
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/verifyhash/libs.git",
|
|
35
|
+
"directory": "psychrometrics"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/verifyhash/libs/tree/main/psychrometrics#readme",
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/verifyhash/libs/issues"
|
|
40
|
+
}
|
|
41
|
+
}
|