safraeli commited on
Commit
d4ceb3a
·
verified ·
1 Parent(s): 8551743

Fix chill units: x1.1 multiplier, Israel timezone

Browse files
Files changed (1) hide show
  1. backend/api/routes/biology.py +40 -27
backend/api/routes/biology.py CHANGED
@@ -48,18 +48,26 @@ async def biology_chill_units(
48
  season_start: str = Query("2025-11-01", description="Season start (YYYY-MM-DD)"),
49
  hub: DataHub = Depends(get_datahub),
50
  ):
51
- """Accumulated chill units from IMS hourly temperature.
52
-
53
- Uses IMS air_temperature_c (full season coverage from Nov 1).
54
- Model: T<=7 → +1 CU/h, 7<T<=10 → +0.5, 10<T<=18 → 0, T>18 → -1.
55
- Daily totals clipped at 0.
56
-
57
- Returns two series: "under_panels" uses IMS temp - 1°C (panels provide
58
- slight thermal buffering), "open_field" uses raw IMS temp.
 
 
 
 
 
 
59
  """
60
  import numpy as np
61
  import pandas as pd
62
 
 
 
63
  try:
64
  df = hub.weather._load_df()
65
  if df.empty:
@@ -73,42 +81,47 @@ async def biology_chill_units(
73
  if subset.empty or "air_temperature_c" not in subset.columns:
74
  return {"error": "No temperature data in season range"}
75
 
 
 
 
 
 
 
 
 
 
76
  # Hourly mean temperature
77
  hourly = subset["air_temperature_c"].resample("1h").mean().dropna()
78
  if hourly.empty:
79
  return {"error": "No hourly temperature data"}
80
 
81
- def _chill_daily(temps_hourly):
82
- temps = temps_hourly.values
83
- chill_h = np.select(
84
- [temps <= 7, (temps > 7) & (temps <= 10), (temps > 10) & (temps <= 18), temps > 18],
85
- [1.0, 0.5, 0.0, -1.0],
86
- )
87
- daily = pd.Series(chill_h, index=temps_hourly.index).resample("D").sum().clip(lower=0)
88
- return daily.cumsum()
89
-
90
- # Open field = raw IMS temperature
91
- cu_open = _chill_daily(hourly)
92
-
93
- # Under panels = IMS temp offset by ~-1°C (panels buffer nighttime lows)
94
- cu_panels = _chill_daily(hourly - 1.0)
95
 
96
- all_dates = sorted(cu_open.index.union(cu_panels.index))
 
 
 
97
 
98
  daily = [
99
  {
100
  "date": ts.strftime("%Y-%m-%d"),
101
- "under_panels": round(float(cu_panels.get(ts, 0)), 1) if ts in cu_panels.index else None,
102
- "open_field": round(float(cu_open.get(ts, 0)), 1) if ts in cu_open.index else None,
103
  }
104
- for ts in all_dates
105
  ]
106
 
107
  return {
108
  "season_start": season_start,
109
  "latest_under_panels": round(float(cu_panels.iloc[-1]), 1) if len(cu_panels) else 0,
110
  "latest_open_field": round(float(cu_open.iloc[-1]), 1) if len(cu_open) else 0,
111
- "days_counted": len(all_dates),
112
  "daily": daily,
113
  }
114
  except Exception as exc:
 
48
  season_start: str = Query("2025-11-01", description="Season start (YYYY-MM-DD)"),
49
  hub: DataHub = Depends(get_datahub),
50
  ):
51
+ """Accumulated chill units from IMS hourly temperature (Utah model).
52
+
53
+ Model (Richardson et al. 1974):
54
+ T <= 7°C → +1.0 CU/hour
55
+ 7 < T <= 10 → +0.5
56
+ 10 < T <= 18 → 0.0
57
+ T > 18 → -1.0
58
+ Daily totals clipped at 0 (no negative daily chill).
59
+ Season cumulative = running sum of daily CU from season_start.
60
+
61
+ Two series:
62
+ - open_field: raw IMS temperature
63
+ - under_panels: open_field × 1.1 (panels buffer nighttime → more chill)
64
+ Multiplier per Research/chill_hours/ANALYSIS_EXPLAINED.md.
65
  """
66
  import numpy as np
67
  import pandas as pd
68
 
69
+ PANEL_MULTIPLIER = 1.1 # under-panel chill ≈ 10% more than open field
70
+
71
  try:
72
  df = hub.weather._load_df()
73
  if df.empty:
 
81
  if subset.empty or "air_temperature_c" not in subset.columns:
82
  return {"error": "No temperature data in season range"}
83
 
84
+ # Convert to Israel local time for correct day boundaries
85
+ try:
86
+ from zoneinfo import ZoneInfo
87
+ tz = ZoneInfo("Asia/Jerusalem")
88
+ except ImportError:
89
+ tz = None
90
+ if tz:
91
+ subset = subset.tz_convert(tz)
92
+
93
  # Hourly mean temperature
94
  hourly = subset["air_temperature_c"].resample("1h").mean().dropna()
95
  if hourly.empty:
96
  return {"error": "No hourly temperature data"}
97
 
98
+ # Compute chill per hour using Utah model
99
+ temps = hourly.values
100
+ chill_hourly = np.select(
101
+ [temps <= 7.0, (temps > 7.0) & (temps <= 10.0),
102
+ (temps > 10.0) & (temps <= 18.0), temps > 18.0],
103
+ [1.0, 0.5, 0.0, -1.0],
104
+ )
 
 
 
 
 
 
 
105
 
106
+ # Daily chill = sum of hourly, clipped at 0
107
+ daily_chill = pd.Series(chill_hourly, index=hourly.index).resample("D").sum().clip(lower=0)
108
+ cu_open = daily_chill.cumsum()
109
+ cu_panels = (daily_chill * PANEL_MULTIPLIER).cumsum()
110
 
111
  daily = [
112
  {
113
  "date": ts.strftime("%Y-%m-%d"),
114
+ "under_panels": round(float(cu_panels.loc[ts]), 1),
115
+ "open_field": round(float(cu_open.loc[ts]), 1),
116
  }
117
+ for ts in daily_chill.index
118
  ]
119
 
120
  return {
121
  "season_start": season_start,
122
  "latest_under_panels": round(float(cu_panels.iloc[-1]), 1) if len(cu_panels) else 0,
123
  "latest_open_field": round(float(cu_open.iloc[-1]), 1) if len(cu_open) else 0,
124
+ "days_counted": len(daily_chill),
125
  "daily": daily,
126
  }
127
  except Exception as exc: