mgbam commited on
Commit
286eea9
·
verified ·
1 Parent(s): 9e0d992

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +83 -948
app.py CHANGED
@@ -1,1360 +1,495 @@
1
  from __future__ import annotations
2
 
3
-
4
  from datetime import datetime, timedelta
5
 
6
-
7
-
8
-
9
-
10
  """Sundew Diabetes Commons – holistic, open Streamlit experience."""
11
 
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
  import json
20
-
21
-
22
  import logging
23
-
24
-
25
  import math
26
-
27
-
28
  import time
29
-
30
-
31
  from dataclasses import dataclass
32
-
33
-
34
  from typing import Any, Dict, List, Optional, Tuple
35
 
36
-
37
-
38
-
39
-
40
  import numpy as np
41
-
42
-
43
  import pandas as pd
44
-
45
-
46
  import streamlit as st
47
-
48
-
49
  from sklearn.linear_model import LogisticRegression
50
-
51
-
52
  from sklearn.pipeline import Pipeline
53
-
54
-
55
  from sklearn.preprocessing import StandardScaler
56
 
57
-
58
-
59
-
60
-
61
  try:
62
-
63
-
64
  from sundew import SundewAlgorithm # type: ignore[attr-defined]
65
-
66
-
67
  from sundew.config import SundewConfig
68
-
69
-
70
  from sundew.config_presets import get_preset
71
 
72
-
73
-
74
-
75
-
76
  _HAS_SUNDEW = True
77
-
78
-
79
  except Exception: # fallback when package is unavailable
80
-
81
-
82
  SundewAlgorithm = None # type: ignore
83
-
84
-
85
  SundewConfig = object # type: ignore
86
 
87
-
88
-
89
-
90
-
91
  def get_preset(_: str) -> Any: # type: ignore
92
-
93
-
94
  return None
95
 
96
-
97
-
98
-
99
-
100
  _HAS_SUNDEW = False
101
 
102
-
103
-
104
-
105
-
106
  LOGGER = logging.getLogger("sundew.diabetes.commons")
107
 
108
 
109
-
110
-
111
-
112
-
113
-
114
-
115
  @dataclass
116
-
117
-
118
  class SundewGateConfig:
119
-
120
-
121
  target_activation: float = 0.22
122
-
123
-
124
  temperature: float = 0.08
125
-
126
-
127
  mode: str = "tuned_v2"
128
-
129
-
130
  use_native: bool = True
131
 
132
 
133
-
134
-
135
-
136
-
137
-
138
-
139
  def _build_sundew_runtime(config: SundewGateConfig) -> Optional[SundewAlgorithm]:
140
-
141
-
142
  if not (config.use_native and _HAS_SUNDEW and SundewAlgorithm is not None):
143
-
144
-
145
  return None
146
-
147
-
148
  try:
149
-
150
-
151
  preset = get_preset(config.mode)
152
-
153
-
154
  except Exception:
155
-
156
-
157
  preset = SundewConfig() # type: ignore
158
-
159
-
160
  for attr, value in (
161
-
162
-
163
  ("target_activation_rate", config.target_activation),
164
-
165
-
166
  ("gate_temperature", config.temperature),
167
-
168
-
169
  ):
170
-
171
-
172
  try:
173
-
174
-
175
  setattr(preset, attr, value)
176
-
177
-
178
  except Exception:
179
-
180
-
181
  pass
182
-
183
-
184
  for constructor in (
185
-
186
-
187
  lambda: SundewAlgorithm(preset), # type: ignore[arg-type]
188
-
189
-
190
  lambda: SundewAlgorithm(config=preset), # type: ignore[arg-type]
191
-
192
-
193
  lambda: SundewAlgorithm(),
194
-
195
-
196
  ):
197
-
198
-
199
  try:
200
-
201
-
202
  return constructor()
203
-
204
-
205
  except Exception:
206
-
207
-
208
  continue
209
-
210
-
211
  return None
212
 
213
 
214
-
215
-
216
-
217
-
218
-
219
-
220
  class AdaptiveGate:
221
-
222
-
223
  """Adapter that hides Sundew/Fallback branching."""
224
 
225
-
226
-
227
-
228
-
229
  def __init__(self, config: SundewGateConfig) -> None:
230
-
231
-
232
  self.config = config
233
-
234
-
235
  self._ema = 0.0
236
-
237
-
238
  self._tau = float(np.clip(config.target_activation, 0.05, 0.95))
239
-
240
-
241
  self._alpha = 0.05
242
-
243
-
244
  self.sundew: Optional[SundewAlgorithm] = _build_sundew_runtime(config)
245
 
246
-
247
-
248
-
249
-
250
  def decide(self, score: float) -> bool:
251
-
252
-
253
  if self.sundew is not None:
254
-
255
-
256
  for attr in ("decide", "step", "open"):
257
-
258
-
259
  fn = getattr(self.sundew, attr, None)
260
-
261
-
262
  if callable(fn):
263
-
264
-
265
  try:
266
-
267
-
268
  return bool(fn(score))
269
-
270
-
271
  except Exception:
272
-
273
-
274
  continue
275
-
276
-
277
  normalized = float(np.clip(score / 1.4, 0.0, 1.0))
278
-
279
-
280
  temperature = max(self.config.temperature, 0.02)
281
-
282
-
283
  probability = 1.0 / (1.0 + math.exp(-(normalized - self._tau) / temperature))
284
-
285
-
286
  fired = bool(np.random.rand() < probability)
287
-
288
-
289
- self._ema = (1 - self._alpha) * self._ema + self._alpha * (
290
-
291
-
292
- 1.0 if fired else 0.0
293
-
294
-
295
- )
296
-
297
-
298
  self._tau += 0.05 * (self.config.target_activation - self._ema)
299
-
300
-
301
  self._tau = float(np.clip(self._tau, 0.05, 0.95))
302
-
303
-
304
  return fired
305
 
306
 
307
-
308
-
309
-
310
-
311
-
312
-
313
  def load_example_dataset(n_rows: int = 720) -> pd.DataFrame:
314
-
315
-
316
  rng = np.random.default_rng(17)
317
-
318
-
319
  t0 = pd.Timestamp.utcnow().floor("5min") - pd.Timedelta(minutes=5 * n_rows)
320
-
321
-
322
  timestamps = [t0 + pd.Timedelta(minutes=5 * i) for i in range(n_rows)]
323
-
324
-
325
  base = 118 + 28 * np.sin(np.linspace(0, 7 * math.pi, n_rows))
326
-
327
-
328
  noise = rng.normal(0, 12, n_rows)
329
-
330
-
331
- meals = (rng.random(n_rows) < 0.05).astype(float) * rng.normal(50, 18, n_rows).clip(
332
-
333
-
334
- 0, 150
335
-
336
-
337
- )
338
-
339
-
340
- insulin = (rng.random(n_rows) < 0.03).astype(float) * rng.normal(
341
-
342
-
343
- 4.2, 1.5, n_rows
344
-
345
-
346
- ).clip(0, 10)
347
-
348
-
349
  steps = rng.integers(0, 200, size=n_rows)
