11from __future__ import annotations
22
33from dataclasses import dataclass
4+ from datetime import datetime , timedelta , timezone
45from typing import Any , Optional
56
67import pandas as pd
@@ -22,6 +23,86 @@ class AutotunePrepConfig:
2223 cgm_col : str = "CGM"
2324 bgi_col : str = "BGI"
2425
26+ # --- oref0-like categorization knobs (simplified) ---
27+ # classify points as CSF for X minutes after any announced carb entry
28+ meal_exclusion_minutes : int = 240
29+ # classify as UAM if deviation > threshold (mg/dL per 5 min)
30+ uam_deviation_threshold : float = 6.0
31+ # avoid using near-zero BGI points for ISF (prevents ratio explosions)
32+ min_bgi_abs_for_isf : float = 0.5
33+
34+
35+ def _parse_loop_ts (s : str ) -> datetime :
36+ # your fixtures use '...Z'; datetime.fromisoformat doesn't parse 'Z' in py<3.11 reliably
37+ if s .endswith ("Z" ):
38+ s = s [:- 1 ] + "+00:00"
39+ dt = datetime .fromisoformat (s )
40+ if dt .tzinfo is None :
41+ dt = dt .replace (tzinfo = timezone .utc )
42+ return dt .astimezone (timezone .utc )
43+
44+
45+ def _categorize_points_oref0_like (
46+ points : list [dict [str , Any ]],
47+ * ,
48+ loop_algorithm_input : dict ,
49+ cfg : AutotunePrepConfig ,
50+ ) -> dict [str , list [dict [str , Any ]]]:
51+ """
52+ Simplified oref0-like categorization:
53+ - CSF: within meal_exclusion_minutes of announced carbs
54+ - UAM: deviation > uam_deviation_threshold
55+ - ISF: remaining points with |BGI| >= min_bgi_abs_for_isf
56+ - basal: everything else
57+ """
58+ carb_entries = loop_algorithm_input .get ("carbEntries" , []) or []
59+ carb_times = []
60+ for c in carb_entries :
61+ if "date" in c and float (c .get ("grams" , 0 ) or 0 ) > 0 :
62+ carb_times .append (_parse_loop_ts (c ["date" ]))
63+ carb_times .sort ()
64+
65+ def in_meal_window (t : datetime ) -> bool :
66+ if not carb_times :
67+ return False
68+ # find latest carb time <= t
69+ # (linear scan is fine for small fixtures; can optimize later)
70+ last = None
71+ for ct in carb_times :
72+ if ct <= t :
73+ last = ct
74+ else :
75+ break
76+ if last is None :
77+ return False
78+ return t <= last + timedelta (minutes = cfg .meal_exclusion_minutes )
79+
80+ csf : list [dict [str , Any ]] = []
81+ uam : list [dict [str , Any ]] = []
82+ isf : list [dict [str , Any ]] = []
83+ basal : list [dict [str , Any ]] = []
84+
85+ for p in points :
86+ t = _parse_loop_ts (p ["date" ]) if isinstance (p ["date" ], str ) else p ["date" ]
87+ bgi = float (p ["BGI" ])
88+ dev = float (p ["deviation" ])
89+
90+ if in_meal_window (t ):
91+ csf .append (p )
92+ elif dev > cfg .uam_deviation_threshold :
93+ uam .append (p )
94+ elif abs (bgi ) >= cfg .min_bgi_abs_for_isf :
95+ isf .append (p )
96+ else :
97+ basal .append (p )
98+
99+ return {
100+ "ISFGlucoseData" : isf ,
101+ "CSFGlucoseData" : csf ,
102+ "UAMGlucoseData" : uam ,
103+ "basalGlucoseData" : basal ,
104+ }
105+
25106
26107def prepare_for_autotune_isf (
27108 df : pd .DataFrame ,
@@ -30,12 +111,16 @@ def prepare_for_autotune_isf(
30111 config : Optional [AutotunePrepConfig ] = None ,
31112) -> dict [str , Any ]:
32113 """
33- Prepare the exact inputs autotune_isf.tune_isf_like_oref0 needs .
114+ Prepare inputs for autotune_isf.tune_isf_like_oref0, with oref0-like categorization .
34115
35116 Returns:
36117 {
37118 "df": df_with_BGI,
38- "isf_glucose_data": list[{"date","avgDelta","BGI","deviation"}...]
119+ "isf_glucose_data": points to use for ISF tuning (ISFGlucoseData),
120+ "ISFGlucoseData": ...,
121+ "CSFGlucoseData": ...,
122+ "UAMGlucoseData": ...,
123+ "basalGlucoseData": ...,
39124 }
40125 """
41126 cfg = config or AutotunePrepConfig ()
@@ -48,12 +133,19 @@ def prepare_for_autotune_isf(
48133 history_hours = cfg .history_hours ,
49134 )
50135
51- df2 , isf_glucose_data = prepare_isf_glucose_data (
136+ df2 , all_points = prepare_isf_glucose_data (
52137 df ,
53138 loop_algorithm_input = loop_algorithm_input ,
54139 config = bgi_cfg ,
55140 cgm_col = cfg .cgm_col ,
56141 bgi_col = cfg .bgi_col ,
57142 )
58143
59- return {"df" : df2 , "isf_glucose_data" : isf_glucose_data }
144+ buckets = _categorize_points_oref0_like (all_points , loop_algorithm_input = loop_algorithm_input , cfg = cfg )
145+
146+ # Backward compatible: return ISF-only points as isf_glucose_data
147+ return {
148+ "df" : df2 ,
149+ "isf_glucose_data" : buckets ["ISFGlucoseData" ],
150+ ** buckets ,
151+ }
0 commit comments