File size: 23,985 Bytes
13fc29d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
"""
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/)")