350
-
351
-
352
  heart_rate = 68 + (steps > 90) * rng.integers(20, 45, size=n_rows)
353
-
354
-
355
  sleep_flag = (rng.random(n_rows) < 0.12).astype(float)
356
-
357
-
358
  stress_index = rng.uniform(0, 1, n_rows)
359
-
360
-
361
  glucose = base + noise
362
-
363
-
364
  for i in range(n_rows):
365
-
366
-
367
  if i >= 6:
368
-
369
-
370
  glucose[i] += 0.4 * meals[i - 6 : i].sum() / 6
371
-
372
-
373
  if i >= 4:
374
-
375
-
376
  glucose[i] -= 1.2 * insulin[i - 4 : i].sum() / 4
377
-
378
-
379
  if steps[i] > 100:
380
-
381
-
382
  glucose[i] -= 15
383
-
384
-
385
  glucose[180:200] = rng.normal(62, 5, 20)
386
-
387
-
388
  glucose[350:365] = rng.normal(210, 10, 15)
389
-
390
-
391
  return pd.DataFrame(
392
-
393
-
394
  {
395
-
396
-
397
  "timestamp": timestamps,
398
-
399
-
400
  "glucose_mgdl": np.round(np.clip(glucose, 40, 350), 1),
401
-
402
-
403
  "carbs_g": np.round(meals, 1),
404
-
405
-
406
  "insulin_units": np.round(insulin, 1),
407
-
408
-
409
  "steps": steps.astype(int),
410
-
411
-
412
  "hr": (heart_rate + rng.normal(0, 5, n_rows)).round().astype(int),
413
-
414
-
415
  "sleep_flag": sleep_flag,
416
-
417
-
418
  "stress_index": stress_index,
419
-
420
-
421
  }
422
-
423
-
424
  )
425
 
426
 
427
-
428
-
429
-
430
-
431
-
432
-
433
  def compute_features(df: pd.DataFrame) -> pd.DataFrame:
434
-
435
-
436
  df = df.copy().sort_values("timestamp").reset_index(drop=True)
437
-
438
-
439
  df["timestamp"] = pd.to_datetime(df["timestamp"], utc=True)
440
-
441
-
442
  df["glucose_prev"] = df["glucose_mgdl"].shift(1)
443
-
444
-
445
- dt = (
446
-
447
-
448
- df["timestamp"].astype("int64") - df["timestamp"].shift(1).astype("int64")
449
-
450
-
451
- ) / 60e9
452
-
453
-
454
  df["roc_mgdl_min"] = (df["glucose_mgdl"] - df["glucose_prev"]) / dt
455
-
456
-
457
  df["roc_mgdl_min"] = df["roc_mgdl_min"].replace([np.inf, -np.inf], 0.0).fillna(0.0)
458
-
459
-
460
  ema = df["glucose_mgdl"].ewm(span=48, adjust=False).mean()
461
-
462
-
463
  df["deviation"] = (df["glucose_mgdl"] - ema).fillna(0.0)
464
-
465
-
466
  df["iob_proxy"] = df["insulin_units"].rolling(12, min_periods=1).sum() / 12.0
467
-
468
-
469
  df["cob_proxy"] = df["carbs_g"].rolling(12, min_periods=1).sum() / 12.0
470
-
471
-
472
  df["variability"] = df["glucose_mgdl"].rolling(24, min_periods=2).std().fillna(0.0)
473
-
474
-
475
  df["activity_factor"] = (df["steps"] / 200.0 + df["hr"] / 160.0).clip(0, 1)
476
-
477
-
478
  df["sleep_flag"] = df["sleep_flag"].fillna(0.0) if "sleep_flag" in df else 0.0
479
-
480
-
481
  df["stress_index"] = df["stress_index"].fillna(0.5) if "stress_index" in df else 0.5
482
-
483
-
484
  return df[
485
-
486
-
487
  [
488
-
489
-
490
  "timestamp",
491
-
492
-
493
  "glucose_mgdl",
494
-
495
-
496
  "roc_mgdl_min",
497
-
498
-
499
  "deviation",
500
-
501
-
502
  "iob_proxy",
503
-
504
-
505
  "cob_proxy",
506
-
507
-
508
  "variability",
509
-
510
-
511
  "activity_factor",
512
-
513
-
514
  "sleep_flag",
515
-
516
-
517
  "stress_index",
518
-
519
-
520
  ]
521
-
522
-
523
  ].copy()
524
 
525
 
526
-
527
-
528
-
529
-
530
-
531
-
532
  def lightweight_score(row: pd.Series) -> float:
533
-
534
-
535
  glucose = row["glucose_mgdl"]
536
-
537
-
538
  roc = row["roc_mgdl_min"]
539
-
540
-
541
  deviation = row["deviation"]
542
-
543
-
544
  iob = row["iob_proxy"]
545
-
546
-
547
  cob = row["cob_proxy"]
548
-
549
-
550
  stress = row["stress_index"]
551
-
552
-
553
  score = 0.0
554
-
555
-
556
  score += max(0.0, (glucose - 180) / 80)
557
-
558
-
559
  score += max(0.0, (70 - glucose) / 30)
560
-
561
-
562
  score += abs(roc) / 6.0
563
-
564
-
565
  score += abs(deviation) / 100.0
566
-
567
-
568
  score += stress * 0.4
569
-
570
-
571
  score += max(0.0, (cob - iob) * 0.04)
572
-
573
-
574
  return float(np.clip(score, 0.0, 1.4))
575
 
576
 
577
-
578
-
579
-
580
-
581
-
582
-
583
  def train_simple_model(df: pd.DataFrame):
584
-
585
-
586
  features = df[
587
-
588
-
589
  [
590
-
591
-
592
  "glucose_mgdl",
593
-
594
-
595
  "roc_mgdl_min",
596
-
597
-
598
  "iob_proxy",
599
-
600
-
601
  "cob_proxy",
602
-
603
-
604
  "activity_factor",
605
-
606
-
607
  "variability",
608
-
609
-
610
  ]
611
-
612
-
613
  ]
614
-
615
-
616
  labels = (df["glucose_mgdl"] > 180).astype(int)
617
-
618
-
619
  model = Pipeline(
620
-
621
-
622
  [
623
-
624
-
625
  ("scaler", StandardScaler()),
626
-
627
-
628
  ("clf", LogisticRegression(max_iter=400, class_weight="balanced")),
629
-
630
-
631
  ]
632
-
633
-
634
  )
635
-
636
-
637
  try:
638
-
639
-
640
  model.fit(features, labels)
641
-
642
-
643
  return model
644
-
645
-
646
  except Exception:
647
-
648
-
649
  return None
650
 
651
 
652
-
653
-
654
-
655
-
656
-
657
-
658
  def render_overview(
659
-
660
-
661
  results: pd.DataFrame,
662
-
663
-
664
  alerts: List[Dict[str, Any]],
665
-
666
-
667
  gate_config: SundewGateConfig,
668
-
669
-
670
  ) -> None:
671
-
672
-
673
  total = len(results)
674
-
675
-
676
  activations = int(results["activated"].sum())
677
-
678
-
679
  activation_rate = activations / max(total, 1)
680
-
681
-
682
  energy_savings = max(0.0, 1.0 - activation_rate)
683
-
684
-
685
  col_a, col_b, col_c, col_d = st.columns(4)
686
-
687
-
688
  col_a.metric("Events", f"{total}")
689
-
690
-
691
  col_b.metric("Heavy activations", f"{activations} ({activation_rate:.1%})")
692
-
693
-
694
  col_c.metric("Estimated energy saved", f"{energy_savings:.1%}")
695
-
696
-
697
  col_d.metric("Alerts", f"{len(alerts)}")
698
-
699
-
700
  if gate_config.use_native and _HAS_SUNDEW:
701
-
702
-
703
  st.caption(
704
-
705
-
706
  "Energy savings follow 1 − activation rate. With native Sundew gating we target "
707
-
708
-
709
  f"≈{gate_config.target_activation:.0%} activations, so savings approach "
710
-
711
-
712
  f"{1 - gate_config.target_activation:.0%}."
713
-
714
-
715
  )
716
-
717
-
718
  else:
719
-
720
-
721
  st.warning(
722
-
723
-
724
  "Fallback gate active – heavy inference runs frequently, so savings mirror the observed activation rate."
725
-
726
-
727
  )
728
-
729
-
730
  with st.expander("Recent alerts", expanded=False):
731
-
732
-
733
  if alerts:
734
-
735
-
736
  st.table(pd.DataFrame(alerts).tail(10))
737
-
738
-
739
  else:
740
-
741
-
742
  st.info("No high-risk alerts in this window.")
743
-
744
-
745
  st.area_chart(results.set_index("timestamp")["glucose_mgdl"], height=220)
746
 
747
 
748
-
749
-
750
-
751
-
752
-
753
-
754
  def render_treatment_plan(medications: Dict[str, Any], next_visit: str) -> None:
755
-
756
-
757
- st.subheader("Full-cycle treatment support")
758
-
759
-
760
  st.write(
761
-
762
-
763
  "Upload or edit medication schedules, insulin titration guidance, and clinician notes."
764
-
765
-
766
  )
767
-
768
-
769
- st.json(medications, expanded=False)
770
-
771
-
772
- st.caption(f"Next scheduled review: {next_visit}")
773
-
774
-
775
-
776
-
777
-
778
-
779
 
780
 
781
  def render_lifestyle_support(results: pd.DataFrame) -> None:
782
-
783
-
784
  st.subheader("Lifestyle & wellbeing")
785
-
786
-
787
  recent = results.tail(96).copy()
788
-
789
-
790
  avg_glucose = recent["glucose_mgdl"].mean()
791
-
792
-
793
  active_minutes = int((recent["activity_factor"] > 0.4).sum() * 5)
794
-
795
-
796
  col1, col2 = st.columns(2)
797
-
798
-
799
  col1.metric("Average glucose (8h)", f"{avg_glucose:.1f} mg/dL")
800
-
801
-
802
  col2.metric("Active minutes", f"{active_minutes} min")
803
-
804
-
805
  st.markdown(
806
-
807
-
808
  """
809
-
810
-
811
  - Aim for gentle movement every hour you are awake.
812
-
813
-
814
  - Pair carbohydrates with protein/fiber to smooth spikes.
815
-
816
-
817
  - Sleep flagged recently? Try 10-minute breathing before bed.
818
-
819
-
820
  - Journal one gratitude moment—stress strongly shapes risk.
821
-
822
-
823
  """
824
-
825
-
826
  )
827
 
828
 
829
-
830
-
831
-
832
-
833
-
834
-
835
  def render_community_actions() -> Dict[str, List[str]]:
836
-
837
-
838
  st.subheader("Community impact")
839
-
840
-
841
  st.write(
842
-
843
-
844
  "Invite families, caregivers, and clinics to the commons. Set up alerts, shared logs, and outreach."
845
-
846
-
847
  )
848
-
849
-
850
  contact_list = [
851
-
852
-
853
  "SMS: +233-200-000-111",
854
-
855
-
856
  "WhatsApp: Care Circle Group",
857
-
858
-
859
  "Clinic portal: sundew.health/community",
860
-
861
-
862
  ]
863
-
864
-
865
  st.table(pd.DataFrame({"Support channel": contact_list}))
866
-
867
-
868
  return {
869
-
870
-
871
  "Desired partners": ["Rural clinics", "Youth ambassadors", "Nutrition co-ops"],
872
-
873
-
874
  "Needs": ["Smartphone grants", "Solar charging kits", "Translation volunteers"],
875
-
876
-
877
  }
878
 
879
 
880
-
881
-
882
-
883
-
884
-
885
-
886
  def render_telemetry(results: pd.DataFrame, telemetry: List[Dict[str, Any]]) -> None:
887
-
888
-
889
  st.subheader("Telemetry & export")
890
-
891
-
892
  st.write(
893
-
894
-
895
  "Download event-level telemetry for validation, research, or regulatory reporting."
896
-
897
-
898
  )
899
-
900
-
901
- st.caption(
902
-
903
-
904
  "Energy savings are computed as 1 minus the observed activation rate. When the gate stays mostly open, savings naturally trend toward zero."
905
-
906
-
907
  )
908
-
909
-
910
  json_payload = json.dumps(telemetry, default=str, indent=2)
911
-
912
-
913
  st.download_button(
914
-
915
-
916
  label="Download telemetry (JSON)",
917
-
918
-
919
  data=json_payload,
920
-
921
-
922
  file_name="sundew_diabetes_telemetry.json",
 
 
 
923
 
924
 
925
- mime="application/json",
926
-
927
-
928
- )
929
-
930
-
931
- st.dataframe(results.tail(100), use_container_width=True)
932
-
933
-
934
-
935
-
936
-
937
-
938
-
939
-
940
- def main() -> None:
941
-
942
-
943
- st.set_page_config(
944
-
945
-
946
- page_title="Sundew Diabetes Commons", layout="wide", page_icon="🕊"
947
-
948
-
949
- )
950
-
951
-
952
- st.title("Sundew Diabetes Commons")
953
-
954
-
955
- st.caption(
956
-
957
-
958
  "Open, compassionate diabetes care—monitoring, treatment, lifestyle, community."
959
-
960
-
961
  )
962
 
963
-
964
-
965
-
966
-
967
  st.sidebar.header("Load data")
968
-
969
-
970
  uploaded = st.sidebar.file_uploader("CGM / diary CSV", type=["csv"])
971
-
972
-
973
  use_example = st.sidebar.checkbox("Use synthetic example", True)
974
 
975
-
976
-
977
-
978
-
979
  st.sidebar.header("Sundew configuration")
980
-
981
-
982
  use_native = st.sidebar.checkbox(
983
-
984
-
985
  "Use native Sundew gating",
986
-
987
-
988
  value=_HAS_SUNDEW,
989
-
990
-
991
  help="Disable to demo the lightweight fallback gate only.",
992
-
993
-
994
  )
995
-
996
-
997
  target_activation = st.sidebar.slider("Target activation", 0.05, 0.90, 0.22, 0.01)
998
-
999
-
1000
  temperature = st.sidebar.slider("Gate temperature", 0.02, 0.50, 0.08, 0.01)
1001
-
1002
-
1003
  mode = st.sidebar.selectbox(
1004
-
1005
-
1006
  "Preset", ["tuned_v2", "conservative", "aggressive", "auto_tuned"], index=0
1007
-
1008
-
1009
  )
1010
 
1011
-
1012
-
1013
-
1014
-
1015
  if uploaded is not None:
1016
-
1017
-
1018
  df = pd.read_csv(uploaded)
1019
-
1020
-
1021
  elif use_example:
1022
-
1023
-
1024
  df = load_example_dataset()
1025
-
1026
-
1027
  else:
1028
-
1029
-
1030
  st.stop()
1031
 
1032
-
1033
-
1034
-
1035
-
1036
  features = compute_features(df)
1037
-
1038
-
1039
  model = train_simple_model(features)
1040
-
1041
-
1042
  gate_config = SundewGateConfig(
1043
-
1044
-
1045
  target_activation=target_activation,
1046
-
1047
-
1048
  temperature=temperature,
1049
-
1050
-
1051
  mode=mode,
1052
-
1053
-
1054
  use_native=use_native,
1055
-
1056
-
1057
  )
1058
-
1059
-
1060
  gate = AdaptiveGate(gate_config)
1061
 
1062
-
1063
-
1064
-
1065
-
1066
  telemetry: List[Dict[str, Any]] = []
1067
-
1068
-
1069
  records: List[Dict[str, Any]] = []
1070
-
1071
-
1072
  alerts: List[Dict[str, Any]] = []
1073
 
1074
-
1075
-
1076
-
1077
-
1078
  progress = st.progress(0)
1079
-
1080
-
1081
  status = st.empty()
1082
-
1083
-
1084
  for idx, row in enumerate(features.itertuples(index=False), start=1):
1085
-
1086
-
1087
  score = lightweight_score(pd.Series(row._asdict()))
1088
-
1089
-
1090
  should_run = gate.decide(score)
1091
-
1092
-
1093
  risk_proba = None
1094
-
1095
-
1096
  if should_run and model is not None:
1097
-
1098
-
1099
  sample = np.array(
1100
-
1101
-
1102
  [
1103
-
1104
-
1105
  [
1106
-
1107
-
1108
  row.glucose_mgdl,
1109
-
1110
-
1111
  row.roc_mgdl_min,
1112
-
1113
-
1114
  row.iob_proxy,
1115
-
1116
-
1117
  row.cob_proxy,
1118
-
1119
-
1120
  row.activity_factor,
1121
-
1122
-
1123
  row.variability,
1124
-
1125
-
1126
  ]
1127
-
1128
-
1129
  ]
1130
-
1131
-
1132
  )
1133
-
1134
-
1135
  try:
1136
-
1137
-
1138
  risk_proba = float(model.predict_proba(sample)[0, 1]) # type: ignore[index]
1139
-
1140
-
1141
  except Exception:
1142
-
1143
-
1144
  risk_proba = None
1145
-
1146
-
1147
  if risk_proba is not None and risk_proba >= 0.6:
1148
-
1149
-
1150
  alerts.append(
1151
-
1152
-
1153
  {
1154
-
1155
-
1156
  "timestamp": row.timestamp,
1157
-
1158
-
1159
  "glucose": row.glucose_mgdl,
1160
-
1161
-
1162
  "risk": risk_proba,
1163
-
1164
-
1165
  "message": "Check CGM, hydrate, plan balanced snack/insulin",
1166
-
1167
-
1168
  }
1169
-
1170
-
1171
  )
1172
-
1173
-
1174
  records.append(
1175
-
1176
-
1177
  {
1178
-
1179
-
1180
  "timestamp": row.timestamp,
1181
-
1182
-
1183
  "glucose_mgdl": row.glucose_mgdl,
1184
-
1185
-
1186
  "roc_mgdl_min": row.roc_mgdl_min,
1187
-
1188
-
1189
  "deviation": row.deviation,
1190
-
1191
-
1192
  "iob_proxy": row.iob_proxy,
1193
-
1194
-
1195
  "cob_proxy": row.cob_proxy,
1196
-
1197
-
1198
  "variability": row.variability,
1199
-
1200
-
1201
  "activity_factor": row.activity_factor,
1202
-
1203
-
1204
  "score": score,
1205
-
1206
-
1207
  "activated": should_run,
1208
-
1209
-
1210
  "risk_proba": risk_proba,
1211
-
1212
-
1213
  }
1214
-
1215
-
1216
  )
1217
-
1218
-
1219
  telemetry.append(
1220
-
1221
-
1222
  {
1223
-
1224
-
1225
  "timestamp": str(row.timestamp),
1226
-
1227
-
1228
  "score": score,
1229
-
1230
-
1231
  "activated": should_run,
1232
-
1233
-
1234
  "risk_proba": risk_proba,
1235
-
1236
-
1237
  }
1238
-
1239
-
1240
  )
1241
-
1242
-
1243
  progress.progress(idx / len(features))
1244
-
1245
-
1246
  status.text(f"Processing event {idx}/{len(features)}")
1247
-
1248
-
1249
  progress.empty()
1250
-
1251
-
1252
  status.empty()
1253
 
1254
-
1255
-
1256
-
1257
-
1258
- results = pd.DataFrame(records)
1259
-
1260
-
1261
-
1262
-
1263
-
1264
 
1265
  tabs = st.tabs(["Overview", "Treatment", "Lifestyle", "Community", "Telemetry"])
 
1266
  with tabs[0]:
1267
- render_overview(results, alerts, gate_config)
 
1268
  with tabs[1]:
