Skip to content

Commit 7412bd9

Browse files
author
Sigrid Tofte Thiis
committed
diurnal schedules
1 parent 581cf7a commit 7412bd9

4 files changed

Lines changed: 191 additions & 1 deletion

File tree

loop_to_python_adaptive/autotune_isf.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,4 +333,41 @@ def run_autotune_isf_iterations(
333333
"finalISF": isf_current,
334334
"isf_history": isf_history,
335335
"last_result": last_result,
336-
}
336+
}
337+
338+
def update_profile_isf(
339+
loop_algorithm_input: dict,
340+
new_isf_scalar: float,
341+
) -> dict:
342+
"""
343+
Return a new loop_algorithm_input dict with the ISF updated to new_isf_scalar.
344+
345+
Called after run_autotune_isf_iterations() to apply the tuned ISF
346+
back into the profile for the next simulation epoch.
347+
348+
The sensitivity schedule shape is preserved — all entries are scaled
349+
by the ratio new_isf / current_pump_isf. This mirrors oref0 behaviour:
350+
the overall ISF level shifts but the diurnal shape stays intact.
351+
352+
Parameters
353+
----------
354+
loop_algorithm_input : The current loop_algorithm_input dict.
355+
Must contain a "sensitivity" key.
356+
new_isf_scalar : The finalISF from run_autotune_isf_iterations().
357+
358+
Returns
359+
-------
360+
A new dict — the original is never mutated.
361+
"""
362+
import copy
363+
old_isf = extract_pump_isf(loop_algorithm_input)
364+
if old_isf == 0:
365+
return loop_algorithm_input
366+
367+
ratio = new_isf_scalar / old_isf
368+
369+
updated = copy.deepcopy(loop_algorithm_input)
370+
for entry in updated["sensitivity"]:
371+
entry["value"] = round(float(entry["value"]) * ratio, 3)
372+
373+
return updated
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"isf_schedule": [[0, 45.0], [8, 50.0], [20, 42.0]],
3+
"basal_schedule": [[0, 0.8], [6, 1.2], [22, 0.9]],
4+
"cr_schedule": 10.0,
5+
"insulin_type": "novolog"
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"isf_schedule": 50.0,
3+
"basal_schedule": 1.0,
4+
"cr_schedule": 10.0,
5+
"insulin_type": "novolog"
6+
}

