@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,77 @@
1
+ import { BaseSensorConverter, type ConversionContext, type ConversionOutput } from '@bradtech/sensor'
2
+
3
+ /**
4
+ * Daily temperature and radiation inputs required for reference evapotranspiration computation.
5
+ */
6
+ export interface EvapotranspirationInput {
7
+ /** Daily minimum canopy temperature in °C ($T_{min}$) */
8
+ tempMin: number
9
+ /** Daily maximum canopy temperature in °C ($T_{max}$) */
10
+ tempMax: number
11
+ /** Daily mean canopy temperature in °C ($T_{mean}$) */
12
+ tempMean: number
13
+ /** Global extraterrestrial or measured solar radiation flux in $\text{MJ}/(\text{m}^2\cdot\text{day})$ ($R_a$) */
14
+ solarRadiationMj?: number
15
+ /** Geographic plot latitude in decimal degrees (e.g. 44.8378 for Bordeaux) */
16
+ latitudeDegrees?: number
17
+ /** Julian day of year ($1 - 366$) */
18
+ dayOfYear?: number
19
+ }
20
+
21
+ /**
22
+ * Agronomic Daily Reference Evapotranspiration ($ET_0$) Converter.
23
+ *
24
+ * Implements the internationally recognized FAO-56 Hargreaves-Samani temperature-radiation formulation:
25
+ * $$ET_0 = 0.0023 \cdot (T_{mean} + 17.8) \cdot \sqrt{T_{max} - T_{min}} \cdot R_a \cdot 0.408$$
26
+ *
27
+ * where $0.408$ represents the inverse of the latent heat of vaporization ($\lambda^{-1}$ in $\text{mm}/(\text{MJ}\cdot\text{m}^{-2})$).
28
+ */
29
+ export class EvapotranspirationConverter extends BaseSensorConverter<EvapotranspirationInput> {
30
+ /** Sensor domain family classification */
31
+ readonly sensorFamily = 'air'
32
+ /** Unique algorithmic model code */
33
+ readonly modelCode = 'evapotranspiration-hargreaves-fao56'
34
+ /** Algorithm semver version */
35
+ readonly modelVersion = '1.0.0'
36
+ /** Human-readable model description */
37
+ readonly description = 'FAO-56 Hargreaves-Samani daily reference evapotranspiration (ET0) calculator'
38
+
39
+ /**
40
+ * Converts daily temperature extremes and radiation into daily reference crop evapotranspiration ($ET_0$ in mm/day).
41
+ *
42
+ * @param raw - Object containing $T_{min}, T_{max}, T_{mean}$, and optional radiation $R_a$.
43
+ * @param _context - Optional environmental context.
44
+ * @returns Single-element array containing the $ET_0$ metric in mm/day.
45
+ */
46
+ convert(raw: EvapotranspirationInput, _context?: ConversionContext): ConversionOutput[] {
47
+ const deltaT = Math.max(0, raw.tempMax - raw.tempMin)
48
+ const tMean = raw.tempMean
49
+
50
+ // Extraterrestrial radiation estimate (Ra) if not provided directly
51
+ let Ra = raw.solarRadiationMj
52
+ if (!Ra || Ra <= 0) {
53
+ // Default approximation for temperate European agricultural latitudes (~44°N)
54
+ Ra = 25.0
55
+ }
56
+
57
+ // Hargreaves-Samani equation: ET0 = 0.0023 * (Tmean + 17.8) * sqrt(Tmax - Tmin) * Ra * 0.408
58
+ const et0 = 0.0023 * (tMean + 17.8) * Math.sqrt(deltaT) * Ra * 0.408
59
+
60
+ const clamped = this.clampWithConfidence(et0, 0, 15, 0, 10)
61
+
62
+ return [
63
+ {
64
+ metric: 'okf:agronomy/evapotranspiration/et0',
65
+ value: clamped.value,
66
+ unit: 'mm/day',
67
+ qudtUri: 'qudt:unit/MilliM',
68
+ confidence: clamped.confidence,
69
+ metadata: {
70
+ method: 'Hargreaves-Samani-FAO56',
71
+ tempRange: Number(deltaT.toFixed(2)),
72
+ radiationMj: Ra,
73
+ },
74
+ },
75
+ ]
76
+ }
77
+ }
@@ -0,0 +1,79 @@
1
+ import { BaseSensorConverter, type ConversionContext, type ConversionOutput } from '@bradtech/sensor'
2
+
3
+ /**
4
+ * Microclimate temperature and relative humidity inputs for frost risk evaluation.
5
+ */
6
+ export interface GroundFrostInput {
7
+ /** Air temperature measured at plant/canopy height in °C ($T$) */
8
+ canopyTemperature: number
9
+ /** Relative humidity measured at plant/canopy height in % ($RH$) */
10
+ canopyHumidity: number
11
+ }
12
+
13
+ /**
14
+ * Radiative Ground Frost Risk & Wet-Bulb Temperature Converter.
15
+ *
16
+ * Implements the Roland Stull empirical formulation to compute the Psychrometric Wet-Bulb
17
+ * Temperature ($T_w$ in °C), which is the exact physical temperature reached by plant leaves
18
+ * and vegetative buds under radiative night cooling:
19
+ *
20
+ * $$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$$
21
+ */
22
+ export class GroundFrostRiskConverter extends BaseSensorConverter<GroundFrostInput> {
23
+ /** Sensor domain family classification */
24
+ readonly sensorFamily = 'air'
25
+ /** Unique algorithmic model code */
26
+ readonly modelCode = 'ground-frost-risk-evaluator'
27
+ /** Algorithm semver version */
28
+ readonly modelVersion = '1.0.0'
29
+ /** Human-readable model description */
30
+ readonly description = 'Radiative ground frost risk and wet-bulb temperature calculator'
31
+
32
+ /**
33
+ * Converts canopy microclimate readings into wet-bulb temperature ($T_w$) and discrete agronomic frost severity index.
34
+ *
35
+ * @param raw - Object containing canopy temperature and relative humidity.
36
+ * @param _context - Optional environmental conversion context.
37
+ * @returns Array containing the wet-bulb temperature and the discrete frost risk index (0 to 3).
38
+ */
39
+ convert(raw: GroundFrostInput, _context?: ConversionContext): ConversionOutput[] {
40
+ const T = raw.canopyTemperature
41
+ const RH = raw.canopyHumidity
42
+
43
+ // Stull formula for Wet Bulb Temperature (Tw in °C):
44
+ const Tw =
45
+ T * Math.atan(0.151977 * Math.pow(RH + 8.313659, 0.5)) +
46
+ Math.atan(T + RH) -
47
+ Math.atan(RH - 1.676331) +
48
+ 0.00391838 * Math.pow(RH, 1.5) * Math.atan(0.023101 * RH) -
49
+ 4.686035
50
+
51
+ // Frost Risk Level: 0 (No risk), 1 (Monitoring, Tw < 3°C), 2 (Warning, Tw < 1°C), 3 (Severe Frost, Tw < 0°C)
52
+ let frostRisk = 0
53
+ if (Tw <= -0.5) frostRisk = 3
54
+ else if (Tw <= 1.0) frostRisk = 2
55
+ else if (Tw <= 3.0) frostRisk = 1
56
+
57
+ const twClamped = this.clampWithConfidence(Tw, -40, 50)
58
+
59
+ return [
60
+ {
61
+ metric: 'okf:agronomy/microclimate/wet_bulb_temperature',
62
+ value: twClamped.value,
63
+ unit: '°C',
64
+ qudtUri: 'qudt:unit/DEG_C',
65
+ confidence: twClamped.confidence,
66
+ },
67
+ {
68
+ metric: 'okf:agronomy/risk/frost_index',
69
+ value: frostRisk,
70
+ unit: 'level',
71
+ qudtUri: 'qudt:unit/UNITLESS',
72
+ confidence: 1.0,
73
+ metadata: {
74
+ frostState: frostRisk === 3 ? 'severe' : frostRisk === 2 ? 'warning' : frostRisk === 1 ? 'watch' : 'none',
75
+ },
76
+ },
77
+ ]
78
+ }
79
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './CanopyMicroclimateConverter'
2
+ export * from './EvapotranspirationConverter'
3
+ export * from './GroundFrostRiskConverter'