fieldmesh

modules/twin/ — a physics twin for the hoop house (layer 3, Q28)#

GreenLight (Katzin et al., Wageningen University & Research; BSD-3-Clause-Clear) models a greenhouse's climate from first principles: the cover's shortwave and long-wave exchanges, the air and the space above it, ventilation by wind and buoyancy, the soil, a crop's transpiration and photosynthesis. It was built for a heated, lit Dutch glasshouse. twin.py configures it as what the farms here have: a 30 × 96 ft single-poly high tunnel with roll-up sides and no heating, lamps, screen or CO2. It then drives the model with the farm's real weather.

Q28's first step is a check on our own physics. The virtual farm's hoop house (sim/physics.Tunnel) is a few plausible numbers. Run on the same real weather (McAllister Farm, NASA POWER hourly, March–April and two weeks of July 2025), tunnel_compare.py sets the two side by side.

What it found#

spring 2025, 60 nights, 6 frosts outside mean night gain worst night frosts inside inside ≤ −2.2 °C daytime excess hottest
the virtual farm's tunnel, as first guessed +3.1 °C +1.6 2 1 +9.5 32.6 °C
the virtual farm's tunnel, fitted to GreenLight (IR film) +1.2 °C +1.0 3 1 +7.5 31.1 °C
the same, knowing the sky from its long-wave (IR film) +1.4 °C +1.1 3 1 +7.5 31.1 °C
the same, knowing the sky (clear film) +0.2 °C −0.4 6 1 +6.5 30.6 °C
GreenLight, thermic (IR) film +1.4 °C −0.4 1 0 +7.3 29.3 °C
GreenLight, clear film +0.3 °C −1.1 5 1 +6.7 29.2 °C

Neither GreenLight's tunnel nor the virtual farm's has met a real one, so this says what to measure first, not who is right: a tunnel's inside and outside air on clear spring nights, with its film type written down. modules/advise/tunnel_gain.py already turns paired sensors into a measured gain.

Step 2: a frost watch for covered crops#