tests/test_pipeline.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
"""
2+
Step 1.1 — Verify the core pipeline:
3+
4+
df (from fixture)
5+
→ prepare_for_autotune_isf (autotune_prep)
6+
→ run_autotune_isf_iterations (autotune_isf)
7+
→ newISF
8+
9+
No SimGlucose. No AdaptiveLoopController. Just the data pipeline.
10+
11+
Run with:
12+
pytest tests/test_pipeline.py -v
13+
"""
14+
from __future__ import annotations
15+
16+
import json
17+
from pathlib import Path
18+
19+
import pandas as pd
20+
import pytest
21+
22+
from loop_to_python_adaptive.autotune_isf import (
23+
run_autotune_isf_iterations,
24+
extract_pump_isf,
25+
extract_pump_basal,
26+
extract_pump_cr,
27+
)
28+
from loop_to_python_adaptive.autotune_prep import AutotunePrepConfig
29+
30+
31+
def find_repo_root(start: Path) -> Path:
32+
p = start
33+
while True:
34+
if (p / "loop_to_python_adaptive").exists():
35+
return p
36+
if p.parent == p:
37+
raise RuntimeError("Could not find repo root.")
38+
p = p.parent
39+
40+
41+
@pytest.fixture
42+
def loop_input() -> dict:
43+
repo_root = find_repo_root(Path(__file__).resolve())
44+
file = repo_root / "tests" / "test_files" / "loop_algorithm_input.json"
45+
assert file.exists(), f"Missing fixture: {file}"
46+
return json.loads(file.read_text(encoding="utf-8"))
47+
48+
49+
@pytest.fixture
50+
def df_window(loop_input) -> pd.DataFrame:
51+
"""Build a CGM DataFrame from the fixture's glucoseHistory."""
52+
glucose = loop_input["glucoseHistory"]
53+
idx = pd.to_datetime([g["date"] for g in glucose], utc=True)
54+
df = pd.DataFrame(
55+
{"CGM": [float(g["value"]) for g in glucose]},
56+
index=idx,
57+
).sort_index()
58+
return df
59+
60+
61+
# ── Test 1: extractors work ───────────────────────────────────────────────────
62+
63+
def test_extract_pump_settings(loop_input):
64+
"""pump_isf, pump_basal, pump_cr can be extracted from the fixture."""
65+
isf = extract_pump_isf(loop_input)
66+
basal = extract_pump_basal(loop_input)
67+
cr = extract_pump_cr(loop_input)
68+
69+
assert isf > 0, f"pump_isf={isf}"
70+
assert basal > 0, f"pump_basal={basal}"
71+
assert cr > 0, f"pump_cr={cr}"
72+
print(f"\npump_isf={isf}, pump_basal={basal}, pump_cr={cr}")
73+
74+
75+
# ── Test 2: full pipeline runs and ISF changes ────────────────────────────────
76+
77+
def test_pipeline_isf_changes(loop_input, df_window):
78+
"""
79+
Core Step 1.1 test:
80+
df → run_autotune_isf_iterations → newISF
81+
Done when: isf_before != isf_after
82+
"""
83+
isf_before = extract_pump_isf(loop_input)
84+
print(f"\nISF before: {isf_before}")
85+
86+
result = run_autotune_isf_iterations(
87+
[df_window],
88+
loop_algorithm_inputs=[loop_input],
89+
n_iterations=1,
90+
)
91+
92+
isf_after = result["finalISF"]
93+
last = result["last_result"]
94+
95+
print(f"ISF after: {isf_after}")
96+
print(f"n_points: {last['n_points']}")
97+
print(f"reason: {last['reason']}")
98+
print(f"p50_ratio: {last['p50_ratio']}")
99+
100+
# Must have run successfully
101+
assert last["reason"] == "OK", (
102+
f"Autotune did not run — reason: {last['reason']}. "
103+
f"Only {last['n_points']} ISF points found in fixture. "
104+
"Check that the fixture has enough basal-only periods."
105+
)
106+
107+
# ISF must have changed
108+
assert isf_before != isf_after, (
109+
f"ISF unchanged at {isf_before}. "
110+
"Autotune ran but produced no change — check p50_ratio above."
111+
)
112+
113+
# newISF must be finite and positive
114+
assert isf_after > 0
115+
assert isf_after < 500 # sanity upper bound
116+
117+
118+
# ── Test 3: multiple iterations converge ─────────────────────────────────────
119+
120+
def test_pipeline_multiple_iterations_stable(loop_input, df_window):
121+
"""
122+
Running 3 iterations should not crash and ISF should stay within bounds.
123+
"""
124+
pump_isf = extract_pump_isf(loop_input)
125+
126+
result = run_autotune_isf_iterations(
127+
[df_window],
128+
loop_algorithm_inputs=[loop_input],
129+
n_iterations=3,
130+
)
131+
132+
history = result["isf_history"]
133+
assert len(history) == 3, f"Expected 3 history entries, got {len(history)}"
134+
135+
for i, isf in enumerate(history):
136+
assert isf > 0, f"Iteration {i+1}: ISF={isf} is not positive"
137+
# Must stay within autosens bounds (default 0.7–1.2 × pump_isf)
138+
assert isf >= pump_isf * 0.7 * 0.99, f"ISF {isf} too low vs pump {pump_isf}"
139+
assert isf <= pump_isf / 0.7 * 1.01, f"ISF {isf} too high vs pump {pump_isf}"
140+
141+
print(f"\nISF history over 3 iterations: {history}")

0 commit comments

Comments
 (0)