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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1360 -1362
app.py CHANGED
@@ -1,1362 +1,1360 @@
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()
1361
-
1362
-
 
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()