frostwatch.py runs tonight through the twin. It warms up on the last 36 hours, starting from soil temperatures taken from the weeks before (not GreenLight's 16.5 °C glasshouse floor). It then runs dusk to dawn on the forecast's air temperature, humidity, wind and cloud. The cloud becomes the long-wave sky the tunnel radiates to (Brutsaert's clear-sky emissivity, raised toward a black body by cloud), and the morning sun is a clear sky dimmed by cloud (Kasten and Czeplak). The inside low is judged by a CEL rule through modules/rules, in English and Spanish.

With probes in the tunnel's soil, its soil starts from them (frostwatch.tunnel_soil, given the tunnel's id): each GreenLight soil layer a probe sits in starts at the probe's newest reading of the last six hours, a layer between two probes between them, and the rest at the air's estimate. It matters less than it sounds. On a clear January night started from a week of cool days, soil 3 °C colder or warmer through the top 28 cm moved the inside low by 0.18 °C either way (−5.46, −5.28, −5.10 °C): the 36-hour warm-up lets the top soil follow the air whatever it started at. The decision records how many probes it used (soil_probes).

Live, everything comes from the forecast log the farm's cron keeps (tools/forecast_collect.py, which now keeps humidity, wind and cloud as well), each hour its latest issue (Q26). The warm-up hours are forecasts made a few hours ahead, near enough to what happened. It runs by hand on a log, or as a module process through the host (§8.3, module.json):

twin-env/bin/python -I modules/twin/frostwatch.py --log forecasts.jsonl --lat 35.06 --lon -86.59 --cover thermic

On a farm it runs each evening at 17:30 as its own module process, from a systemd timer (deploy/systemd/), with its place, film and tunnel in a config file. Its decision names the tunnel and stands until dawn, so the notifier can send it and the board can show it. Run so on McAllister Farm's forecast log on 2026-10-11, it said 14.4 °C inside a thermic-film tunnel against 12.8 °C outside: no warning.

The 2025–26 season at McAllister Farm (covered_backtest.py): 194 nights, mid-October to the end of April. Each night uses NOAA's National Blend of Models issued at 6 pm, after a warm-up on what the weather did (NASA POWER). The same night is run on what the weather did as the twin's own answer; the farm's SCAN station is the truth outside. A damaging frost is −2.2 °C or below, as in the open-bed watch's backtest.

damaging frosts caught missed false alarms open-bed warnings spared
open-bed watch, outside 38 38 0 7 —
covered watch, IR film, warn at 0 °C 28 inside 28 0 8 9 of 45
covered watch, clear film, warn at 0 °C 36 inside 35 1 6 4 of 45
covered watch, clear film, warn at +0.5 °C 36 inside 36 0 7 3 of 45

Neither "truth" inside is a thermometer in a tunnel. The twin run on the weather that came is the best available while no real tunnel is logged.

Step 3: the twin against the tunnel's own sensor#

A tunnel has one air sensor more often than two, and one sensor cannot see its own fault. twin_check.py sets the twin beside it, and says, as an advisory decision, when the two part company and stay apart:

hoop house's air sensor and the twin have disagreed by -8.4 °C for 3 hours. Check the sensor and its sun shield, and whether the sides or a door are open.

That is what it said two hours after the sensor was knocked outside on the morning of April 16 (the backtest below).

It never marks a reading bad. A module does not write quality verdicts (§8.3), and the twin may be the one that is wrong.

What counts as apart is learned. Two models of one tunnel never agree. On the spring of 2025, GreenLight and the virtual farm's tunnel, lit by the measured sunshine, differ by about 1 °C at night and up to 2 °C at midday. So the check first learns the usual difference and its spread for each four hours of the solar day over two weeks of good data, after the sensor is installed or serviced. After that, an hour departs when its difference lies further from the usual one than the larger of 2 °C and three spreads. The alert stands once three such hours run in a row.

Backtested on the spring of 2025, with GreenLight's saved run as the twin and the virtual farm's tunnel as the sensor, with faults put in:

fault said after
none, seven weeks; five noise seeds never: no false alert
the sensor reads 4 °C high (miscalibrated, replaced) 12 h; across five placings, 8 to 40 h
its sun shield is gone, reading up to 8 °C high in full sun 8 h
the sides are left up for two days 15 h, at the first sunny midday
it reads the outside air (knocked down, moved) 2 h by day; at night a tunnel is barely warmer than outside

What it cannot see: a sensor 1.5 °C off (no case in five), 2 °C off (one in five) or 3 °C off (four in five). The twin is a second opinion for gross faults; a sensor slowly going wrong is a second sensor's to catch. And the "sensor" here is a second model, not a thermometer in a tunnel: what a real one's residuals look like is untested.

What it took to run GreenLight as a tunnel#

Every unheated configuration failed at the first sunrise, in the solver, whatever its film or vents. The cause was in the crop model. The CO2 compensation point (Vanthoor 2011, ch. 9) scales the leaf's by j25LeafMax / j25CanMax, which is 1/LAI. That holds for a canopy of one leaf layer or more. A tunnel's young transplants (LAI 0.13 here) make the ratio near 8 and the compensation point negative, about −150 ppm. Gross photosynthesis divides by (Cs + 2Γ), so the model has a pole at a leaf CO2 of about 300 ppm, and a house that is not enriched passes through 300 ppm every sunny morning. An enriched glasshouse never comes near it, which is presumably why it was never hit. Capping the ratio at 1 (one formula override, FORMULAS in twin.py) runs every configuration; without it, every one fails. The test test_it_runs_a_tunnel_through_sunrise_only_with_the_compensation_point_fixed keeps both halves of that true, and will say when an upstream fix makes the override unnecessary. Reported upstream as davkat1/GreenLight#23.

Running it#

GreenLight pins numpy 1.26 and brings scipy, pandas and matplotlib, so it gets its own environment, and as a live twin it will be its own module process (§8.3):

python3 -m venv twin-env && twin-env/bin/pip install -r modules/twin/requirements.txt
twin-env/bin/python -I modules/twin/tunnel_compare.py --run    # about 3½ minutes; rewrites data/
python3 modules/twin/tunnel_compare.py                          # anywhere: from the saved runs

Sixty-one days of the tunnel take about 80 seconds here.

file what it is
data/mcallister-2025-spring-hourly.csv, july the weather: NASA POWER hourly, CC BY 4.0, irradiance converted from MJ/h
data/greenlight-tunnel-*.csv GreenLight's hourly inside air, per film and period
data/mcallister-2025-26-frost-season-hourly.csv the 2025–26 frost season's weather, NASA POWER hourly
data/frost-backtest-2025-26-*.csv the covered-crop watch per night, per film: NBM's outside low, the station's, and the twin's inside low on the forecast and on the weather

Code AGPL-3.0-or-later (LICENSE), like the other modules; GreenLight is BSD-3-Clause-Clear and is installed, not copied.

Next (Q28): the twin run live on a farm's weather beside a real tunnel's sensor, as a module process; the twin's warm bias on forecasts, which a season of a real tunnel's lows would let the watch correct rather than pad; step 4 waits on §6.