1269
- st.subheader("Full-cycle treatment support")
1270
- default_plan = {
1271
- "Insulin": {
1272
- "Basal": "14u glargine at 21:00",
1273
- "Bolus": "1u per 10g carbs + correction 1u per 40 mg/dL over 140",
1274
- },
1275
- "Oral medications": {
1276
- "Metformin": "500mg breakfast + 500mg dinner",
1277
- "Empagliflozin": "10mg once daily (if eGFR > 45)",
1278
- },
1279
- "Monitoring": [
1280
- "CGM sensor change every 10 days",
1281
- "Morning fasted CGM calibration",
1282
- "Weekly telehealth coaching",
1283
- "Quarterly in-person clinician review",
1284
- ],
1285
- "Safety plan": [
1286
- "Carry glucose tabs + glucagon kit",
1287
- "Emergency contact: +233-200-000-888",
1288
- ],
1289
- "Lifestyle": [
1290
- "30 min brisk walk 5x/week",
1291
- "Bedtime snack if glucose < 110 mg/dL",
1292
- "Hydrate 2L water daily unless contraindicated",
1293
- ],
1294
- }
1295
- st.caption("Upload or edit schedules, medication titration guidance, and clinician notes.")
1296
- uploaded_plan = st.file_uploader(
1297
- "Optional plan JSON", type=["json"], key="plan_uploader"
1298
- )
1299
- plan_text = st.text_area(
1300
- "Edit plan JSON",
1301
- json.dumps(default_plan, indent=2),
1302
- height=240,
1303
- )
1304
- plan_data = default_plan
1305
- if uploaded_plan is not None:
1306
- try:
1307
- plan_data = json.load(uploaded_plan)
1308
- except Exception as exc:
1309
- st.error(f"Could not parse uploaded plan JSON: {exc}")
1310
- plan_data = default_plan
1311
- else:
1312
- try:
1313
- plan_data = json.loads(plan_text)
1314
- except Exception as exc:
1315
- st.warning(
1316
- f"Using default plan because text could not be parsed: {exc}"
1317
- )
1318
- plan_data = default_plan
1319
- next_visit = (datetime.utcnow() + timedelta(days=30)).strftime(
1320
- "%Y-%m-%d (telehealth)"
1321
- )
1322
- render_treatment_plan(plan_data, next_visit=next_visit)
 
1323
  with tabs[2]:
1324
- render_lifestyle_support(results)
 
1325
  with tabs[3]:
1326
- community_items = render_community_actions()
1327
- st.json(community_items, expanded=False)
1328
- with tabs[4]:
1329
- render_telemetry(results, telemetry)
1330
- st.sidebar.markdown("---")
1331
 
 
 
1332
 
 
1333
  status_text = (
1334
-
1335
-
1336
  "native gating"
1337
-
1338
-
1339
  if gate_config.use_native and gate.sundew is not None
1340
-
1341
-
1342
  else "fallback gate"
1343
-
1344
-
1345
  )
1346
-
1347
-
1348
  st.sidebar.caption(f"Sundew status: {status_text}")
1349
 
1350
 
1351
-
1352
-
1353
-
1354
-
1355
-
1356
-
1357
  if __name__ == "__main__":
1358
-
1359
-
1360
  main()
 
1
  from __future__ import annotations
2
 
 
3
  from datetime import datetime, timedelta
4
 
 
 
 
 
5
  """Sundew Diabetes Commons – holistic, open Streamlit experience."""
6
 
 
 
 
 
 
 
 
7
  import json
 
 
8
  import logging
 
 
9
  import math
 
 
10
  import time
 
 
11
  from dataclasses import dataclass
 
 
12
  from typing import Any, Dict, List, Optional, Tuple
13
 
 
 
 
 
14
  import numpy as np
 
 
15
  import pandas as pd
 
 
16
  import streamlit as st
 
 
17
  from sklearn.linear_model import LogisticRegression
 
 
18
  from sklearn.pipeline import Pipeline
 
 
19
  from sklearn.preprocessing import StandardScaler
20
 
 
 
 
 
21
  try:
 
 
22
  from sundew import SundewAlgorithm # type: ignore[attr-defined]
 
 
23
  from sundew.config import SundewConfig
 
 
24
  from sundew.config_presets import get_preset
25
 
 
 
 
 
26
  _HAS_SUNDEW = True
 
 
27
  except Exception: # fallback when package is unavailable
 
 
28
  SundewAlgorithm = None # type: ignore
 
 
29
  SundewConfig = object # type: ignore
30
 
 
 
 
 
31
  def get_preset(_: str) -> Any: # type: ignore
 
 
32
  return None
33
 
 
 
 
 
34
  _HAS_SUNDEW = False
35
 
 
 
 
 
36
  LOGGER = logging.getLogger("sundew.diabetes.commons")
37
 
38
 
 
 
 
 
 
 
39
  @dataclass
 
 
40
  class SundewGateConfig:
 
 
41
  target_activation: float = 0.22
 
 
42
  temperature: float = 0.08
 
 
43
  mode: str = "tuned_v2"
 
 
44
  use_native: bool = True
45
 
46
 
 
 
 
 
 
 
47
  def _build_sundew_runtime(config: SundewGateConfig) -> Optional[SundewAlgorithm]:
 
 
48
  if not (config.use_native and _HAS_SUNDEW and SundewAlgorithm is not None):
 
 
49
  return None
 
 
50
  try:
 
 
51
  preset = get_preset(config.mode)
 
 
52
  except Exception:
 
 
53
  preset = SundewConfig() # type: ignore
 
 
54
  for attr, value in (
 
 
55
  ("target_activation_rate", config.target_activation),
 
 
56
  ("gate_temperature", config.temperature),
 
 
57
  ):
 
 
58
  try:
 
 
59
  setattr(preset, attr, value)
 
 
60
  except Exception:
 
 
61
  pass
 
 
62
  for constructor in (
 
 
63
  lambda: SundewAlgorithm(preset), # type: ignore[arg-type]
 
 
64
  lambda: SundewAlgorithm(config=preset), # type: ignore[arg-type]
 
 
65
  lambda: SundewAlgorithm(),
 
 
66
  ):
 
 
67
  try:
 
 
68
  return constructor()
 
 
69
  except Exception:
 
 
70
  continue
 
 
71
  return None
72
 
73
 
 
 
 
 
 
 
74
  class AdaptiveGate:
 
 
75
  """Adapter that hides Sundew/Fallback branching."""
76
 
 
 
 
 
77
  def __init__(self, config: SundewGateConfig) -> None:
 
 
78
  self.config = config
 
 
79
  self._ema = 0.0
 
 
80
  self._tau = float(np.clip(config.target_activation, 0.05, 0.95))
 
 
81
  self._alpha = 0.05
 
 
82
  self.sundew: Optional[SundewAlgorithm] = _build_sundew_runtime(config)
83
 
 
 
 
 
84
  def decide(self, score: float) -> bool:
 
 
85
  if self.sundew is not None:
 
 
86
  for attr in ("decide", "step", "open"):
 
 
87
  fn = getattr(self.sundew, attr, None)
 
 
88
  if callable(fn):
 
 
89
  try:
 
 
90
  return bool(fn(score))
 
 
91
  except Exception:
 
 
92
  continue
 
 
93
  normalized = float(np.clip(score / 1.4, 0.0, 1.0))
 
 
94
  temperature = max(self.config.temperature, 0.02)
 
 
