Skip to content

Commit a8d539b

Browse files
author
Sigrid Tofte Thiis
committed
introduced categorization of buckets
1 parent ba178fa commit a8d539b

8 files changed

Lines changed: 153 additions & 4 deletions

File tree

Binary file not shown.
Binary file not shown.
0 Bytes
Binary file not shown.

loop_to_python_adaptive/autotune_prep.py

Lines changed: 96 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
from dataclasses import dataclass
4+
from datetime import datetime, timedelta, timezone
45
from typing import Any, Optional
56

67
import 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

26107
def 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+
}
4.43 KB
Binary file not shown.

tests/test_.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from __future__ import annotations
2+
3+
import json
4+
from pathlib import Path
5+
6+
import numpy as np
7+
import pandas as pd
8+
9+
from loop_to_python_adaptive.autotune_prep import AutotunePrepConfig, prepare_for_autotune_isf
10+
from loop_to_python_adaptive.autotune_isf import tune_isf_like_oref0
11+
12+
13+
def find_repo_root(start: Path) -> Path:
14+
p = start
15+
while True:
16+
if (p / "loop_to_python_adaptive").exists():
17+
return p
18+
if p.parent == p:
19+
raise RuntimeError("Could not find repo root (no loop_to_python_adaptive directory found).")
20+
p = p.parent
21+
22+
23+
def test_autotune_isf_runs_on_loop_algorithm_input_fixture():
24+
repo_root = find_repo_root(Path(__file__).resolve())
25+
file = repo_root / "tests" / "test_files" / "loop_algorithm_input.json"
26+
assert file.exists(), f"Missing file: {file}"
27+
28+
loop_input = json.loads(file.read_text(encoding="utf-8"))
29+
30+
glucose = loop_input["glucoseHistory"]
31+
assert len(glucose) > 20
32+
33+
idx = pd.to_datetime([g["date"] for g in glucose], utc=True)
34+
df = pd.DataFrame({"CGM": [float(g["value"]) for g in glucose]}, index=idx).sort_index()
35+
36+
# Key change: fixture basal schedule starts ~6h before first glucose point
37+
prep = prepare_for_autotune_isf(
38+
df,
39+
loop_algorithm_input=loop_input,
40+
config=AutotunePrepConfig(history_hours=6),
41+
)
42+
43+
res = tune_isf_like_oref0(
44+
isf_current=50.0,
45+
pump_isf=None,
46+
isf_glucose_data=prep["isf_glucose_data"],
47+
)
48+
print("Autotune ISF result:", res)
49+
print(len(prep["ISFGlucoseData"]), len(prep["CSFGlucoseData"]), len(prep["UAMGlucoseData"]), len(prep["basalGlucoseData"]))
50+
51+
assert len(prep["ISFGlucoseData"]) > 0
52+
assert len(prep["CSFGlucoseData"]) >= 0
53+
assert len(prep["UAMGlucoseData"]) >= 0
54+
assert res["reason"] == "OK", res
55+
assert res["n_points"] >= 10, res
56+
assert np.isfinite(res["newISF"]), res
57+
assert abs(res["newISF"] - 80.0) > 0.01, res
3.23 KB
Binary file not shown.

tests/test_files/tests.py

Whitespace-only changes.

0 commit comments

Comments
 (0)