""" Docs tab. """ from __future__ import annotations import pandas as pd import streamlit as st def render_tab_docs() -> None: st.header("Documentation") st.markdown( "Technical documentation, sensor inventory, project roadmap, " "scientific references, and glossary." ) with st.expander("Sensor Inventory & Data Usage", expanded=False): st.subheader("On-site sensor inventory") st.markdown(""" The Sde Boker research plot is equipped with a dense sensor network from [Seymour](https://www.seymouragri.com/). The dataset (`sensors_wide.csv`) contains **600+ columns** from the following device categories: """) sensor_inventory = pd.DataFrame([ {"Device": "Air1 (ref)", "Role": "Reference air station", "Location": "Reference plot", "Key measurements": "PAR, Tleaf, Tair, CO2, VPD, RH, GSR, wind, pressure, rain, spectral (10 incoming + 18 outgoing bands), NDVI, PRI, LWD/LWUP, VOC/NOX, dew point, UV, DLI, ToF distance", "Columns": "~80"}, {"Device": "Air2 (test)", "Role": "Test air station", "Location": "Test plot (under panels)", "Key measurements": "Same suite as Air1: PAR, Tleaf, Tair, CO2, VPD, spectral bands, NDVI, PRI, UV, ToF", "Columns": "~70"}, {"Device": "Air3 (all)", "Role": "Third air station", "Location": "Full-plot coverage", "Key measurements": "PAR, Tleaf, Tair, CO2, VPD, spectral bands, NDVI, PRI, UV, ToF", "Columns": "~70"}, {"Device": "Air4 (all)", "Role": "Fourth air station", "Location": "Full-plot coverage", "Key measurements": "PAR, Tleaf, Tair, CO2, VPD, spectral bands, NDVI, PRI, UV, rain, DLI, ToF", "Columns": "~80"}, {"Device": "Crop1", "Role": "Crop sensor", "Location": "Canopy-level", "Key measurements": "Leaf temp (IRT), NDVI, spectral outgoing (18 bands), ToF distance (16 zones), PRI, rNDVI, RENDVI, vegetation indices, UV", "Columns": "~60"}, {"Device": "Crop2", "Role": "Crop sensor (extended)", "Location": "Canopy-level", "Key measurements": "All of Crop1 + VPD, CO2, Tair, RH, GSR, PAR, wind, soil moisture/temp/EC/pH, mock leaf temp, leaf wetness, VOC/NOX", "Columns": "~80"}, {"Device": "Crop3\u20137", "Role": "Crop sensors", "Location": "Various canopy positions", "Key measurements": "GSR, PAR, leaf temp (IRT), spectral (incoming + outgoing), NDVI, PRI, vegetation indices, UV, ToF, DLI", "Columns": "~60 each"}, {"Device": "Soil1\u20134", "Role": "Soil probes (dual depth)", "Location": "Root zone", "Key measurements": "Soil temperature, moisture, dielectric, bulk EC, pore-water EC, pH \u2014 at two depths per probe", "Columns": "~20 each"}, {"Device": "Soil5\u20137, 9", "Role": "Additional soil probes", "Location": "Root zone", "Key measurements": "Soil temperature, moisture, dielectric, EC, pH (some dual depth)", "Columns": "~15 each"}, {"Device": "Irrigation1", "Role": "Irrigation monitor", "Location": "Drip line", "Key measurements": "Flow rate, volume, pressure, EC, pH, water temperature, irrigation duration, per-cycle aggregates", "Columns": "~23"}, {"Device": "Thermocouples 1\u20132", "Role": "Temperature arrays", "Location": "Canopy profile (multi-height)", "Key measurements": "4 thermocouple channels each (thermocoupleTemperature_1\u20134) + internal reference temperatures", "Columns": "~8 each"}, ]) st.dataframe(sensor_inventory, hide_index=True) st.subheader("What the current model uses") st.markdown("Stage 1 Farquhar model inputs (6 columns from Air1 ref) are listed " "in the **Photosynthesis & Data** tab (Farquhar Model section).") st.markdown("#### Stage 2 \u2014 ML prediction features (IMS station 43)") st.markdown(""" Stage 2 combines on-site sensor history with public IMS weather data to **forecast A ahead of time**, enabling the day-ahead tracker optimizer to plan shading interventions. """) ims_features = pd.DataFrame([ {"Column": "air_temperature_c", "IMS channel": "TD (ch 6)", "Units": "\u00b0C", "Notes": "Ambient air temperature"}, {"Column": "tdmax_c / tdmin_c", "IMS channel": "TDmax/TDmin (ch 8/9)", "Units": "\u00b0C", "Notes": "Daily extremes"}, {"Column": "ghi_w_m2", "IMS channel": "Grad (ch 10)", "Units": "W/m\u00b2", "Notes": "Global Horizontal Irradiance \u2014 proxy for PAR"}, {"Column": "rh_percent", "IMS channel": "RH (ch 7)", "Units": "%", "Notes": "Relative humidity \u2014 used to derive VPD proxy"}, {"Column": "rain_mm", "IMS channel": "Rain (ch 20)", "Units": "mm", "Notes": "Rainfall"}, {"Column": "wind_speed_ms", "IMS channel": "WS (ch 3)", "Units": "m/s", "Notes": "Wind speed"}, ]) st.dataframe(ims_features, hide_index=True) st.markdown(""" Time features are added during preprocessing: `hour_sin`, `hour_cos`, `doy_sin`, `doy_cos`, `month`, `day_of_year` \u2014 cyclical encodings for linear models and raw integers for tree-based models. """) with st.expander("Unused Sensors — Available for Future Improvements", expanded=False): st.subheader("Unused sensors \u2014 available for future improvements") st.markdown("#### High impact \u2014 would directly improve model accuracy") high_impact = pd.DataFrame([ {"Sensor": "Soil moisture", "Columns": "Soil1\u20139: soilMoisture (dual depth)", "Why it matters": "Root-zone water availability is THE primary driver of stomatal conductance in the Negev. Our CWSI is a proxy from (Tleaf \u2212 Tair); actual soil moisture would be far more accurate for computing stomatal conductance (gs) and intercellular CO\u2082 (ci).", "Impact": "High"}, {"Sensor": "PRI (Photochemical Reflectance Index)", "Columns": "Air1_PRI_ref, Crop1\u20137_PRI_all", "Why it matters": "Direct optical measurement of photosynthetic light-use efficiency. PRI tracks the xanthophyll cycle in real time \u2014 essentially a ground-truth signal for how efficiently the vine converts light to carbon. Could calibrate the quantum yield parameter (\u03b1) in Farquhar.", "Impact": "High"}, {"Sensor": "NDVI", "Columns": "Air1_NDVI_ref, Crop1\u20137_NDVI_all", "Why it matters": "Tracks canopy greenness and effective leaf area through the season. We use a fixed LAI = 2.5 \u2014 NDVI could make this dynamic (LAI increases May\u2192July, declines Aug\u2192Sep after veraison).", "Impact": "High"}, {"Sensor": "On-site wind", "Columns": "Air1_windSpeed_ref, Air1_windAngle_ref", "Why it matters": "Boundary-layer conductance affects the coupling between leaf temperature and air temperature. Wind drives transpiration cooling and modifies effective VPD at the leaf surface. Without it, our leaf energy balance is incomplete.", "Impact": "High"}, {"Sensor": "GSR (Global Solar Radiation)", "Columns": "Air1_GSR_ref", "Why it matters": "Cross-check with PAR. The ratio PAR/GSR indicates diffuse fraction (cloudy vs clear sky). We use a fixed diffuse_fraction = 0.15 in the shadow model \u2014 GSR could make this dynamic.", "Impact": "High"}, {"Sensor": "Thermocouple arrays", "Columns": "Thermocouples-1 and -2 (4 channels each)", "Why it matters": "Likely placed at different canopy heights. Could validate the 3-zone temperature model (we assume a single Tleaf with a \u22121.5\u00b0C shade offset for shaded zones).", "Impact": "High"}, ]) st.dataframe(high_impact, hide_index=True) st.markdown("#### Medium impact \u2014 improves accuracy or enables validation") medium_impact = pd.DataFrame([ {"Sensor": "LWD / LWUP (longwave radiation)", "Columns": "Air1_LWD_ref, Air1_LWUP_ref", "Why it matters": "Leaf energy balance: longwave down from sky + up from ground. Required for predicting Tleaf when moving from measured to modeled leaf temperature."}, {"Sensor": "airLeafDeltaT (pre-computed)", "Columns": "Air1_airLeafDeltaT_ref", "Why it matters": "Direct Tleaf \u2212 Tair measurement. We compute CWSI from exactly this \u2014 should verify our derived CWSI against this pre-computed value."}, {"Sensor": "Dew point / leaf dew delta", "Columns": "Air1_dewTemperature_ref, Air1_leafDewDeltaT_ref", "Why it matters": "Early morning dew affects PAR readings and blocks gas exchange. Condensation on leaves is not accounted for."}, {"Sensor": "Air2 (test) \u2014 full repeat", "Columns": "Air2_PAR_test, Air2_leafTemperature_test, ...", "Why it matters": "Appears to be a treatment comparison (ref = open?, test = under panels?). Could validate the shadow model against measured below-panel PAR."}, {"Sensor": "Irrigation data", "Columns": "Irrigation1_irrigationVolume_all, irrigationEC_all, ...", "Why it matters": "Irrigation timing and volume directly affect soil moisture \u2192 stomatal conductance \u2192 A. Without this, we cannot account for the irrigation signal."}, {"Sensor": "Crop spectral vegetation indices", "Columns": "Crop1\u20137: rNDVI, RENDVI, SIPI, GCI, GRVI, LCI, WI, mARI, PSRI, ...", "Why it matters": "Red-edge indices track chlorophyll content; water indices track leaf water status; senescence indices track aging. Each tells a different story about vine physiological state."}, {"Sensor": "ToF (Time of Flight) distance", "Columns": "Crop1\u20137_tofDist1a\u201316b, Air1\u20134_tofDist*", "Why it matters": "Up to 16 zones per device measuring distances \u2014 likely canopy depth profile, fruit sizing, or panel-vine clearance. Could track canopy growth through the season."}, ]) st.dataframe(medium_impact, hide_index=True) st.markdown("#### Lower impact \u2014 useful for v2.0 / edge cases") low_impact = pd.DataFrame([ {"Sensor": "VOC / NOX", "Columns": "Air1_srawVOC_ref, Air1_srawNOX_ref", "Why it matters": "Volatile organic compounds emitted by stressed plants. Potential stress indicator for the v2 intervention gate."}, {"Sensor": "Air pressure", "Columns": "Air1_airPressure_ref", "Why it matters": "At 475 m altitude (Sde Boker), atmospheric pressure is ~95.5 kPa vs 101.3 at sea level. Affects CO\u2082 partial pressure in Farquhar by ~5.7%."}, {"Sensor": "Rain (on-site)", "Columns": "Air1_rain_ref", "Why it matters": "Identifies periods where PAR sensor readings may be unreliable (wet sensor dome). Also marks soil wetting events."}, {"Sensor": "UV measurements", "Columns": "Air1_outgoingUVA/B/C_ref, Air1_DUVI_ref", "Why it matters": "Berry sunburn risk assessment for v2 fruiting-zone protection algorithm."}, {"Sensor": "Incoming spectral bands", "Columns": "Air1_incoming{Purple..NIR}_ref (10 bands)", "Why it matters": "Could decompose PAR into photosynthetically useful vs heat-load wavelengths. Red:far-red ratio affects canopy shade responses."}, {"Sensor": "DLI / PAR averages", "Columns": "Air1_DLI_ref, Air1_PARAvg1H_ref, Air1_PARAvg24H_ref", "Why it matters": "Pre-computed Daily Light Integral and running averages. Relevant for cumulative light stress assessment."}, {"Sensor": "Soil pH, EC, temperature", "Columns": "Soil1\u20139: soilpH, soilBulkEC, soilTemperature", "Why it matters": "Root-zone conditions affecting nutrient uptake and root respiration."}, ]) st.dataframe(low_impact, hide_index=True) with st.expander("Known Correctness Notes", expanded=False): st.subheader("Known correctness notes") st.markdown(""" 1. **Humidity loaded but unused:** `Air1_airHumidity_ref` is listed in Stage 1 columns but never consumed by `calc_photosynthesis()`. VPD already encodes humidity, so there is no model error \u2014 but the column is redundant. 2. **Fixed diffuse fraction (0.15):** The canopy shadow model assumes 15% diffuse light at all times. With both GSR and PAR sensors available, this could be computed dynamically: `diffuse \u2248 1 \u2212 PAR / (GSR \u00d7 0.45)`. 3. **Fixed LAI (2.5):** Leaf area index is constant through the season. Multiple NDVI sensors could track seasonal LAI changes. 4. **CWSI is a proxy:** We derive the Crop Water Stress Index from (Tleaf \u2212 Tair) when actual soil moisture sensors exist at multiple depths and locations. Direct soil moisture would give a more accurate stomatal conductance estimate. 5. **Altitude CO\u2082 correction missing:** At 475 m elevation, atmospheric pressure is ~5.7% lower than sea level. The `airPressure` sensor could correct the CO\u2082 partial pressure used in the Farquhar model. 6. **No ref/test comparison:** The dataset has explicit \"ref\" and \"test\" device roles. Understanding whether Air1 (ref) is the open-sky reference and Air2 (test) is under panels would allow direct validation of the shadow model against measured below-panel PAR. """) with st.expander("Project File Structure", expanded=False): st.subheader("Project file structure") st.code(""" config/settings.py Site/panel geometry, IMS channels, paths src/ farquhar_model.py Farquhar-Greer-Weedon mechanistic model sensor_data_loader.py Load and filter on-site sensor CSVs ims_client.py IMS REST API client with caching preprocessor.py Merge IMS + labels, time features, split predictor.py Train/evaluate ML regressors (LR, DT, RF, GBR, XGB) ts_predictor.py Multi-horizon time-series forecasting tracker_optimizer.py Tilt-angle simulation and stress heatmap solar_geometry.py Sun position, shadow projection, canopy PAR canopy_photosynthesis.py Zone-weighted canopy photosynthesis scripts/ download_ims_data.py Fetch IMS station data (CLI) run_pipeline.py Execute Stage 1 -> Stage 2 pipeline create_sample_data.py Generate trimmed sample for cloud deploy app.py This Streamlit dashboard Data/Seymour/ On-site sensor data (15-min intervals) Data/ims/ Cached IMS API responses Data/processed/ Intermediate files (labels, merged data) """, language="text") with st.expander("Roadmap — SolarWine 2.0", expanded=False): st.markdown(""" This baseline establishes the photosynthesis prediction floor. **SolarWine 2.0** evolves it into a full agrivoltaic tracker control system \u2014 the panels follow the sun by default and only deviate when the vine genuinely needs protection. ### The control principle **Full astronomical tracking is the default.** The panels generate maximum energy 100% of the time. Anti-tracking (tilting away to shade the vine) is a rare, targeted intervention used ONLY when the fruiting zone is at measurable risk of heat or radiation damage, and ONLY at the minimum dose necessary. ### Day-ahead forecast (Chronos-2) A pre-trained time-series foundation model ([Amazon Chronos-2](https://github.com/amazon-science/chronos-forecasting)) produces a **24-hour, 96-slot (15-min) photosynthesis forecast** each sunrise. The model ingests multivariate context \u2014 historical A\u2099 plus past IMS weather measurements (GHI, temperature, wind, humidity) and on-site sensors (PAR, T\u2091\u2090\u2099\u2092\u209a\u2099, VPD, CWSI). IMS *forecasts* for the prediction horizon are used as known-future covariates. This day-ahead baseline tells the system what photosynthesis to expect under full tracking, so it can judge whether an intervention is worth its energy cost. ### Budget-constrained dynamic programming The user sets a hard ceiling on annual energy sacrifice (e.g. 5%). A hierarchical budget planner distributes that ceiling across **Year \u2192 Month \u2192 Week \u2192 Day \u2192 15-min slot**, concentrating nearly all of it in July\u2013August midday windows when heat stress actually threatens grape quality. At sunrise each day, a **Discrete Dynamic Programming** optimizer computes the optimal tracker angle trajectory for all 96 slots: \u2003\u2003max \u2211 [ Price\u2091\u2099\u2091\u1d63\u1d4d\u1d67(t) \u00b7 E(t,\u03b8) + Price\u1d9c\u1d63\u1d52\u1d56(t) \u00b7 A(t,\u03b8) \u2212 Cost\u2098\u2092\u1d65\u1d49 \u00b7 |\u03b8\u209c \u2212 \u03b8\u209c\u208b\u2081| ] subject to: total energy sacrifice \u2264 daily budget. The crop price term scales with **phenological stage** (1.5\u00d7 during veraison for sugar loading, 0.5\u00d7 post-harvest) and **canopy zone** (top canopy \u00d70.6, fruiting zone \u00d70.3 \u2014 penalizing angles that shade the productive upper leaves). ### Real-time control loop (every 15 min) The day-ahead plan is a schedule, not a command. A real-time arbiter executes the planned trajectory but can override it: 1. **Compute \u03b8\u2090\u209b\u209c\u1d63\u2092** (astronomical tracking angle via pvlib) \u2014 this is the default 2. **Check hard gates** \u2014 no shading before 10:00, never in May, not below 28\u00b0C / CWSI < 0.3 3. **Check eligibility** \u2014 all must pass: T\u2097\u2091\u2090\u1da0 > 30\u00b0C (Rubisco-limited), CWSI > 0.4, GHI > 500 W/m\u00b2, FvCB confirms shading helps 4. **Minimum effective dose** \u2014 scan offsets [3\u00b0, 5\u00b0, 8\u00b0, 10\u00b0, 15\u00b0, 20\u00b0] smallest-first; select the first that brings fruiting zone PAR < 400 \u00b5mol while keeping top canopy > 70% sunlit 5. **Budget accounting** \u2014 record sacrifice, roll unused budget forward A typical July day: full tracking 05:00\u201311:00, minimum-dose shading (\u223c3\u20138\u00b0) 11:00\u201315:30, back to full tracking by 16:00. Season total: \u223c92% full tracking, \u223c8% minimum-dose shading, energy sacrifice guaranteed \u2264 5%. ### Intelligent model routing (Gemini) An LLM-based routing layer selects the best photosynthesis model for each cycle: the mechanistic FvCB model (accurate under standard conditions) or the ML ensemble (handles non-linear stress better). This replaces the hard-coded 12% divergence threshold with context-aware switching. ### What's next | Phase | Status | Focus | |---|---|---| | **Chronos-2 forecasting** | ✅ Done | Zero-shot MAE ~3.9 µmol m⁻² s⁻¹ (R² ~0.52); IMS + on-site sensor covariates, walk-forward eval | | **Gemini routing agent** | ✅ Done | Context-aware FvCB / ML selection from real-time telemetry | | **Gemini data engineering** | 🔜 Next | LLM-assisted sensor anomaly detection, feature generation | | **Control algorithm** | 🔜 Next | Intervention gate, minimum-dose engine, energy budget, command arbiter | | **Day-ahead DP planner** | 🔜 Next | Trajectory optimization with movement cost + budget constraint | | **Dashboard v2** | 🔜 Next | Budget planner, control replay, fruiting zone monitor, ROI tabs | """) with st.expander("Glossary", expanded=False): st.markdown( "- **A (Net photosynthesis rate):** The net rate of CO₂ fixation by the leaf, " "measured in µmol CO₂ m⁻² s⁻¹. Calculated as min(Ac, Aj) − Rd.\n" "- **PAR (Photosynthetically Active Radiation):** Light in the 400–700 nm range " "that drives photosynthesis, in µmol photons m⁻² s⁻¹.\n" "- **VPD (Vapor Pressure Deficit):** Difference between saturated and actual water " "vapor pressure (kPa). High VPD causes stomatal closure, reducing photosynthesis.\n" "- **CWSI (Crop Water Stress Index):** Index from 0 (no stress) to 1 (max stress) " "based on leaf–air temperature difference. Modulates stomatal conductance.\n" "- **Vcmax:** Maximum rate of Rubisco carboxylation (µmol m⁻² s⁻¹).\n" "- **Jmax:** Maximum rate of electron transport (µmol m⁻² s⁻¹).\n" "- **GHI (Global Horizontal Irradiance):** Total solar radiation on a horizontal surface (W/m²). " "Used as a proxy for PAR from IMS data.\n" "- **IMS:** Israel Meteorological Service.\n" "- **Temporal split:** Train/test split preserving time order (first 75%% train, last 25%% test) " "to prevent data leakage from future observations." ) with st.expander("References", expanded=False): st.markdown( "- [Farquhar, von Caemmerer & Berry (1980)](https://doi.org/10.1007/BF00386231) — " "A biochemical model of photosynthetic CO₂ assimilation in leaves of C₃ species. " "*Planta*, 149, 78–90.\n" "- [Greer & Weedon (2012)](https://doi.org/10.1111/j.1365-3040.2011.02471.x) — " "Modelling photosynthetic responses to temperature of grapevine. " "*Plant, Cell & Environment*, 35, 1050–1064.\n" "- [Bernacchi et al. (2001)](https://doi.org/10.1046/j.1365-3040.2001.00668.x) — " "Improved temperature response functions for models of Rubisco-limited photosynthesis. " "*Plant, Cell & Environment*, 24, 253–259.\n" "- [IMS API documentation](https://ims.gov.il/en/ObservationDataAPI) — " "Israel Meteorological Service environmental data API." ) st.markdown("---") st.markdown( "Learn more at [solarwine.ai](https://www.solarwine.ai/)" )