95
  probability = 1.0 / (1.0 + math.exp(-(normalized - self._tau) / temperature))
 
 
96
  fired = bool(np.random.rand() < probability)
97
+ self._ema = (1 - self._alpha) * self._ema + self._alpha * (1.0 if fired else 0.0)
 
 
 
 
 
 
 
 
 
 
98
  self._tau += 0.05 * (self.config.target_activation - self._ema)
 
 
99
  self._tau = float(np.clip(self._tau, 0.05, 0.95))
 
 
100
  return fired
101
 
102
 
 
 
 
 
 
 
103
  def load_example_dataset(n_rows: int = 720) -> pd.DataFrame:
 
 
104
  rng = np.random.default_rng(17)
 
 
105
  t0 = pd.Timestamp.utcnow().floor("5min") - pd.Timedelta(minutes=5 * n_rows)
 
 
106
  timestamps = [t0 + pd.Timedelta(minutes=5 * i) for i in range(n_rows)]
 
 
107
  base = 118 + 28 * np.sin(np.linspace(0, 7 * math.pi, n_rows))
 
 
108
  noise = rng.normal(0, 12, n_rows)
109
+ meals = (rng.random(n_rows) < 0.05).astype(float) * rng.normal(50, 18, n_rows).clip(0, 150)
110
+ insulin = (rng.random(n_rows) < 0.03).astype(float) * rng.normal(4.2, 1.5, n_rows).clip(0, 10)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  steps = rng.integers(0, 200, size=n_rows)
 
 
112
  heart_rate = 68 + (steps > 90) * rng.integers(20, 45, size=n_rows)
 
 
113
  sleep_flag = (rng.random(n_rows) < 0.12).astype(float)
 
 
114
  stress_index = rng.uniform(0, 1, n_rows)
 
 
115
  glucose = base + noise
 
 
116
  for i in range(n_rows):
 
 
117
  if i >= 6:
 
 
118
  glucose[i] += 0.4 * meals[i - 6 : i].sum() / 6
 
 
119
  if i >= 4:
 
 
120
  glucose[i] -= 1.2 * insulin[i - 4 : i].sum() / 4
 
 
121
  if steps[i] > 100:
 
 
122
  glucose[i] -= 15
 
 
123
  glucose[180:200] = rng.normal(62, 5, 20)
 
 
124
  glucose[350:365] = rng.normal(210, 10, 15)
 
 
125
  return pd.DataFrame(
 
 
126
  {
 
 
127
  "timestamp": timestamps,
 
 
128
  "glucose_mgdl": np.round(np.clip(glucose, 40, 350), 1),
 
 
129
  "carbs_g": np.round(meals, 1),
 
 
130
  "insulin_units": np.round(insulin, 1),
 
 
131
  "steps": steps.astype(int),
 
 
132
  "hr": (heart_rate + rng.normal(0, 5, n_rows)).round().astype(int),
 
 
133
  "sleep_flag": sleep_flag,
 
 
134
  "stress_index": stress_index,
 
 
135
  }
 
 
136
  )
137
 
138
 
 
 
 
 
 
 
139
  def compute_features(df: pd.DataFrame) -> pd.DataFrame:
 
 
140
  df = df.copy().sort_values("timestamp").reset_index(drop=True)
 
 
141
  df["timestamp"] = pd.to_datetime(df["timestamp"], utc=True)
 
 
142
  df["glucose_prev"] = df["glucose_mgdl"].shift(1)
143
+ dt = (df["timestamp"].astype("int64") - df["timestamp"].shift(1).astype("int64")) / 60e9
 
 
 
 
 
 
 
 
 
 
144
  df["roc_mgdl_min"] = (df["glucose_mgdl"] - df["glucose_prev"]) / dt
 
 
145
  df["roc_mgdl_min"] = df["roc_mgdl_min"].replace([np.inf, -np.inf], 0.0).fillna(0.0)
 
 
146
  ema = df["glucose_mgdl"].ewm(span=48, adjust=False).mean()
 
 
147
  df["deviation"] = (df["glucose_mgdl"] - ema).fillna(0.0)
 
 
148
  df["iob_proxy"] = df["insulin_units"].rolling(12, min_periods=1).sum() / 12.0
 
 
149
  df["cob_proxy"] = df["carbs_g"].rolling(12, min_periods=1).sum() / 12.0
 
 
150
  df["variability"] = df["glucose_mgdl"].rolling(24, min_periods=2).std().fillna(0.0)
 
 
151
  df["activity_factor"] = (df["steps"] / 200.0 + df["hr"] / 160.0).clip(0, 1)
 
 
152
  df["sleep_flag"] = df["sleep_flag"].fillna(0.0) if "sleep_flag" in df else 0.0
 
 
153
  df["stress_index"] = df["stress_index"].fillna(0.5) if "stress_index" in df else 0.5
 
 
154
  return df[
 
 
155
  [
 
 
156
  "timestamp",
 
 
157
  "glucose_mgdl",
 
 
158
  "roc_mgdl_min",
 
 
159
  "deviation",
 
 
160
  "iob_proxy",
 
 
161
  "cob_proxy",
 
 
162
  "variability",
 
 
163
  "activity_factor",
 
 
164
  "sleep_flag",
 
 
165
  "stress_index",
 
 
166
  ]
 
 
167
  ].copy()
168
 
169
 
 
 
 
 
 
 
170
  def lightweight_score(row: pd.Series) -> float:
 
 
171
  glucose = row["glucose_mgdl"]
 
 
172
  roc = row["roc_mgdl_min"]
 
 
173
  deviation = row["deviation"]
 
 
174
  iob = row["iob_proxy"]
 
 
175
  cob = row["cob_proxy"]
 
 
176
  stress = row["stress_index"]
 
 
177
  score = 0.0
 
 
178
  score += max(0.0, (glucose - 180) / 80)
 
 
179
  score += max(0.0, (70 - glucose) / 30)
 
 
180
  score += abs(roc) / 6.0
 
 
181
  score += abs(deviation) / 100.0
 
 
182
  score += stress * 0.4
 
 
183
  score += max(0.0, (cob - iob) * 0.04)
 
 
184
  return float(np.clip(score, 0.0, 1.4))
185
 
186
 
 
 
 
 
 
 
187
  def train_simple_model(df: pd.DataFrame):
 
 
188
  features = df[
 
 
189
  [
 
 
190
  "glucose_mgdl",
 
 
191
  "roc_mgdl_min",
 
 
192
  "iob_proxy",
 
 
193
  "cob_proxy",
 
 
194
  "activity_factor",
 
 
195
  "variability",
 
 
196
  ]
 
 
197
  ]
 
 
198
  labels = (df["glucose_mgdl"] > 180).astype(int)
 
 
199
  model = Pipeline(
 
 
200
  [
 
 
201
  ("scaler", StandardScaler()),
 
 
202
  ("clf", LogisticRegression(max_iter=400, class_weight="balanced")),
 
 
203
  ]
 
 
204
  )
 
 
