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

Fix CU: use TB Air1 data in 7-day chunks (matches TB 282/310)

Browse files
Files changed (1) hide show
  1. backend/api/routes/biology.py +36 -28
backend/api/routes/biology.py CHANGED
@@ -48,7 +48,7 @@ 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 (Utah model).
52
 
53
  Model (Richardson et al. 1974):
54
  T <= 7°C → +1.0 CU/hour
@@ -59,43 +59,52 @@ async def biology_chill_units(
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:
74
- return {"error": "No IMS data available for chill computation"}
75
-
76
- if "timestamp_utc" in df.columns:
77
- df = df.set_index(pd.to_datetime(df["timestamp_utc"], utc=True))
78
 
 
 
79
  start = pd.Timestamp(season_start, tz="UTC")
80
- subset = df.loc[start:]
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),
@@ -103,7 +112,6 @@ async def biology_chill_units(
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()
 
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 ThingsBoard Air1 on-site temperature (Utah model).
52
 
53
  Model (Richardson et al. 1974):
54
  T <= 7°C → +1.0 CU/hour
 
59
  Season cumulative = running sum of daily CU from season_start.
60
 
61
  Two series:
62
+ - open_field: on-site Air1 temperature
63
  - under_panels: open_field × 1.1 (panels buffer nighttime → more chill)
64
+ Per Research/chill_hours/ANALYSIS_EXPLAINED.md.
65
  """
66
  import numpy as np
67
  import pandas as pd
68
 
69
+ PANEL_MULTIPLIER = 1.1
70
 
71
  try:
72
+ from src.data.thingsboard_client import ThingsBoardClient
73
+ from zoneinfo import ZoneInfo
 
 
 
 
74
 
75
+ client = ThingsBoardClient()
76
+ tz = ZoneInfo("Asia/Jerusalem")
77
  start = pd.Timestamp(season_start, tz="UTC")
78
+ end = pd.Timestamp.now(tz="UTC")
79
+
80
+ # Fetch Air1 temperature in 7-day chunks (TB rejects large ranges)
81
+ chunks = []
82
+ cursor = start
83
+ while cursor < end:
84
+ chunk_end = min(cursor + pd.Timedelta(days=7), end)
85
+ try:
86
+ df = client.get_timeseries(
87
+ "Air1", ["airTemperature"],
88
+ start=cursor.to_pydatetime(), end=chunk_end.to_pydatetime(),
89
+ interval_ms=0, agg="NONE", limit=10000,
90
+ )
91
+ if not df.empty:
92
+ chunks.append(df)
93
+ except Exception:
94
+ pass
95
+ cursor = chunk_end
96
+
97
+ if not chunks:
98
+ return {"error": "No Air1 temperature data available from ThingsBoard"}
99
+
100
+ full = pd.concat(chunks).sort_index()
101
+ full = full[~full.index.duplicated(keep="first")]
102
+ full = full.tz_convert(tz)
103
+
104
+ hourly = full["airTemperature"].resample("1h").mean().dropna()
105
  if hourly.empty:
106
+ return {"error": "No hourly temperature after resampling"}
107
 
 
108
  temps = hourly.values
109
  chill_hourly = np.select(
110
  [temps <= 7.0, (temps > 7.0) & (temps <= 10.0),
 
112
  [1.0, 0.5, 0.0, -1.0],
113
  )
114
 
 
115
  daily_chill = pd.Series(chill_hourly, index=hourly.index).resample("D").sum().clip(lower=0)
116
  cu_open = daily_chill.cumsum()
117
  cu_panels = (daily_chill * PANEL_MULTIPLIER).cumsum()