@bradtech/sensor-soil 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,95 @@
1
+ import { BaseSensorConverter, type ConversionContext, type ConversionOutput } from '@bradtech/sensor'
2
+
3
+ /**
4
+ * Raw input parameters for soil volumetric water content evaluation.
5
+ */
6
+ export interface SoilMoistureInput {
7
+ /** Sensor depth probe location in centimeters (10, 20, 30 cm) */
8
+ depthCm: 10 | 20 | 30 | number
9
+ /** Raw dielectric permittivity or capacitive reading from the probe */
10
+ rawValue: number
11
+ /** Soil temperature at the same depth in °C for thermal drift compensation */
12
+ soilTemperature?: number
13
+ }
14
+
15
+ /**
16
+ * Multi-depth Soil Moisture & Volumetric Water Content (VWC %) Converter.
17
+ *
18
+ * Implements:
19
+ * - Texture-specific linear regression calibration curves (Sand, Loam, Clay, Silt, Peat):
20
+ * $$VWC = a \cdot \text{raw} + b$$
21
+ * - Custom plot-specific laboratory calibration model overrides ($y = \text{slope} \cdot x + \text{intercept}$).
22
+ * - Dielectric permittivity temperature compensation normalized to 20°C:
23
+ * $$VWC_{comp} = VWC - (T_{soil} - 20) \cdot 0.04$$
24
+ */
25
+ export class SoilMoistureConverter extends BaseSensorConverter<SoilMoistureInput> {
26
+ /** Sensor domain family classification */
27
+ readonly sensorFamily = 'soil'
28
+ /** Unique algorithmic model code */
29
+ readonly modelCode = 'soil-vwc-texture-calibrated'
30
+ /** Algorithm semver version */
31
+ readonly modelVersion = '1.0.0'
32
+ /** Human-readable model description */
33
+ readonly description = 'Texture-calibrated multi-depth soil volumetric water content converter'
34
+
35
+ /** Standard USDA soil texture regression coefficients [slope a, intercept b] */
36
+ private static TEXTURE_COEFFICIENTS: Record<string, { slope: number; intercept: number }> = {
37
+ sand: { slope: 0.92, intercept: -0.5 },
38
+ loam: { slope: 1.0, intercept: 0.0 },
39
+ clay: { slope: 1.15, intercept: 1.2 },
40
+ silt: { slope: 1.05, intercept: 0.5 },
41
+ peat: { slope: 1.25, intercept: 2.0 },
42
+ default: { slope: 1.0, intercept: 0.0 },
43
+ }
44
+
45
+ /**
46
+ * Converts raw capacitive probe readings into physical volumetric water content percentage (% VWC).
47
+ *
48
+ * @param raw - Raw sensor reading containing depth and uncalibrated value.
49
+ * @param context - Optional context providing soil texture classification or custom laboratory calibration models.
50
+ * @returns Array containing the calibrated soil moisture metric for the corresponding depth.
51
+ */
52
+ convert(raw: SoilMoistureInput, context?: ConversionContext): ConversionOutput[] {
53
+ const textureKey = context?.soilTexture || 'default'
54
+ const presetCoeff = SoilMoistureConverter.TEXTURE_COEFFICIENTS[textureKey] || SoilMoistureConverter.TEXTURE_COEFFICIENTS.default
55
+
56
+ // Check for plot-specific custom linear regression calibration model: y = slope * x + intercept
57
+ const customRegression = context?.soilLinearRegression || context?.calibration?.linearRegression
58
+ const slope = typeof customRegression?.slope === 'number' ? customRegression.slope : presetCoeff.slope
59
+ const intercept = typeof customRegression?.intercept === 'number' ? customRegression.intercept : presetCoeff.intercept
60
+ const calibrationType = customRegression ? 'custom_linear_regression' : 'texture_preset'
61
+
62
+ // Apply linear regression calibration: VWC (%) = a * raw + b
63
+ let vwc = raw.rawValue * slope + intercept
64
+
65
+ // Apply temperature compensation if soil temperature is known (standard reference at 20°C)
66
+ if (raw.soilTemperature !== undefined) {
67
+ const tempDelta = raw.soilTemperature - 20.0
68
+ // Dielectric permittivity of water decreases ~0.4%/°C
69
+ vwc = vwc - tempDelta * 0.04
70
+ }
71
+
72
+ // Clamp between 0% (oven dry) and 100% (free water saturation)
73
+ const clamped = this.clampWithConfidence(vwc, 0, 100, 0, 70)
74
+ const depth = raw.depthCm || 10
75
+
76
+ return [
77
+ {
78
+ metric: `okf:soil/moisture/${depth}cm`,
79
+ value: clamped.value,
80
+ unit: '%',
81
+ qudtUri: 'qudt:unit/PERCENT',
82
+ confidence: clamped.confidence,
83
+ metadata: {
84
+ depthCm: depth,
85
+ soilTexture: textureKey,
86
+ calibrationType,
87
+ calibrationSlope: slope,
88
+ calibrationIntercept: intercept,
89
+ customModelLabel: customRegression?.modelLabel,
90
+ rawInput: raw.rawValue,
91
+ },
92
+ },
93
+ ]
94
+ }
95
+ }
@@ -0,0 +1,49 @@
1
+ import { BaseSensorConverter, type ConversionContext, type ConversionOutput } from '@bradtech/sensor'
2
+
3
+ /**
4
+ * Raw input reading for depth-specific soil temperature.
5
+ */
6
+ export interface SoilTemperatureInput {
7
+ /** Sensor depth probe location in centimeters (e.g. 10, 20, 30 cm) */
8
+ depthCm: number
9
+ /** Soil temperature in °C */
10
+ temperature: number
11
+ }
12
+
13
+ /**
14
+ * Multi-depth Soil Temperature Profile Converter.
15
+ * Normalizes multi-depth soil thermistor measurements with agronomic plausibility clamping.
16
+ */
17
+ export class SoilTemperatureConverter extends BaseSensorConverter<SoilTemperatureInput> {
18
+ /** Sensor domain family classification */
19
+ readonly sensorFamily = 'soil'
20
+ /** Unique algorithmic model code */
21
+ readonly modelCode = 'soil-temperature-evaluator'
22
+ /** Algorithm semver version */
23
+ readonly modelVersion = '1.0.0'
24
+ /** Human-readable model description */
25
+ readonly description = 'Multi-depth soil profile temperature converter'
26
+
27
+ /**
28
+ * Converts raw soil temperature into depth-tagged physical metric records.
29
+ *
30
+ * @param raw - Object containing probe depth and measured temperature.
31
+ * @param _context - Optional environmental context.
32
+ * @returns Array containing the depth-tagged soil temperature metric.
33
+ */
34
+ convert(raw: SoilTemperatureInput, _context?: ConversionContext): ConversionOutput[] {
35
+ const depth = raw.depthCm || 10
36
+ const clamped = this.clampWithConfidence(raw.temperature, -20, 60, -10, 45)
37
+
38
+ return [
39
+ {
40
+ metric: `okf:soil/temperature/${depth}cm`,
41
+ value: clamped.value,
42
+ unit: '°C',
43
+ qudtUri: 'qudt:unit/DEG_C',
44
+ confidence: clamped.confidence,
45
+ metadata: { depthCm: depth },
46
+ },
47
+ ]
48
+ }
49
+ }
@@ -0,0 +1,96 @@
1
+ import { BaseSensorConverter, type ConversionContext, type ConversionOutput } from '@bradtech/sensor'
2
+
3
+ /**
4
+ * Input parameters for matric water potential and pF calculation.
5
+ */
6
+ export interface SoilWaterPotentialInput {
7
+ /** Sensor depth probe location in centimeters */
8
+ depthCm: number
9
+ /** Volumetric water content in % (0 - 100) */
10
+ vwcPercent: number
11
+ }
12
+
13
+ /**
14
+ * Soil Matric Water Potential ($\Psi_m$ in kPa) & pF Retention Converter.
15
+ *
16
+ * Implements the Mualem-van Genuchten hydraulic retention model to relate volumetric
17
+ * water content ($\theta$) to matric suction pressure head ($h$ in cm of water) and logarithmic pF scale:
18
+ *
19
+ * $$S_e = \frac{\theta - \theta_r}{\theta_s - \theta_r}, \quad m = 1 - \frac{1}{n}$$
20
+ * $$h = \frac{1}{\alpha} \left(S_e^{-1/m} - 1\right)^{1/n}, \quad \Psi_m = -h \cdot 0.0980665 \text{ kPa}$$
21
+ * $$pF = \log_{10}(h_{cm})$$
22
+ */
23
+ export class SoilWaterPotentialConverter extends BaseSensorConverter<SoilWaterPotentialInput> {
24
+ /** Sensor domain family classification */
25
+ readonly sensorFamily = 'soil'
26
+ /** Unique algorithmic model code */
27
+ readonly modelCode = 'soil-water-potential-van-genuchten'
28
+ /** Algorithm semver version */
29
+ readonly modelVersion = '1.0.0'
30
+ /** Human-readable model description */
31
+ readonly description = 'Van Genuchten soil water retention and pF water potential calculator'
32
+
33
+ /** Van Genuchten soil hydraulic retention parameters [$\alpha$, $n$, $\theta_r$, $\theta_s$] */
34
+ private static SOIL_VG_PARAMS: Record<string, { alpha: number; n: number; thetaR: number; thetaS: number }> = {
35
+ sand: { alpha: 0.145, n: 2.68, thetaR: 4.5, thetaS: 43.0 },
36
+ loam: { alpha: 0.036, n: 1.56, thetaR: 7.8, thetaS: 43.0 },
37
+ clay: { alpha: 0.008, n: 1.09, thetaR: 6.8, thetaS: 38.0 },
38
+ silt: { alpha: 0.016, n: 1.37, thetaR: 3.4, thetaS: 46.0 },
39
+ default: { alpha: 0.036, n: 1.56, thetaR: 7.8, thetaS: 43.0 },
40
+ }
41
+
42
+ /**
43
+ * Converts volumetric water content percentage into matric water potential (kPa) and pF availability scale.
44
+ *
45
+ * @param raw - Object containing probe depth and VWC percentage.
46
+ * @param context - Optional context specifying soil textural class.
47
+ * @returns Array containing both matric potential (kPa) and pF metrics.
48
+ */
49
+ convert(raw: SoilWaterPotentialInput, context?: ConversionContext): ConversionOutput[] {
50
+ const textureKey = context?.soilTexture || 'default'
51
+ const params = SoilWaterPotentialConverter.SOIL_VG_PARAMS[textureKey] || SoilWaterPotentialConverter.SOIL_VG_PARAMS.default
52
+
53
+ const theta = Math.min(params.thetaS - 0.1, Math.max(params.thetaR + 0.1, raw.vwcPercent))
54
+ const Se = (theta - params.thetaR) / (params.thetaS - params.thetaR) // Effective saturation
55
+ const m = 1.0 - 1.0 / params.n
56
+
57
+ // Matric suction head (h in cm of water column): h = (1/alpha) * (Se^(-1/m) - 1)^(1/n)
58
+ const hCm = (1.0 / params.alpha) * Math.pow(Math.pow(Se, -1.0 / m) - 1.0, 1.0 / params.n)
59
+
60
+ // Convert head cm to matric potential in kPa: 1 cm H2O ≈ 0.0980665 kPa
61
+ const potentialKpa = -(hCm * 0.0980665)
62
+
63
+ // pF = log10(h in cm)
64
+ const pF = Math.log10(Math.max(1.0, hCm))
65
+
66
+ const pFClamped = this.clampWithConfidence(pF, 0, 7.0)
67
+ const kpaClamped = this.clampWithConfidence(potentialKpa, -2000, 0)
68
+ const depth = raw.depthCm || 10
69
+
70
+ return [
71
+ {
72
+ metric: `okf:soil/potential/matric/${depth}cm`,
73
+ value: kpaClamped.value,
74
+ unit: 'kPa',
75
+ qudtUri: 'qudt:unit/KiloPA',
76
+ confidence: pFClamped.confidence,
77
+ metadata: {
78
+ depthCm: depth,
79
+ soilTexture: textureKey,
80
+ },
81
+ },
82
+ {
83
+ metric: `okf:soil/potential/pf/${depth}cm`,
84
+ value: pFClamped.value,
85
+ unit: 'pF',
86
+ qudtUri: 'qudt:unit/UNITLESS',
87
+ confidence: pFClamped.confidence,
88
+ metadata: {
89
+ depthCm: depth,
90
+ soilTexture: textureKey,
91
+ waterAvailabilityState: pF < 2.0 ? 'saturated' : pF < 2.5 ? 'field_capacity' : pF < 3.8 ? 'readily_available' : pF < 4.2 ? 'water_stress' : 'wilting_point',
92
+ },
93
+ },
94
+ ]
95
+ }
96
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from './SoilMoistureConverter'
2
+ export * from './SoilWaterPotentialConverter'
3
+ export * from './SoilTemperatureConverter'
4
+ export * from './SoilElectricalConductivityConverter'