205
  try:
 
 
206
  model.fit(features, labels)
 
 
207
  return model
 
 
208
  except Exception:
 
 
209
  return None
210
 
211
 
 
 
 
 
 
 
212
  def render_overview(
 
 
213
  results: pd.DataFrame,
 
 
214
  alerts: List[Dict[str, Any]],
 
 
215
  gate_config: SundewGateConfig,
 
 
216
  ) -> None:
 
 
217
  total = len(results)
 
 
218
  activations = int(results["activated"].sum())
 
 
219
  activation_rate = activations / max(total, 1)
 
 
220
  energy_savings = max(0.0, 1.0 - activation_rate)
 
 
221
  col_a, col_b, col_c, col_d = st.columns(4)
 
 
222
  col_a.metric("Events", f"{total}")
 
 
223
  col_b.metric("Heavy activations", f"{activations} ({activation_rate:.1%})")
 
 
224
  col_c.metric("Estimated energy saved", f"{energy_savings:.1%}")
 
 
225
  col_d.metric("Alerts", f"{len(alerts)}")
 
 
226
  if gate_config.use_native and _HAS_SUNDEW:
 
 
227
  st.caption(
 
 
228
  "Energy savings follow 1 − activation rate. With native Sundew gating we target "
 
 
229
  f"≈{gate_config.target_activation:.0%} activations, so savings approach "
 
 
230
  f"{1 - gate_config.target_activation:.0%}."
 
 
231
  )
 
 
232
  else:
 
 
233
  st.warning(
 
 
234
  "Fallback gate active – heavy inference runs frequently, so savings mirror the observed activation rate."
 
 
235
  )
 
 
236
  with st.expander("Recent alerts", expanded=False):
 
 
237
  if alerts:
 
 
238
  st.table(pd.DataFrame(alerts).tail(10))
 
 
239
  else:
 
 
240
  st.info("No high-risk alerts in this window.")
 
 
241
  st.area_chart(results.set_index("timestamp")["glucose_mgdl"], height=220)
242
 
243
 
 
 
 
 
 
 
244
  def render_treatment_plan(medications: Dict[str, Any], next_visit: str) -> None:
245
+ st.subheader("Full-cycle treatment support")
 
 
 
 
246
  st.write(
 
 
247
  "Upload or edit medication schedules, insulin titration guidance, and clinician notes."
 
 
248
  )
249
+ st.json(medications, expanded=False)
250
+ st.caption(f"Next scheduled review: {next_visit}")
 
 
 
 
 
 
 
 
 
 
251
 
252
 
253
  def render_lifestyle_support(results: pd.DataFrame) -> None:
 
 
254
  st.subheader("Lifestyle & wellbeing")
 
 
255
  recent = results.tail(96).copy()
 
 
256
  avg_glucose = recent["glucose_mgdl"].mean()
 
 
257
  active_minutes = int((recent["activity_factor"] > 0.4).sum() * 5)
 
 
258
  col1, col2 = st.columns(2)
 
 
259
  col1.metric("Average glucose (8h)", f"{avg_glucose:.1f} mg/dL")
 
 
260
  col2.metric("Active minutes", f"{active_minutes} min")
 
 
261
  st.markdown(
 
 
262
  """
 
 
263
  - Aim for gentle movement every hour you are awake.
 
 
264
  - Pair carbohydrates with protein/fiber to smooth spikes.
 
 
265
  - Sleep flagged recently? Try 10-minute breathing before bed.
 
 
266
  - Journal one gratitude moment—stress strongly shapes risk.
 
 
267
  """
 
 
268
  )
269
 
270
 
 
 
 
 
 
 
271
  def render_community_actions() -> Dict[str, List[str]]:
 
 
272
  st.subheader("Community impact")
 
 
273
  st.write(
 
 
274
  "Invite families, caregivers, and clinics to the commons. Set up alerts, shared logs, and outreach."
 
 
275
  )
 
 
276
  contact_list = [
 
 
277
  "SMS: +233-200-000-111",
 
 
278
  "WhatsApp: Care Circle Group",
 
 
279
  "Clinic portal: sundew.health/community",
 
 
280
  ]
 
 
281
  st.table(pd.DataFrame({"Support channel": contact_list}))
 
 
282
  return {
 
 
283
  "Desired partners": ["Rural clinics", "Youth ambassadors", "Nutrition co-ops"],
 
 
284
  "Needs": ["Smartphone grants", "Solar charging kits", "Translation volunteers"],
 
 
285
  }
286
 
287
 
 
 
 
 
 
 
288
  def render_telemetry(results: pd.DataFrame, telemetry: List[Dict[str, Any]]) -> None:
 
 
289
  st.subheader("Telemetry & export")
 
 
290
  st.write(
 
 
291
  "Download event-level telemetry for validation, research, or regulatory reporting."
 
 
292
  )
293
+ st.caption(
 
 
 
 
294
  "Energy savings are computed as 1 minus the observed activation rate. When the gate stays mostly open, savings naturally trend toward zero."
 
 
295
  )
 
 
296
  json_payload = json.dumps(telemetry, default=str, indent=2)
 
 
297
  st.download_button(
 
 
298
  label="Download telemetry (JSON)",
 
 
299
  data=json_payload,
 
 
300
  file_name="sundew_diabetes_telemetry.json",
301
+ mime="application/json",
302
+ )
303
+ st.dataframe(results.tail(100), use_container_width=True)
304
 
305
 
306
+ def main() -> None:
307
+ st.set_page_config(
308
+ page_title="Sundew Diabetes Commons", layout="wide", page_icon="🕊"
309
+ )
310
+ st.title("Sundew Diabetes Commons")
311
+ st.caption(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
312
  "Open, compassionate diabetes care—monitoring, treatment, lifestyle, community."
 
 
313
  )
314
 
 
 
 
 
315
  st.sidebar.header("Load data")
 
 
316
  uploaded = st.sidebar.file_uploader("CGM / diary CSV", type=["csv"])
 
 
317
  use_example = st.sidebar.checkbox("Use synthetic example", True)
318
 
 
 
 
 
319
  st.sidebar.header("Sundew configuration")
 
 
320
  use_native = st.sidebar.checkbox(
 
 
321
  "Use native Sundew gating",
 
 
322
  value=_HAS_SUNDEW,
 
 
323
  help="Disable to demo the lightweight fallback gate only.",
 
 
324
  )
 
 
325
  target_activation = st.sidebar.slider("Target activation", 0.05, 0.90, 0.22, 0.01)
 
 
326
  temperature = st.sidebar.slider("Gate temperature", 0.02, 0.50, 0.08, 0.01)
 
 
327
  mode = st.sidebar.selectbox(
 
 
328
  "Preset", ["tuned_v2", "conservative", "aggressive", "auto_tuned"], index=0
 
 
329
  )
