File size: 21,941 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
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
"""
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/)"

    )