@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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +185 -0
  3. package/index.js +233 -0
  4. 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
+ }