@bradtech/sensor-air 1.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.
@@ -0,0 +1,40 @@
1
+ import { BaseSensorConverter, type ConversionContext, type ConversionOutput } from '@bradtech/sensor';
2
+ /**
3
+ * Raw input reading for canopy-level ambient microclimate.
4
+ */
5
+ export interface CanopyAirReading {
6
+ /** In-canopy air temperature in °C */
7
+ temperature: number;
8
+ /** In-canopy relative humidity in % (0 - 100) */
9
+ humidity: number;
10
+ }
11
+ /**
12
+ * In-plot Canopy Microclimate & Foliar VPD Converter.
13
+ *
14
+ * Computes:
15
+ * - Direct canopy temperature (°C)
16
+ * - Direct canopy relative humidity (%)
17
+ * - Foliar Dew Point (°C) via Magnus-Tetens formula:
18
+ * $$\gamma = \frac{a \cdot T}{b + T} + \ln\left(\frac{RH}{100}\right), \quad T_{dew} = \frac{b \cdot \gamma}{a - \gamma}$$
19
+ * - Foliar Vapor Pressure Deficit ($VPD_{canopy}$ in kPa) via Tetens saturation vapor pressure:
20
+ * $$e_s(T) = 0.61078 \cdot \exp\left(\frac{17.27 \cdot T}{T + 237.3}\right), \quad VPD = e_s(T) \cdot \left(1 - \frac{RH}{100}\right)$$
21
+ */
22
+ export declare class CanopyMicroclimateConverter extends BaseSensorConverter<CanopyAirReading> {
23
+ /** Sensor domain family classification */
24
+ readonly sensorFamily = "air";
25
+ /** Unique algorithmic model code */
26
+ readonly modelCode = "canopy-microclimate-evaluator";
27
+ /** Algorithm semver version */
28
+ readonly modelVersion = "1.0.0";
29
+ /** Human-readable model description */
30
+ readonly description = "In-plot canopy microclimate, foliar VPD, and dew point converter";
31
+ /**
32
+ * Converts raw canopy temperature and humidity readings into normalized microclimate and bioclimatic indicators.
33
+ *
34
+ * @param raw - Object containing canopy temperature and relative humidity.
35
+ * @param _context - Optional environmental conversion context.
36
+ * @returns Array containing temperature, humidity, dew point, and foliar VPD metrics.
37
+ */
38
+ convert(raw: CanopyAirReading, _context?: ConversionContext): ConversionOutput[];
39
+ }
40
+ //# sourceMappingURL=CanopyMicroclimateConverter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CanopyMicroclimateConverter.d.ts","sourceRoot":"","sources":["../src/CanopyMicroclimateConverter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,KAAK,iBAAiB,EAAE,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAErG;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC9B,sCAAsC;IACtC,WAAW,EAAE,MAAM,CAAA;IACnB,iDAAiD;IACjD,QAAQ,EAAE,MAAM,CAAA;CAClB;AAED;;;;;;;;;;GAUG;AACH,qBAAa,2BAA4B,SAAQ,mBAAmB,CAAC,gBAAgB,CAAC;IACnF,0CAA0C;IAC1C,QAAQ,CAAC,YAAY,SAAQ;IAC7B,oCAAoC;IACpC,QAAQ,CAAC,SAAS,mCAAkC;IACpD,+BAA+B;IAC/B,QAAQ,CAAC,YAAY,WAAU;IAC/B,uCAAuC;IACvC,QAAQ,CAAC,WAAW,sEAAqE;IAEzF;;;;;;OAMG;IACH,OAAO,CAAC,GAAG,EAAE,gBAAgB,EAAE,QAAQ,CAAC,EAAE,iBAAiB,GAAG,gBAAgB,EAAE;CA+DlF"}
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CanopyMicroclimateConverter = void 0;
4
+ const sensor_1 = require("@bradtech/sensor");
5
+ /**
6
+ * In-plot Canopy Microclimate & Foliar VPD Converter.
7
+ *
8
+ * Computes:
9
+ * - Direct canopy temperature (°C)
10
+ * - Direct canopy relative humidity (%)
11
+ * - Foliar Dew Point (°C) via Magnus-Tetens formula:
12
+ * $$\gamma = \frac{a \cdot T}{b + T} + \ln\left(\frac{RH}{100}\right), \quad T_{dew} = \frac{b \cdot \gamma}{a - \gamma}$$
13
+ * - Foliar Vapor Pressure Deficit ($VPD_{canopy}$ in kPa) via Tetens saturation vapor pressure:
14
+ * $$e_s(T) = 0.61078 \cdot \exp\left(\frac{17.27 \cdot T}{T + 237.3}\right), \quad VPD = e_s(T) \cdot \left(1 - \frac{RH}{100}\right)$$
15
+ */
16
+ class CanopyMicroclimateConverter extends sensor_1.BaseSensorConverter {
17
+ constructor() {
18
+ super(...arguments);
19
+ /** Sensor domain family classification */
20
+ this.sensorFamily = 'air';
21
+ /** Unique algorithmic model code */
22
+ this.modelCode = 'canopy-microclimate-evaluator';
23
+ /** Algorithm semver version */
24
+ this.modelVersion = '1.0.0';
25
+ /** Human-readable model description */
26
+ this.description = 'In-plot canopy microclimate, foliar VPD, and dew point converter';
27
+ }
28
+ /**
29
+ * Converts raw canopy temperature and humidity readings into normalized microclimate and bioclimatic indicators.
30
+ *
31
+ * @param raw - Object containing canopy temperature and relative humidity.
32
+ * @param _context - Optional environmental conversion context.
33
+ * @returns Array containing temperature, humidity, dew point, and foliar VPD metrics.
34
+ */
35
+ convert(raw, _context) {
36
+ const outputs = [];
37
+ // 1. Canopy Temperature
38
+ const tempClamped = this.clampWithConfidence(raw.temperature, -40, 65, -20, 50);
39
+ outputs.push({
40
+ metric: 'okf:agronomy/microclimate/canopy_temperature',
41
+ value: tempClamped.value,
42
+ unit: '°C',
43
+ qudtUri: 'qudt:unit/DEG_C',
44
+ confidence: tempClamped.confidence,
45
+ });
46
+ // 2. Canopy Relative Humidity
47
+ const humClamped = this.clampWithConfidence(raw.humidity, 0, 100);
48
+ outputs.push({
49
+ metric: 'okf:agronomy/microclimate/canopy_humidity',
50
+ value: humClamped.value,
51
+ unit: '%',
52
+ qudtUri: 'qudt:unit/PERCENT',
53
+ confidence: humClamped.confidence,
54
+ });
55
+ // 3. Foliar Dew Point (Magnus-Tetens)
56
+ const T = tempClamped.value;
57
+ const RH = Math.max(0.1, humClamped.value);
58
+ const a = 17.27;
59
+ const b = 237.7;
60
+ const gamma = (a * T) / (b + T) + Math.log(RH / 100.0);
61
+ const dewPoint = (b * gamma) / (a - gamma);
62
+ const dewPointClamped = this.clampWithConfidence(dewPoint, -50, 50);
63
+ outputs.push({
64
+ metric: 'okf:agronomy/microclimate/dew_point',
65
+ value: dewPointClamped.value,
66
+ unit: '°C',
67
+ qudtUri: 'qudt:unit/DEG_C',
68
+ confidence: Math.min(tempClamped.confidence, humClamped.confidence),
69
+ metadata: { formula: 'Magnus-Tetens' },
70
+ });
71
+ // 4. Foliar Vapor Pressure Deficit (VPD in kPa)
72
+ const es = 0.61078 * Math.exp((17.27 * T) / (T + 237.3));
73
+ const ea = es * (RH / 100.0);
74
+ const vpd = Math.max(0, es - ea);
75
+ const vpdClamped = this.clampWithConfidence(vpd, 0, 10, 0, 5);
76
+ outputs.push({
77
+ metric: 'okf:agronomy/plant/foliar_vpd',
78
+ value: vpdClamped.value,
79
+ unit: 'kPa',
80
+ qudtUri: 'qudt:unit/KiloPA',
81
+ confidence: Math.min(tempClamped.confidence, humClamped.confidence),
82
+ metadata: {
83
+ saturationVaporPressureKpa: Number(es.toFixed(3)),
84
+ actualVaporPressureKpa: Number(ea.toFixed(3)),
85
+ transpirationStress: vpd > 2.0 ? 'high_water_stress' : vpd < 0.4 ? 'low_transpiration_fungal_risk' : 'optimal',
86
+ },
87
+ });
88
+ return outputs;
89
+ }
90
+ }
91
+ exports.CanopyMicroclimateConverter = CanopyMicroclimateConverter;
92
+ //# sourceMappingURL=CanopyMicroclimateConverter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CanopyMicroclimateConverter.js","sourceRoot":"","sources":["../src/CanopyMicroclimateConverter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ import { BaseSensorConverter, type ConversionContext, type ConversionOutput } from '@bradtech/sensor';
2
+ /**
3
+ * Daily temperature and radiation inputs required for reference evapotranspiration computation.
4
+ */
5
+ export interface EvapotranspirationInput {
6
+ /** Daily minimum canopy temperature in °C ($T_{min}$) */
7
+ tempMin: number;
8
+ /** Daily maximum canopy temperature in °C ($T_{max}$) */
9
+ tempMax: number;
10
+ /** Daily mean canopy temperature in °C ($T_{mean}$) */
11
+ tempMean: number;
12
+ /** Global extraterrestrial or measured solar radiation flux in $\text{MJ}/(\text{m}^2\cdot\text{day})$ ($R_a$) */
13
+ solarRadiationMj?: number;
14
+ /** Geographic plot latitude in decimal degrees (e.g. 44.8378 for Bordeaux) */
15
+ latitudeDegrees?: number;
16
+ /** Julian day of year ($1 - 366$) */
17
+ dayOfYear?: number;
18
+ }
19
+ /**
20
+ * Agronomic Daily Reference Evapotranspiration ($ET_0$) Converter.
21
+ *
22
+ * Implements the internationally recognized FAO-56 Hargreaves-Samani temperature-radiation formulation:
23
+ * $$ET_0 = 0.0023 \cdot (T_{mean} + 17.8) \cdot \sqrt{T_{max} - T_{min}} \cdot R_a \cdot 0.408$$
24
+ *
25
+ * where $0.408$ represents the inverse of the latent heat of vaporization ($\lambda^{-1}$ in $\text{mm}/(\text{MJ}\cdot\text{m}^{-2})$).
26
+ */
27
+ export declare class EvapotranspirationConverter extends BaseSensorConverter<EvapotranspirationInput> {
28
+ /** Sensor domain family classification */
29
+ readonly sensorFamily = "air";
30
+ /** Unique algorithmic model code */
31
+ readonly modelCode = "evapotranspiration-hargreaves-fao56";
32
+ /** Algorithm semver version */
33
+ readonly modelVersion = "1.0.0";
34
+ /** Human-readable model description */
35
+ readonly description = "FAO-56 Hargreaves-Samani daily reference evapotranspiration (ET0) calculator";
36
+ /**
37
+ * Converts daily temperature extremes and radiation into daily reference crop evapotranspiration ($ET_0$ in mm/day).
38
+ *
39
+ * @param raw - Object containing $T_{min}, T_{max}, T_{mean}$, and optional radiation $R_a$.
40
+ * @param _context - Optional environmental context.
41
+ * @returns Single-element array containing the $ET_0$ metric in mm/day.
42
+ */
43
+ convert(raw: EvapotranspirationInput, _context?: ConversionContext): ConversionOutput[];
44
+ }
45
+ //# sourceMappingURL=EvapotranspirationConverter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EvapotranspirationConverter.d.ts","sourceRoot":"","sources":["../src/EvapotranspirationConverter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,KAAK,iBAAiB,EAAE,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAErG;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACrC,yDAAyD;IACzD,OAAO,EAAE,MAAM,CAAA;IACf,yDAAyD;IACzD,OAAO,EAAE,MAAM,CAAA;IACf,uDAAuD;IACvD,QAAQ,EAAE,MAAM,CAAA;IAChB,kHAAkH;IAClH,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,8EAA8E;IAC9E,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,qCAAqC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAA;CACpB;AAED;;;;;;;GAOG;AACH,qBAAa,2BAA4B,SAAQ,mBAAmB,CAAC,uBAAuB,CAAC;IAC1F,0CAA0C;IAC1C,QAAQ,CAAC,YAAY,SAAQ;IAC7B,oCAAoC;IACpC,QAAQ,CAAC,SAAS,yCAAwC;IAC1D,+BAA+B;IAC/B,QAAQ,CAAC,YAAY,WAAU;IAC/B,uCAAuC;IACvC,QAAQ,CAAC,WAAW,kFAAiF;IAErG;;;;;;OAMG;IACH,OAAO,CAAC,GAAG,EAAE,uBAAuB,EAAE,QAAQ,CAAC,EAAE,iBAAiB,GAAG,gBAAgB,EAAE;CA+BzF"}
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EvapotranspirationConverter = void 0;
4
+ const sensor_1 = require("@bradtech/sensor");
5
+ /**
6
+ * Agronomic Daily Reference Evapotranspiration ($ET_0$) Converter.
7
+ *
8
+ * Implements the internationally recognized FAO-56 Hargreaves-Samani temperature-radiation formulation:
9
+ * $$ET_0 = 0.0023 \cdot (T_{mean} + 17.8) \cdot \sqrt{T_{max} - T_{min}} \cdot R_a \cdot 0.408$$
10
+ *
11
+ * where $0.408$ represents the inverse of the latent heat of vaporization ($\lambda^{-1}$ in $\text{mm}/(\text{MJ}\cdot\text{m}^{-2})$).
12
+ */
13
+ class EvapotranspirationConverter extends sensor_1.BaseSensorConverter {
14
+ constructor() {
15
+ super(...arguments);
16
+ /** Sensor domain family classification */
17
+ this.sensorFamily = 'air';
18
+ /** Unique algorithmic model code */
19
+ this.modelCode = 'evapotranspiration-hargreaves-fao56';
20
+ /** Algorithm semver version */
21
+ this.modelVersion = '1.0.0';
22
+ /** Human-readable model description */
23
+ this.description = 'FAO-56 Hargreaves-Samani daily reference evapotranspiration (ET0) calculator';
24
+ }
25
+ /**
26
+ * Converts daily temperature extremes and radiation into daily reference crop evapotranspiration ($ET_0$ in mm/day).
27
+ *
28
+ * @param raw - Object containing $T_{min}, T_{max}, T_{mean}$, and optional radiation $R_a$.
29
+ * @param _context - Optional environmental context.
30
+ * @returns Single-element array containing the $ET_0$ metric in mm/day.
31
+ */
32
+ convert(raw, _context) {
33
+ const deltaT = Math.max(0, raw.tempMax - raw.tempMin);
34
+ const tMean = raw.tempMean;
35
+ // Extraterrestrial radiation estimate (Ra) if not provided directly
36
+ let Ra = raw.solarRadiationMj;
37
+ if (!Ra || Ra <= 0) {
38
+ // Default approximation for temperate European agricultural latitudes (~44°N)
39
+ Ra = 25.0;
40
+ }
41
+ // Hargreaves-Samani equation: ET0 = 0.0023 * (Tmean + 17.8) * sqrt(Tmax - Tmin) * Ra * 0.408
42
+ const et0 = 0.0023 * (tMean + 17.8) * Math.sqrt(deltaT) * Ra * 0.408;
43
+ const clamped = this.clampWithConfidence(et0, 0, 15, 0, 10);
44
+ return [
45
+ {
46
+ metric: 'okf:agronomy/evapotranspiration/et0',
47
+ value: clamped.value,
48
+ unit: 'mm/day',
49
+ qudtUri: 'qudt:unit/MilliM',
50
+ confidence: clamped.confidence,
51
+ metadata: {
52
+ method: 'Hargreaves-Samani-FAO56',
53
+ tempRange: Number(deltaT.toFixed(2)),
54
+ radiationMj: Ra,
55
+ },
56
+ },
57
+ ];
58
+ }
59
+ }
60
+ exports.EvapotranspirationConverter = EvapotranspirationConverter;
61
+ //# sourceMappingURL=EvapotranspirationConverter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EvapotranspirationConverter.js","sourceRoot":"","sources":["../src/EvapotranspirationConverter.ts"],"names":[],"mappings":";;;AAAA,6CAAqG;AAoBrG;;;;;;;GAOG;AACH,MAAa,2BAA4B,SAAQ,4BAA4C;IAA7F;;QACG,0CAA0C;QACjC,iBAAY,GAAG,KAAK,CAAA;QAC7B,oCAAoC;QAC3B,cAAS,GAAG,qCAAqC,CAAA;QAC1D,+BAA+B;QACtB,iBAAY,GAAG,OAAO,CAAA;QAC/B,uCAAuC;QAC9B,gBAAW,GAAG,8EAA8E,CAAA;IAwCxG,CAAC;IAtCE;;;;;;OAMG;IACH,OAAO,CAAC,GAA4B,EAAE,QAA4B;QAC/D,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC,CAAA;QACrD,MAAM,KAAK,GAAG,GAAG,CAAC,QAAQ,CAAA;QAE1B,oEAAoE;QACpE,IAAI,EAAE,GAAG,GAAG,CAAC,gBAAgB,CAAA;QAC7B,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC;YAClB,8EAA8E;YAC9E,EAAE,GAAG,IAAI,CAAA;QACZ,CAAC;QAED,6FAA6F;QAC7F,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,GAAG,KAAK,CAAA;QAEpE,MAAM,OAAO,GAAG,IAAI,CAAC,mBAAmB,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,CAAA;QAE3D,OAAO;YACJ;gBACG,MAAM,EAAE,qCAAqC;gBAC7C,KAAK,EAAE,OAAO,CAAC,KAAK;gBACpB,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,kBAAkB;gBAC3B,UAAU,EAAE,OAAO,CAAC,UAAU;gBAC9B,QAAQ,EAAE;oBACP,MAAM,EAAE,yBAAyB;oBACjC,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;oBACpC,WAAW,EAAE,EAAE;iBACjB;aACH;SACH,CAAA;IACJ,CAAC;CACH;AAhDD,kEAgDC"}
@@ -0,0 +1,38 @@
1
+ import { BaseSensorConverter, type ConversionContext, type ConversionOutput } from '@bradtech/sensor';
2
+ /**
3
+ * Microclimate temperature and relative humidity inputs for frost risk evaluation.
4
+ */
5
+ export interface GroundFrostInput {
6
+ /** Air temperature measured at plant/canopy height in °C ($T$) */
7
+ canopyTemperature: number;
8
+ /** Relative humidity measured at plant/canopy height in % ($RH$) */
9
+ canopyHumidity: number;
10
+ }
11
+ /**
12
+ * Radiative Ground Frost Risk & Wet-Bulb Temperature Converter.
13
+ *
14
+ * Implements the Roland Stull empirical formulation to compute the Psychrometric Wet-Bulb
15
+ * Temperature ($T_w$ in °C), which is the exact physical temperature reached by plant leaves
16
+ * and vegetative buds under radiative night cooling:
17
+ *
18
+ * $$T_w = T \cdot \arctan\left(0.151977 \cdot (RH + 8.313659)^{1/2}\right) + \arctan(T + RH) - \arctan(RH - 1.676331) + 0.00391838 \cdot RH^{3/2} \cdot \arctan(0.023101 \cdot RH) - 4.686035$$
19
+ */
20
+ export declare class GroundFrostRiskConverter extends BaseSensorConverter<GroundFrostInput> {
21
+ /** Sensor domain family classification */
22
+ readonly sensorFamily = "air";
23
+ /** Unique algorithmic model code */
24
+ readonly modelCode = "ground-frost-risk-evaluator";
25
+ /** Algorithm semver version */
26
+ readonly modelVersion = "1.0.0";
27
+ /** Human-readable model description */
28
+ readonly description = "Radiative ground frost risk and wet-bulb temperature calculator";
29
+ /**
30
+ * Converts canopy microclimate readings into wet-bulb temperature ($T_w$) and discrete agronomic frost severity index.
31
+ *
32
+ * @param raw - Object containing canopy temperature and relative humidity.
33
+ * @param _context - Optional environmental conversion context.
34
+ * @returns Array containing the wet-bulb temperature and the discrete frost risk index (0 to 3).
35
+ */
36
+ convert(raw: GroundFrostInput, _context?: ConversionContext): ConversionOutput[];
37
+ }
38
+ //# sourceMappingURL=GroundFrostRiskConverter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GroundFrostRiskConverter.d.ts","sourceRoot":"","sources":["../src/GroundFrostRiskConverter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,KAAK,iBAAiB,EAAE,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAErG;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC9B,kEAAkE;IAClE,iBAAiB,EAAE,MAAM,CAAA;IACzB,oEAAoE;IACpE,cAAc,EAAE,MAAM,CAAA;CACxB;AAED;;;;;;;;GAQG;AACH,qBAAa,wBAAyB,SAAQ,mBAAmB,CAAC,gBAAgB,CAAC;IAChF,0CAA0C;IAC1C,QAAQ,CAAC,YAAY,SAAQ;IAC7B,oCAAoC;IACpC,QAAQ,CAAC,SAAS,iCAAgC;IAClD,+BAA+B;IAC/B,QAAQ,CAAC,YAAY,WAAU;IAC/B,uCAAuC;IACvC,QAAQ,CAAC,WAAW,qEAAoE;IAExF;;;;;;OAMG;IACH,OAAO,CAAC,GAAG,EAAE,gBAAgB,EAAE,QAAQ,CAAC,EAAE,iBAAiB,GAAG,gBAAgB,EAAE;CAwClF"}
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GroundFrostRiskConverter = void 0;
4
+ const sensor_1 = require("@bradtech/sensor");
5
+ /**
6
+ * Radiative Ground Frost Risk & Wet-Bulb Temperature Converter.
7
+ *
8
+ * Implements the Roland Stull empirical formulation to compute the Psychrometric Wet-Bulb
9
+ * Temperature ($T_w$ in °C), which is the exact physical temperature reached by plant leaves
10
+ * and vegetative buds under radiative night cooling:
11
+ *
12
+ * $$T_w = T \cdot \arctan\left(0.151977 \cdot (RH + 8.313659)^{1/2}\right) + \arctan(T + RH) - \arctan(RH - 1.676331) + 0.00391838 \cdot RH^{3/2} \cdot \arctan(0.023101 \cdot RH) - 4.686035$$
13
+ */
14
+ class GroundFrostRiskConverter extends sensor_1.BaseSensorConverter {
15
+ constructor() {
16
+ super(...arguments);
17
+ /** Sensor domain family classification */
18
+ this.sensorFamily = 'air';
19
+ /** Unique algorithmic model code */
20
+ this.modelCode = 'ground-frost-risk-evaluator';
21
+ /** Algorithm semver version */
22
+ this.modelVersion = '1.0.0';
23
+ /** Human-readable model description */
24
+ this.description = 'Radiative ground frost risk and wet-bulb temperature calculator';
25
+ }
26
+ /**
27
+ * Converts canopy microclimate readings into wet-bulb temperature ($T_w$) and discrete agronomic frost severity index.
28
+ *
29
+ * @param raw - Object containing canopy temperature and relative humidity.
30
+ * @param _context - Optional environmental conversion context.
31
+ * @returns Array containing the wet-bulb temperature and the discrete frost risk index (0 to 3).
32
+ */
33
+ convert(raw, _context) {
34
+ const T = raw.canopyTemperature;
35
+ const RH = raw.canopyHumidity;
36
+ // Stull formula for Wet Bulb Temperature (Tw in °C):
37
+ const Tw = T * Math.atan(0.151977 * Math.pow(RH + 8.313659, 0.5)) +
38
+ Math.atan(T + RH) -
39
+ Math.atan(RH - 1.676331) +
40
+ 0.00391838 * Math.pow(RH, 1.5) * Math.atan(0.023101 * RH) -
41
+ 4.686035;
42
+ // Frost Risk Level: 0 (No risk), 1 (Monitoring, Tw < 3°C), 2 (Warning, Tw < 1°C), 3 (Severe Frost, Tw < 0°C)
43
+ let frostRisk = 0;
44
+ if (Tw <= -0.5)
45
+ frostRisk = 3;
46
+ else if (Tw <= 1.0)
47
+ frostRisk = 2;
48
+ else if (Tw <= 3.0)
49
+ frostRisk = 1;
50
+ const twClamped = this.clampWithConfidence(Tw, -40, 50);
51
+ return [
52
+ {
53
+ metric: 'okf:agronomy/microclimate/wet_bulb_temperature',
54
+ value: twClamped.value,
55
+ unit: '°C',
56
+ qudtUri: 'qudt:unit/DEG_C',
57
+ confidence: twClamped.confidence,
58
+ },
59
+ {
60
+ metric: 'okf:agronomy/risk/frost_index',
61
+ value: frostRisk,
62
+ unit: 'level',
63
+ qudtUri: 'qudt:unit/UNITLESS',
64
+ confidence: 1.0,
65
+ metadata: {
66
+ frostState: frostRisk === 3 ? 'severe' : frostRisk === 2 ? 'warning' : frostRisk === 1 ? 'watch' : 'none',
67
+ },
68
+ },
69
+ ];
70
+ }
71
+ }
72
+ exports.GroundFrostRiskConverter = GroundFrostRiskConverter;
73
+ //# sourceMappingURL=GroundFrostRiskConverter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GroundFrostRiskConverter.js","sourceRoot":"","sources":["../src/GroundFrostRiskConverter.ts"],"names":[],"mappings":";;;AAAA,6CAAqG;AAYrG;;;;;;;;GAQG;AACH,MAAa,wBAAyB,SAAQ,4BAAqC;IAAnF;;QACG,0CAA0C;QACjC,iBAAY,GAAG,KAAK,CAAA;QAC7B,oCAAoC;QAC3B,cAAS,GAAG,6BAA6B,CAAA;QAClD,+BAA+B;QACtB,iBAAY,GAAG,OAAO,CAAA;QAC/B,uCAAuC;QAC9B,gBAAW,GAAG,iEAAiE,CAAA;IAiD3F,CAAC;IA/CE;;;;;;OAMG;IACH,OAAO,CAAC,GAAqB,EAAE,QAA4B;QACxD,MAAM,CAAC,GAAG,GAAG,CAAC,iBAAiB,CAAA;QAC/B,MAAM,EAAE,GAAG,GAAG,CAAC,cAAc,CAAA;QAE7B,qDAAqD;QACrD,MAAM,EAAE,GACL,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,QAAQ,EAAE,GAAG,CAAC,CAAC;YACtD,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,CAAC;YACjB,IAAI,CAAC,IAAI,CAAC,EAAE,GAAG,QAAQ,CAAC;YACxB,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,GAAG,EAAE,CAAC;YACzD,QAAQ,CAAA;QAEX,6GAA6G;QAC7G,IAAI,SAAS,GAAG,CAAC,CAAA;QACjB,IAAI,EAAE,IAAI,CAAC,GAAG;YAAE,SAAS,GAAG,CAAC,CAAA;aACxB,IAAI,EAAE,IAAI,GAAG;YAAE,SAAS,GAAG,CAAC,CAAA;aAC5B,IAAI,EAAE,IAAI,GAAG;YAAE,SAAS,GAAG,CAAC,CAAA;QAEjC,MAAM,SAAS,GAAG,IAAI,CAAC,mBAAmB,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAA;QAEvD,OAAO;YACJ;gBACG,MAAM,EAAE,gDAAgD;gBACxD,KAAK,EAAE,SAAS,CAAC,KAAK;gBACtB,IAAI,EAAE,IAAI;gBACV,OAAO,EAAE,iBAAiB;gBAC1B,UAAU,EAAE,SAAS,CAAC,UAAU;aAClC;YACD;gBACG,MAAM,EAAE,+BAA+B;gBACvC,KAAK,EAAE,SAAS;gBAChB,IAAI,EAAE,OAAO;gBACb,OAAO,EAAE,oBAAoB;gBAC7B,UAAU,EAAE,GAAG;gBACf,QAAQ,EAAE;oBACP,UAAU,EAAE,SAAS,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM;iBAC3G;aACH;SACH,CAAA;IACJ,CAAC;CACH;AAzDD,4DAyDC"}
@@ -0,0 +1,4 @@
1
+ export * from './CanopyMicroclimateConverter';
2
+ export * from './EvapotranspirationConverter';
3
+ export * from './GroundFrostRiskConverter';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,+BAA+B,CAAA;AAC7C,cAAc,+BAA+B,CAAA;AAC7C,cAAc,4BAA4B,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./CanopyMicroclimateConverter"), exports);
18
+ __exportStar(require("./EvapotranspirationConverter"), exports);
19
+ __exportStar(require("./GroundFrostRiskConverter"), exports);
20
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,gEAA6C;AAC7C,gEAA6C;AAC7C,6DAA0C"}
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@bradtech/sensor-air",
3
+ "version": "1.1.0",
4
+ "description": "In-plot crop canopy air microclimate, foliar VPD, evapotranspiration, and frost risk converters",
5
+ "author": "Olivier Lépine <olivier@lepine.fr>",
6
+ "license": "AGPL-3.0-or-later",
7
+ "copyright": "Copyright (C) 2026 Olivier Lépine",
8
+ "publishConfig": {
9
+ "access": "public",
10
+ "registry": "https://registry.npmjs.org/"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/bradtech-oss/stack.git",
15
+ "directory": "packages/sensor-air"
16
+ },
17
+ "keywords": [
18
+ "iot",
19
+ "microclimate",
20
+ "canopy-temperature",
21
+ "foliar-vpd",
22
+ "vpd",
23
+ "evapotranspiration",
24
+ "et0",
25
+ "frost-risk",
26
+ "agronomy",
27
+ "brad",
28
+ "agpl-v3"
29
+ ],
30
+ "main": "./dist/index.js",
31
+ "module": "./dist/index.js",
32
+ "types": "./dist/index.d.ts",
33
+ "exports": {
34
+ ".": {
35
+ "bun": "./src/index.ts",
36
+ "import": "./dist/index.js",
37
+ "require": "./dist/index.js",
38
+ "types": "./dist/index.d.ts",
39
+ "default": "./dist/index.js"
40
+ }
41
+ },
42
+ "files": [
43
+ "dist",
44
+ "src",
45
+ "LICENSE",
46
+ "README.md",
47
+ "HOWTO.md",
48
+ "!**/*.test.ts"
49
+ ],
50
+ "scripts": {
51
+ "clean": "rm -rf dist",
52
+ "build": "tsc",
53
+ "wbuild": "tsc --watch",
54
+ "test": "bun test"
55
+ },
56
+ "dependencies": {
57
+ "@bradtech/sensor": "workspace:*"
58
+ },
59
+ "devDependencies": {
60
+ "@tsconfig/recommended": "^1.0.1",
61
+ "@types/node": "^22.10.1",
62
+ "typescript": "^5.1.5"
63
+ }
64
+ }
@@ -0,0 +1,104 @@
1
+ import { BaseSensorConverter, type ConversionContext, type ConversionOutput } from '@bradtech/sensor'
2
+
3
+ /**
4
+ * Raw input reading for canopy-level ambient microclimate.
5
+ */
6
+ export interface CanopyAirReading {
7
+ /** In-canopy air temperature in °C */
8
+ temperature: number
9
+ /** In-canopy relative humidity in % (0 - 100) */
10
+ humidity: number
11
+ }
12
+
13
+ /**
14
+ * In-plot Canopy Microclimate & Foliar VPD Converter.
15
+ *
16
+ * Computes:
17
+ * - Direct canopy temperature (°C)
18
+ * - Direct canopy relative humidity (%)
19
+ * - Foliar Dew Point (°C) via Magnus-Tetens formula:
20
+ * $$\gamma = \frac{a \cdot T}{b + T} + \ln\left(\frac{RH}{100}\right), \quad T_{dew} = \frac{b \cdot \gamma}{a - \gamma}$$
21
+ * - Foliar Vapor Pressure Deficit ($VPD_{canopy}$ in kPa) via Tetens saturation vapor pressure:
22
+ * $$e_s(T) = 0.61078 \cdot \exp\left(\frac{17.27 \cdot T}{T + 237.3}\right), \quad VPD = e_s(T) \cdot \left(1 - \frac{RH}{100}\right)$$
23
+ */
24
+ export class CanopyMicroclimateConverter extends BaseSensorConverter<CanopyAirReading> {
25
+ /** Sensor domain family classification */
26
+ readonly sensorFamily = 'air'
27
+ /** Unique algorithmic model code */
28
+ readonly modelCode = 'canopy-microclimate-evaluator'
29
+ /** Algorithm semver version */
30
+ readonly modelVersion = '1.0.0'
31
+ /** Human-readable model description */
32
+ readonly description = 'In-plot canopy microclimate, foliar VPD, and dew point converter'
33
+
34
+ /**
35
+ * Converts raw canopy temperature and humidity readings into normalized microclimate and bioclimatic indicators.
36
+ *
37
+ * @param raw - Object containing canopy temperature and relative humidity.
38
+ * @param _context - Optional environmental conversion context.
39
+ * @returns Array containing temperature, humidity, dew point, and foliar VPD metrics.
40
+ */
41
+ convert(raw: CanopyAirReading, _context?: ConversionContext): ConversionOutput[] {
42
+ const outputs: ConversionOutput[] = []
43
+
44
+ // 1. Canopy Temperature
45
+ const tempClamped = this.clampWithConfidence(raw.temperature, -40, 65, -20, 50)
46
+ outputs.push({
47
+ metric: 'okf:agronomy/microclimate/canopy_temperature',
48
+ value: tempClamped.value,
49
+ unit: '°C',
50
+ qudtUri: 'qudt:unit/DEG_C',
51
+ confidence: tempClamped.confidence,
52
+ })
53
+
54
+ // 2. Canopy Relative Humidity
55
+ const humClamped = this.clampWithConfidence(raw.humidity, 0, 100)
56
+ outputs.push({
57
+ metric: 'okf:agronomy/microclimate/canopy_humidity',
58
+ value: humClamped.value,
59
+ unit: '%',
60
+ qudtUri: 'qudt:unit/PERCENT',
61
+ confidence: humClamped.confidence,
62
+ })
63
+
64
+ // 3. Foliar Dew Point (Magnus-Tetens)
65
+ const T = tempClamped.value
66
+ const RH = Math.max(0.1, humClamped.value)
67
+ const a = 17.27
68
+ const b = 237.7
69
+ const gamma = (a * T) / (b + T) + Math.log(RH / 100.0)
70
+ const dewPoint = (b * gamma) / (a - gamma)
71
+ const dewPointClamped = this.clampWithConfidence(dewPoint, -50, 50)
72
+
73
+ outputs.push({
74
+ metric: 'okf:agronomy/microclimate/dew_point',
75
+ value: dewPointClamped.value,
76
+ unit: '°C',
77
+ qudtUri: 'qudt:unit/DEG_C',
78
+ confidence: Math.min(tempClamped.confidence, humClamped.confidence),
79
+ metadata: { formula: 'Magnus-Tetens' },
80
+ })
81
+
82
+ // 4. Foliar Vapor Pressure Deficit (VPD in kPa)
83
+ const es = 0.61078 * Math.exp((17.27 * T) / (T + 237.3))
84
+ const ea = es * (RH / 100.0)
85
+ const vpd = Math.max(0, es - ea)
86
+ const vpdClamped = this.clampWithConfidence(vpd, 0, 10, 0, 5)
87
+
88
+ outputs.push({
89
+ metric: 'okf:agronomy/plant/foliar_vpd',
90
+ value: vpdClamped.value,
91
+ unit: 'kPa',
92
+ qudtUri: 'qudt:unit/KiloPA',
93
+
94
+ confidence: Math.min(tempClamped.confidence, humClamped.confidence),
95
+ metadata: {
96
+ saturationVaporPressureKpa: Number(es.toFixed(3)),
97
+ actualVaporPressureKpa: Number(ea.toFixed(3)),
98
+ transpirationStress: vpd > 2.0 ? 'high_water_stress' : vpd < 0.4 ? 'low_transpiration_fungal_risk' : 'optimal',
99
+ },
100
+ })
101
+
102
+ return outputs
103
+ }
104
+ }