330
 
 
 
 
 
331
  if uploaded is not None:
 
 
332
  df = pd.read_csv(uploaded)
 
 
333
  elif use_example:
 
 
334
  df = load_example_dataset()
 
 
335
  else:
 
 
336
  st.stop()
337
 
 
 
 
 
338
  features = compute_features(df)
 
 
339
  model = train_simple_model(features)
 
 
340
  gate_config = SundewGateConfig(
 
 
341
  target_activation=target_activation,
 
 
342
  temperature=temperature,
 
 
343
  mode=mode,
 
 
344
  use_native=use_native,
 
 
345
  )
 
 
346
  gate = AdaptiveGate(gate_config)
347
 
 
 
 
 
348
  telemetry: List[Dict[str, Any]] = []
 
 
349
  records: List[Dict[str, Any]] = []
 
 
350
  alerts: List[Dict[str, Any]] = []
351
 
 
 
 
 
352
  progress = st.progress(0)
 
 
353
  status = st.empty()
 
 
354
  for idx, row in enumerate(features.itertuples(index=False), start=1):
 
 
355
  score = lightweight_score(pd.Series(row._asdict()))
 
 
356
  should_run = gate.decide(score)
 
 
357
  risk_proba = None
 
 
358
  if should_run and model is not None:
 
 
359
  sample = np.array(
 
 
360
  [
 
 
361
  [
 
 
362
  row.glucose_mgdl,
 
 
363
  row.roc_mgdl_min,
 
 
364
  row.iob_proxy,
 
 
365
  row.cob_proxy,
 
 
366
  row.activity_factor,
 
 
367
  row.variability,
 
 
368
  ]
 
 
369
  ]
 
 
370
  )
 
 
371
  try:
 
 
372
  risk_proba = float(model.predict_proba(sample)[0, 1]) # type: ignore[index]
 
 
373
  except Exception:
 
 
374
  risk_proba = None
 
 
375
  if risk_proba is not None and risk_proba >= 0.6:
 
 
376
  alerts.append(
 
 
377
  {
 
 
378
  "timestamp": row.timestamp,
 
 
379
  "glucose": row.glucose_mgdl,
 
 
380
  "risk": risk_proba,
 
 
381
  "message": "Check CGM, hydrate, plan balanced snack/insulin",
 
 
382
  }
 
 
383
  )
 
 
384
  records.append(
 
 
385
  {
 
 
386
  "timestamp": row.timestamp,
 
 
387
  "glucose_mgdl": row.glucose_mgdl,
 
 
388
  "roc_mgdl_min": row.roc_mgdl_min,
 
 
389
  "deviation": row.deviation,
 
 
390
  "iob_proxy": row.iob_proxy,
 
 
391
  "cob_proxy": row.cob_proxy,
 
 
392
  "variability": row.variability,
 
 
393
  "activity_factor": row.activity_factor,
 
 
394
  "score": score,
 
 
395
  "activated": should_run,
 
 
396
  "risk_proba": risk_proba,
 
 
397
  }
 
 
398
  )
 
 
399
  telemetry.append(
 
 
400
  {
 
 
401
  "timestamp": str(row.timestamp),
 
 
402
  "score": score,
 
 
403
  "activated": should_run,
 
 
404
  "risk_proba": risk_proba,
 
 
405
  }
 
 
406
  )
 
 
407
  progress.progress(idx / len(features))
 
 
408
  status.text(f"Processing event {idx}/{len(features)}")
 
 
409
  progress.empty()
 
 
410
  status.empty()
411
 
412
+ results = pd.DataFrame(records)
 
 
 
 
 
 
 
 
 
413
 
414
  tabs = st.tabs(["Overview", "Treatment", "Lifestyle", "Community", "Telemetry"])
415
+
416
  with tabs[0]:
417
+ render_overview(results, alerts, gate_config)
418
+
419
  with tabs[1]:
420
+ st.subheader("Full-cycle treatment support")
421
+ default_plan = {
422
+ "Insulin": {
423
+ "Basal": "14u glargine at 21:00",
424
+ "Bolus": "1u per 10g carbs + correction 1u per 40 mg/dL over 140",
425
+ },
426
+ "Oral medications": {
427
+ "Metformin": "500mg breakfast + 500mg dinner",
428
+ "Empagliflozin": "10mg once daily (if eGFR > 45)",
429
+ },
430
+ "Monitoring": [
431
+ "CGM sensor change every 10 days",
432
+ "Morning fasted CGM calibration",
433
+ "Weekly telehealth coaching",
434
+ "Quarterly in-person clinician review",
435
+ ],
436
+ "Safety plan": [
437
+ "Carry glucose tabs + glucagon kit",
438
+ "Emergency contact: +233-200-000-888",
439
+ ],
440
+ "Lifestyle": [
441
+ "30 min brisk walk 5x/week",
442
+ "Bedtime snack if glucose < 110 mg/dL",
443
+ "Hydrate 2L water daily unless contraindicated",
444
+ ],
445
+ }
446
+ st.caption("Upload or edit schedules, medication titration guidance, and clinician notes.")
447
+ uploaded_plan = st.file_uploader(
448
+ "Optional plan JSON", type=["json"], key="plan_uploader"
449
+ )
450
+ plan_text = st.text_area(
451
+ "Edit plan JSON",
452
+ json.dumps(default_plan, indent=2),
453
+ height=240,
454
+ )
455
+ plan_data = default_plan
456
+ if uploaded_plan is not None:
457
+ try:
458
+ plan_data = json.load(uploaded_plan)
459
+ except Exception as exc:
460
+ st.error(f"Could not parse uploaded plan JSON: {exc}")
461
+ plan_data = default_plan
462
+ else:
463
+ try:
464
+ plan_data = json.loads(plan_text)
465
+ except Exception as exc:
466
+ st.warning(
467
+ f"Using default plan because text could not be parsed: {exc}"
468
+ )
469
+ plan_data = default_plan
470
+ next_visit = (datetime.utcnow() + timedelta(days=30)).strftime(
471
+ "%Y-%m-%d (telehealth)"
472
+ )
473
+ render_treatment_plan(plan_data, next_visit=next_visit)
474
+
475
  with tabs[2]:
476
+ render_lifestyle_support(results)
477
+
478
  with tabs[3]:
479
+ community_items = render_community_actions()
480
+ st.json(community_items, expanded=False)
 
 
 
481
 
482
+ with tabs[4]:
483
+ render_telemetry(results, telemetry)
484
 
485
+ st.sidebar.markdown("---")
486
  status_text = (
 
 
487
  "native gating"
 
 
488
  if gate_config.use_native and gate.sundew is not None
 
 
489
  else "fallback gate"
 
 
490
  )
 
 
491
  st.sidebar.caption(f"Sundew status: {status_text}")
492
 
493
 
 
 
 
 
 
 
494
  if __name__ == "__main__":
 
 
495
  main()