api / ui /tab_overview.py
safraeli's picture
Deploy: 2026 sensor migration + redesign + bucket B endpoints
13fc29d verified
Raw History Blame Contribute Delete
24 kB
"""
Overview tab: presentation and key takeaways for the SolarWine app.
"""
from __future__ import annotations
import pandas as pd
import streamlit as st
from config import settings
from ui.bootstrap import (
check_ai_health,
ensure_ims_refresh_background,
load_labels,
load_metrics,
)
_ASSETS = settings.PROJECT_ROOT / "assets"
def render_tab_overview() -> None:
_ai_status = check_ai_health()
_ims_status = ensure_ims_refresh_background()
status_cols = st.columns(2)
with status_cols[0]:
if _ai_status.get("ok"):
st.caption("AI service status: healthy (Gemini reachable).")
else:
st.warning(
"AI service status: unavailable. "
"Gemini calls may fall back to biology-only answers or cached data. "
"Check your GOOGLE_API_KEY or network access."
)
if _ai_status.get("error"):
st.caption(f"AI health check error: `{_ai_status['error']}`")
with status_cols[1]:
if _ims_status.get("running"):
st.caption("IMS cache status: refreshing recent weather data in the background…")
elif _ims_status.get("ok"):
st.caption("IMS cache status: refreshed with recent weather data.")
else:
st.warning(
"IMS cache status: not up to date. "
"Latest weather in the app may be stale until IMS is fixed."
)
if _ims_status.get("error"):
st.caption(f"IMS refresh error: `{_ims_status['error']}`")
st.markdown("## 1. SolarWine — Smart Shading for Agrivoltaic Vineyards")
st.markdown(
"**Site:** Sde Boker research station, Negev desert, Israel \n"
"**Crop:** *Vitis vinifera* cv. Semillon grapevine \n"
"**Challenge:** Solar panels and grapevines compete for the same sunlight. "
"How do we split it so both thrive?"
)
_vineyard_img = _ASSETS / "vineyard_panels.png"
_closeup_img = _ASSETS / "vineyard_closeup.png"
if _vineyard_img.exists() or _closeup_img.exists():
_p_img_l, _p_img_r = st.columns(2)
if _vineyard_img.exists():
with _p_img_l:
st.image(str(_vineyard_img), width='stretch',
caption="Solar trackers above grapevine rows")
if _closeup_img.exists():
with _p_img_r:
st.image(str(_closeup_img), width='stretch',
caption="Grapevines under tilted solar panels")
st.markdown(
"**Why photosynthesis?** Our working hypothesis is simple: "
"a vine that photosynthesizes well produces good wine. Photosynthesis "
"drives sugar accumulation, flavor-compound synthesis, and canopy health "
"— so if we can keep A (net carbon assimilation) high throughout the "
"growing season, grape quality follows. Everything in this system is "
"built around measuring, predicting, and protecting photosynthesis."
)
st.markdown(
"The Negev provides **far more radiation** than a grapevine can use — "
"midday PAR routinely exceeds 2000 µmol/m²/s, but photosynthesis "
"saturates well below that. The surplus can be harvested as electricity "
"without hurting the vine."
)
st.markdown(
"But first we need to *measure* how much photosynthesis the vine actually "
"performs — that's what the Farquhar model does."
)
with st.expander("What is agrivoltaics? What is PAR?"):
st.markdown(
"**Agrivoltaics** is the dual use of land for both agriculture and solar "
"energy — panels are mounted above the crops so both share the same field.\n\n"
"**PAR (Photosynthetically Active Radiation)** is the slice of sunlight "
"(wavelengths 400–700 nm) that plants actually use for photosynthesis, "
"measured in µmol photons/m²/s.\n\n"
"**Light saturation** means that beyond a certain PAR threshold, more "
"light does *not* increase photosynthesis — the vine's internal "
"biochemistry becomes the bottleneck, not the light supply.\n\n"
"**Why Sde Boker?** The Negev desert delivers extreme radiation, well "
"above the vine's saturation point, making it an ideal site to demonstrate "
"that surplus light can be redirected to electricity without harming the crop."
)
st.divider()
st.markdown("## 2. Two-Stage Pipeline")
st.markdown(
"We built a two-stage system to model and then predict vine photosynthesis:"
)
_p_s1, _p_arrow1, _p_s2, _p_arrow2, _p_s3 = st.columns([3, 1, 3, 1, 3])
with _p_s1:
st.markdown(
"### Stage 1: Measure\n"
"**6 on-site sensors** (PAR, leaf temp, air temp, CO₂, VPD, humidity) "
"→ Farquhar FvCB mechanistic model → **photosynthesis rate A** "
"(µmol CO₂ m⁻² s⁻¹)"
)
with _p_arrow1:
st.markdown("<div style='text-align:center; font-size:2rem; padding-top:2rem'>→</div>",
unsafe_allow_html=True)
with _p_s2:
st.markdown(
"### Stage 2: Predict\n"
"**On-site sensors + IMS weather** (station 43 — temperature, GHI, "
"humidity, wind, rain) → ML regressors (RF, GBR, XGBoost) → "
"**forecast A ahead of time**"
)
with _p_arrow2:
st.markdown("<div style='text-align:center; font-size:2rem; padding-top:2rem'>→</div>",
unsafe_allow_html=True)
with _p_s3:
st.markdown(
"### Goal\n"
"Predict tomorrow's photosynthesis so the **panel-tilt optimizer** "
"can plan tracker angles a day ahead — shading only when the vine "
"actually needs protection."
)
st.info(
"**CO₂ correction:** On-site CO₂ sensor reads ~600 ppm (known warm bias). "
"A ×0.7 correction factor brings values to ~420 ppm, consistent with "
"ambient atmospheric levels."
)
with st.expander("Key terms: Farquhar model, ML regressors, IMS"):
st.markdown(
"**Farquhar FvCB model** (1980) is the standard physics-based model that "
"predicts leaf photosynthesis from light, temperature, and CO₂ — think of "
"it as a formula that says *'given this much light and heat, the leaf fixes "
"this much carbon.'*\n\n"
"**ML regressors** are machine-learning algorithms that learn patterns "
"from data:\n"
"- **RF (Random Forest):** many decision trees vote together\n"
"- **GBR (Gradient Boosted Regression):** trees are added one at a time, "
"each correcting the previous errors\n"
"- **XGBoost:** an optimised variant of gradient boosting\n\n"
"**IMS** — Israel Meteorological Service, providing free public weather "
"station data.\n\n"
"**VPD (Vapor Pressure Deficit)** measures how dry the air is. High VPD "
"makes the vine close its stomata (leaf pores) to conserve water, which "
"reduces CO₂ intake and slows photosynthesis.\n\n"
"**Why two stages?** Stage 1 computes A from current sensor readings "
"(what is happening *now*); Stage 2 learns to *forecast* A ahead of time "
"so the tracker optimizer can plan tomorrow's panel angles today."
)
st.divider()
st.markdown("## 3. Stage 1 — Farquhar Mechanistic Model")
st.markdown(
"The Farquhar et al. (1980) model with Greer & Weedon (2012) grapevine "
"parameters computes **A = min(Ac, Aj) − Rd** at each 15-min interval "
"during the growing season (May–Sep, daytime PAR > 50)."
)
_p_labels_path = settings.PROCESSED_DIR / "stage1_labels.csv"
_p_val_img = settings.OUTPUTS_DIR / "stage1_validation.png"
if _p_labels_path.exists():
_p_labels = load_labels(str(_p_labels_path))
_p_labels.index = pd.to_datetime(_p_labels.index, utc=True)
_pc1, _pc2, _pc3, _pc4 = st.columns(4)
_pc1.metric("Observations", f"{len(_p_labels):,}")
_pc2.metric("Mean A", f"{_p_labels.iloc[:, 0].mean():.1f} µmol m⁻² s⁻¹")
_pc3.metric("Date from", _p_labels.index.min().strftime("%Y-%m-%d"))
_pc4.metric("Date to", _p_labels.index.max().strftime("%Y-%m-%d"))
else:
st.warning("Stage 1 labels not found — run the pipeline first.")
if _p_val_img.exists():
st.image(str(_p_val_img), width='stretch',
caption="Top: Diurnal A pattern (bell curve peaking mid-morning). "
"Bottom: A vs PAR (light-response curve with saturation).")
st.success(
"**Key insight:** The model produces physiologically plausible A values — "
"a clear diurnal bell curve and the expected light-saturation response."
)
st.markdown(
"With reliable A measurements in hand, the next step is *predicting* A "
"from weather data so the system can plan ahead."
)
with st.expander("Reading the results: what is A? What do the plots show?"):
st.markdown(
"**A (net photosynthesis rate)** is measured in µmol CO₂ per m² of leaf "
"per second — higher means the vine is growing faster. Healthy grapevines "
"typically range from 5 to 20.\n\n"
"**Diurnal bell curve:** photosynthesis peaks mid-morning when both light "
"and temperature are favourable, then drops in the afternoon heat.\n\n"
"**Light-response curve:** A increases with PAR up to a point, then "
"flattens (saturates) — this proves the vine cannot use all of the "
"Negev's intense sunlight.\n\n"
"**\"Physiologically plausible\"** means the model reproduces the patterns "
"that plant biologists expect from real grapevines — it behaves like a "
"real vine, not just a curve fit."
)
st.divider()
st.markdown("## 4. Stage 2 — ML Prediction from Weather Data")
_p_metrics_path = settings.OUTPUTS_DIR / "stage2_metrics.csv"
_p_pred_img = settings.OUTPUTS_DIR / "stage2_predictions.png"
_p_ts_path = settings.OUTPUTS_DIR / "ts_horizon_comparison.csv"
if _p_metrics_path.exists():
_p_metrics = load_metrics(str(_p_metrics_path))
st.markdown("#### Cross-sectional model comparison")
st.dataframe(_p_metrics, hide_index=True)
if "R2" in _p_metrics.columns:
_p_best = _p_metrics.loc[_p_metrics["R2"].idxmax()]
st.markdown(
f"**Best model:** {_p_best['model']} — "
f"R² = {_p_best['R2']:.3f}, RMSE = {_p_best['RMSE']:.2f} µmol m⁻² s⁻¹. "
f"Explains **{_p_best['R2']*100:.0f}%** of variance in A from weather data."
)
else:
st.warning("Stage 2 metrics not found.")
if _p_pred_img.exists():
st.image(str(_p_pred_img), width='stretch',
caption="Left: Predicted vs approximate A (scatter). "
"Right: Time-series overlay showing temporal patterns.")
if _p_ts_path.exists():
_p_ts = load_metrics(str(_p_ts_path))
st.markdown("#### Time-series forecasting (lagged features)")
_p_ts_all = _p_ts[_p_ts["season"] == "all"] if "season" in _p_ts.columns else _p_ts
_p_ts_ts = _p_ts_all[_p_ts_all["approach"] == "time_series"].copy()
if not _p_ts_ts.empty:
_p_ts_disp = _p_ts_ts[["horizon", "RMSE", "MAE", "R2"]].copy()
for _c in ["RMSE", "MAE", "R2"]:
_p_ts_disp[_c] = _p_ts_disp[_c].round(3)
st.dataframe(_p_ts_disp, hide_index=True)
_p_best_ts = _p_ts_ts.loc[_p_ts_ts["R2"].idxmax()]
st.markdown(
f"**Best horizon:** {_p_best_ts['horizon']} — "
f"R² = {_p_best_ts['R2']:.3f}, RMSE = {_p_best_ts['RMSE']:.2f}. "
"Adding recent A history dramatically improves short-term predictions."
)
with st.expander("Understanding the metrics: R², RMSE, cross-sectional vs time-series"):
st.markdown(
"**R² (R-squared):** the proportion of variance explained by the model. "
"R² = 0.87 means the model explains 87% of the variation in A — close to "
"1.0 is excellent, 0.0 means no better than guessing the average.\n\n"
"**RMSE (Root Mean Squared Error):** the average prediction error in the "
"same units as A (µmol/m²/s). Lower is better. It penalises large errors "
"more than small ones.\n\n"
"**Cross-sectional model:** predicts A from the weather at the *same* "
"moment — no memory of what happened before.\n\n"
"**Time-series model:** also uses recent past values (lags) — "
"*\"what was A 15 minutes ago?\"* This helps because photosynthesis changes "
"smoothly over time.\n\n"
"**Why short horizons are better:** recent A values are very informative; "
"predicting 15 minutes ahead is much easier than predicting a month ahead."
)
st.divider()
st.markdown("## 5. ML Strategy — Four Prediction Layers")
st.markdown(
"Predicting photosynthesis is not a single-model problem. We stack four "
"complementary approaches, each adding a capability the previous one lacks:"
)
_ml_cols = st.columns(4)
with _ml_cols[0]:
st.markdown(
"### Layer 1\n"
"**Cross-sectional ML**\n\n"
"RF, GBR, XGBoost trained on IMS weather features at the *same* time step. "
"No memory of the past — purely \"given today's weather, what is A right now?\" "
"Best R² ≈ 0.80."
)
with _ml_cols[1]:
st.markdown(
"### Layer 2\n"
"**Time-series forecasting**\n\n"
"Adds lagged A and rolling weather windows (15 min – 3 h) as features. "
"A separate model per horizon (15-min to 1-month). "
"Best R² ≈ 0.87 at 15-min; degrades at longer horizons."
)
with _ml_cols[2]:
st.markdown(
"### Layer 3\n"
"**Chronos-2 foundation model**\n\n"
"Amazon's pre-trained 120M-parameter time-series model. Zero-shot "
"day-ahead forecast (24 h / 96 steps) with probabilistic confidence "
"bands — no task-specific training needed. Optional LoRA fine-tuning."
)
with _ml_cols[3]:
st.markdown(
"### Layer 4\n"
"**LLM orchestration (Gemini)**\n\n"
"Two roles: (a) *Data engineer* — LLM-generated anomaly thresholds and "
"stress features; (b) *Routing agent* — classifies real-time conditions "
"and picks FvCB or ML ensemble per 15-min slot, plus day-ahead advisory."
)
st.markdown(
"Each layer feeds the next: cross-sectional models establish a baseline, "
"time-series models exploit temporal continuity, Chronos-2 extends the "
"horizon to 24 hours with uncertainty quantification, and Gemini ties "
"everything together — cleaning the data, selecting the right model for "
"each moment, and producing actionable day-ahead shading recommendations."
)
st.markdown(
"Predictions alone aren't enough — we also need to understand exactly "
"how panel shadows affect each part of the canopy."
)
with st.expander("Why four layers instead of one best model?"):
st.markdown(
"**No single model wins everywhere.** Cross-sectional ML is robust but "
"blind to recent trends. Time-series models excel at short horizons but "
"degrade beyond a few hours. Chronos-2 handles day-ahead with built-in "
"uncertainty but needs a sanity check. The Gemini routing agent acts as "
"an intelligent dispatcher:\n\n"
"- **Cool morning, T < 30 °C →** route to Farquhar (mechanistic model "
"is most accurate in the RuBP-limited regime)\n"
"- **Hot afternoon, T > 30 °C →** route to ML ensemble (better at "
"capturing stress-induced non-linearities)\n"
"- **Day-ahead planning →** Chronos-2 probabilistic forecast, cross-checked "
"against vine biology rules (no shade before 10:00, no shade in May, "
"5% annual energy ceiling)\n\n"
"**Foundation model (Chronos-2):** a large neural network pre-trained on "
"millions of time series from many domains. It learns general patterns "
"(trends, seasonality, noise) and can forecast new series *zero-shot* — "
"without any vineyard-specific training.\n\n"
"**LoRA fine-tuning:** Low-Rank Adaptation — a lightweight method to "
"specialise the foundation model on our vineyard data using only ~500 "
"training steps, without modifying the original weights.\n\n"
"**LLM orchestration:** using a large language model (Gemini) not for "
"chat, but as a reasoning engine that reads sensor statistics, applies "
"domain rules, and outputs structured decisions (thresholds, routing "
"choices, shading schedules)."
)
st.divider()
st.markdown("## 6. Shadow Model & Shading Analysis")
st.markdown(
"A 3D ray-tracing shadow model computes where panel shadows fall on a "
"VSP-trained canopy (1.2 m tall, 3 vertical zones). Combined with the "
"Farquhar model, this shows how much photosynthesis the vine retains "
"under panels. At midsummer noon the vine retains the vast majority of "
"its photosynthesis under the panel — light saturation means halving PAR "
"barely affects A, so the surplus can be harvested as electricity."
)
st.markdown(
"This shadow model becomes the physics engine inside the tracker "
"controller — it evaluates candidate tilt angles and their effect on "
"each canopy zone in real time."
)
st.info(
"See the **Shading Simulator** tab for the interactive 3D shadow viewer "
"with date/hour controls, PAR and photosynthesis heatmaps, and zone-level analysis."
)
with st.expander("How the shadow model works"):
st.markdown(
"**VSP trellis (Vertical Shoot Positioning):** the standard vineyard "
"training system where shoots grow upward in a flat plane, creating a wall "
"of leaves roughly 1.2 m tall.\n\n"
"**3 vertical zones:** the canopy is divided into bottom (trunk/cordons), "
"middle (fruiting zone — where the grapes hang), and top (main leaf area). "
"Each zone has different leaf density and economic importance.\n\n"
"**Ray-tracing:** the model tracks the sun's position, calculates where "
"the panel casts a shadow, and determines which parts of the canopy are "
"shaded versus sunlit at each time step.\n\n"
"**Why the vine barely notices the panel:** at midday, even the shaded "
"parts receive enough scattered (diffuse) light plus direct light from "
"unshaded portions to saturate photosynthesis."
)
st.divider()
st.markdown("## 7. Tracker Control & Optimization")
st.markdown(
"**Default = full astronomical tracking** (maximum energy generation). "
"Anti-tracking is a rare, minimum-dose intervention ONLY when the fruiting "
"zone faces measurable heat/radiation risk. The user sets a hard energy "
"sacrifice ceiling (e.g. 5%/year) — the system guarantees it is never exceeded."
)
_tc_cols = st.columns(4)
with _tc_cols[0]:
st.markdown(
"### Budget Hierarchy\n"
"User sets an annual ceiling (e.g. 5%). Distributed "
"Year → Month → Week → Day → 15-min slot. "
"July–Aug get ~85% of the budget. Unused slots roll forward."
)
with _tc_cols[1]:
st.markdown(
"### Intervention Gate\n"
"Hard pass/fail filter. **No-shade windows:** before 10:00, May, "
"T < 28 °C, CWSI < 0.3. Eligible only when ALL pass: "
"T ≥ 30 °C, CWSI ≥ 0.4, GHI ≥ 500, FvCB confirms shading helps."
)
with _tc_cols[2]:
st.markdown(
"### Minimum Dose\n"
"Scan offsets [3°, 5°, 8°, 10°, 15°, 20°] smallest-first. "
"Select first that brings fruiting-zone PAR < 400 µmol while top "
"canopy > 70% sunlit, within budget."
)
with _tc_cols[3]:
st.markdown(
"### Day-Ahead DP\n"
"Discrete Dynamic Programming at sunrise: optimize 96-slot trajectory. "
"Utility = energy value + crop value − movement cost, subject to "
"daily budget. Phenological weighting (1.5× veraison)."
)
st.markdown(
"**Typical day:** full tracking 05:00–11:00 → minimum-dose shading "
"~3–8° from 11:00–15:30 → full tracking by 16:00. Over a season: "
"~92% full tracking, ~8% shading, sacrifice ≤ 5%."
)
st.info(
"This control system is the next implementation phase. The measurement, "
"prediction, and shadow modules above are its foundation. "
"Manual **LiCor gas-exchange measurements** (LI-6400/6800) will serve as "
"ground truth for calibrating model parameters and fine-tuning the ML pipeline."
)
with st.expander("How does the day-ahead optimizer work?"):
st.markdown(
"**DP in plain English:** plan the best tilt angle for each 15-min slot, "
"knowing that each choice affects how much budget remains for later slots.\n\n"
"**Utility function:** balances electricity revenue vs crop protection, and "
"penalises motor movement (each tilt change costs energy and wear).\n\n"
"**Phenological weighting:** crop value varies by growth stage — 1.5× during "
"veraison (berry ripening), 0.5× post-harvest — so the system shades more "
"aggressively when fruit quality is most at stake.\n\n"
"**Zone weighting:** top canopy 60%, fruiting zone 30%, trunk 10% — "
"penalises shading productive upper leaves, which generate most of the "
"vine's energy.\n\n"
"**Real-time override:** the DP plan is a *schedule*, not a command. Live "
"sensor readings override if the forecast diverges from reality.\n\n"
"**Fallback:** if anything fails — sensor dropout, model error, comms loss — "
"the tracker reverts to full astronomical tracking (zero sacrifice)."
)
st.markdown("## 8. Key Takeaways")
st.markdown("""
- **Farquhar model** → reliable 15-min photosynthesis measurements from on-site sensors
- **Four ML layers** → day-ahead prediction with uncertainty (cross-sectional → time-series → Chronos-2 → Gemini routing)
- **Shadow model** → physics engine for evaluating tilt-angle effects on each canopy zone
- **Tracker control** → budget-constrained DP optimization; minimum-dose shading; ≤ 5% energy sacrifice guaranteed
- **LiCor calibration** → manual gas-exchange ground truth triggers automated model retraining and versioned deployment
- **Full story** → measure → predict → simulate → control → calibrate — each module feeds the next
""")
st.markdown("Learn more at [solarwine.ai](https://www.solarwine.ai/)")