sensor-modeling 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. sensor_modeling/__init__.py +45 -0
  2. sensor_modeling/alerts/__init__.py +26 -0
  3. sensor_modeling/alerts/alert.py +532 -0
  4. sensor_modeling/analysis/__init__.py +43 -0
  5. sensor_modeling/analysis/_frame.py +19 -0
  6. sensor_modeling/analysis/behavioral_analysis.py +57 -0
  7. sensor_modeling/analysis/behavioral_metrics.py +66 -0
  8. sensor_modeling/analysis/comparison.py +164 -0
  9. sensor_modeling/analysis/dependency_network.py +408 -0
  10. sensor_modeling/analysis/granger_causality.py +314 -0
  11. sensor_modeling/analysis/pipeline.py +168 -0
  12. sensor_modeling/analysis/reporting.py +109 -0
  13. sensor_modeling/baseline/__init__.py +30 -0
  14. sensor_modeling/baseline/adaptive.py +520 -0
  15. sensor_modeling/baseline/features.py +224 -0
  16. sensor_modeling/change_point/__init__.py +13 -0
  17. sensor_modeling/change_point/_validation.py +31 -0
  18. sensor_modeling/change_point/adaptive_normalization.py +55 -0
  19. sensor_modeling/change_point/embedding_cpd.py +60 -0
  20. sensor_modeling/change_point/energy_efficient.py +57 -0
  21. sensor_modeling/change_point/genetic_optimization.py +65 -0
  22. sensor_modeling/cli.py +416 -0
  23. sensor_modeling/context/__init__.py +33 -0
  24. sensor_modeling/context/occupancy.py +529 -0
  25. sensor_modeling/data/__init__.py +5 -0
  26. sensor_modeling/data/loaders.py +146 -0
  27. sensor_modeling/data/preprocessing.py +83 -0
  28. sensor_modeling/data/synthetic.py +121 -0
  29. sensor_modeling/data/validation.py +81 -0
  30. sensor_modeling/evaluation/__init__.py +92 -0
  31. sensor_modeling/evaluation/ablation.py +303 -0
  32. sensor_modeling/evaluation/attribution.py +474 -0
  33. sensor_modeling/evaluation/detection.py +297 -0
  34. sensor_modeling/evaluation/metrics.py +541 -0
  35. sensor_modeling/evaluation/provenance.py +309 -0
  36. sensor_modeling/examples/__init__.py +1 -0
  37. sensor_modeling/examples/demos/__init__.py +1 -0
  38. sensor_modeling/examples/demos/ambient_pipeline_demo.py +418 -0
  39. sensor_modeling/examples/demos/bernoulli_ar_demo.py +356 -0
  40. sensor_modeling/examples/demos/cpd_ar_demo.py +25 -0
  41. sensor_modeling/examples/demos/cpd_benchmark.py +42 -0
  42. sensor_modeling/examples/demos/hmm_granger_demo.py +30 -0
  43. sensor_modeling/examples/demos/nhpp_pelt_demo.py +80 -0
  44. sensor_modeling/examples/tutorials/__init__.py +1 -0
  45. sensor_modeling/fusion/__init__.py +46 -0
  46. sensor_modeling/fusion/defaults.py +296 -0
  47. sensor_modeling/fusion/emissions.py +339 -0
  48. sensor_modeling/fusion/estimate.py +375 -0
  49. sensor_modeling/fusion/filter.py +323 -0
  50. sensor_modeling/health/__init__.py +31 -0
  51. sensor_modeling/health/monitor.py +590 -0
  52. sensor_modeling/health/status.py +74 -0
  53. sensor_modeling/hmm/__init__.py +15 -0
  54. sensor_modeling/hmm/adaptive_hmm.py +22 -0
  55. sensor_modeling/hmm/base.py +134 -0
  56. sensor_modeling/hmm/circadian_hmm.py +22 -0
  57. sensor_modeling/hmm/heterogeneous_hmm.py +22 -0
  58. sensor_modeling/hmm/hierarchical_hmm.py +35 -0
  59. sensor_modeling/hmm/scaled_dirichlet_hmm.py +23 -0
  60. sensor_modeling/interop/__init__.py +57 -0
  61. sensor_modeling/interop/fhir.py +418 -0
  62. sensor_modeling/interop/privacy.py +308 -0
  63. sensor_modeling/models/__init__.py +12 -0
  64. sensor_modeling/models/bernoulli_ar/__init__.py +6 -0
  65. sensor_modeling/models/bernoulli_ar/base_model.py +569 -0
  66. sensor_modeling/models/bernoulli_ar/multivariate_model.py +411 -0
  67. sensor_modeling/models/change_point_detection/__init__.py +10 -0
  68. sensor_modeling/models/change_point_detection/deep.py +65 -0
  69. sensor_modeling/models/change_point_detection/pelt.py +159 -0
  70. sensor_modeling/models/nhpp_pelt/__init__.py +5 -0
  71. sensor_modeling/models/nhpp_pelt/bspline.py +96 -0
  72. sensor_modeling/models/nhpp_pelt/cli.py +243 -0
  73. sensor_modeling/models/nhpp_pelt/diagnostics.py +234 -0
  74. sensor_modeling/models/nhpp_pelt/io.py +58 -0
  75. sensor_modeling/models/nhpp_pelt/model.py +408 -0
  76. sensor_modeling/models/nhpp_pelt/optimizer.py +142 -0
  77. sensor_modeling/models/nhpp_pelt/plotting.py +218 -0
  78. sensor_modeling/models/nhpp_pelt/quad.py +72 -0
  79. sensor_modeling/models/nhpp_pelt/regularization.py +121 -0
  80. sensor_modeling/models/nhpp_pelt/utils.py +174 -0
  81. sensor_modeling/observations/__init__.py +59 -0
  82. sensor_modeling/observations/adapters.py +195 -0
  83. sensor_modeling/observations/ingest.py +269 -0
  84. sensor_modeling/observations/observation.py +270 -0
  85. sensor_modeling/observations/registry.py +262 -0
  86. sensor_modeling/observations/stream.py +342 -0
  87. sensor_modeling/observations/types.py +107 -0
  88. sensor_modeling/observations/units.py +117 -0
  89. sensor_modeling/online/__init__.py +36 -0
  90. sensor_modeling/online/benchmarks.py +242 -0
  91. sensor_modeling/online/pipeline.py +485 -0
  92. sensor_modeling/simulation/__init__.py +54 -0
  93. sensor_modeling/simulation/faults.py +191 -0
  94. sensor_modeling/simulation/household.py +862 -0
  95. sensor_modeling/states/__init__.py +23 -0
  96. sensor_modeling/states/markov.py +105 -0
  97. sensor_modeling/states/ontology.py +238 -0
  98. sensor_modeling/utils/__init__.py +41 -0
  99. sensor_modeling/utils/data_io.py +199 -0
  100. sensor_modeling/utils/logging_config.py +10 -0
  101. sensor_modeling/utils/missing.py +188 -0
  102. sensor_modeling/utils/plotting.py +98 -0
  103. sensor_modeling/utils/validation.py +117 -0
  104. sensor_modeling/visualization/__init__.py +3 -0
  105. sensor_modeling/visualization/clinical.py +67 -0
  106. sensor_modeling/visualization/interactive.py +208 -0
  107. sensor_modeling/visualization/research.py +60 -0
  108. sensor_modeling/visualization/web_app.py +137 -0
  109. sensor_modeling-0.2.0.dist-info/METADATA +683 -0
  110. sensor_modeling-0.2.0.dist-info/RECORD +114 -0
  111. sensor_modeling-0.2.0.dist-info/WHEEL +5 -0
  112. sensor_modeling-0.2.0.dist-info/entry_points.txt +18 -0
  113. sensor_modeling-0.2.0.dist-info/licenses/LICENSE +21 -0
  114. sensor_modeling-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,418 @@
1
+ """FHIR-style export that keeps measurement and inference distinguishable.
2
+
3
+ The hazard this module exists to avoid is specific. Once a behavioural
4
+ conclusion is written into a clinical record it looks like every other entry
5
+ there, and a downstream reader has no way to tell that ``sleeping`` was
6
+ inferred by a Markov filter from a bed sensor and a wearable rather than
7
+ measured. Exporting inferred states as though they were observations is how a
8
+ research prototype ends up quoted as a clinical fact.
9
+
10
+ So the four kinds this platform keeps separate stay separate on the way out:
11
+
12
+ .. code-block:: text
13
+
14
+ measured observation -> Observation, status "final",
15
+ derived from a physical Device
16
+ derived feature -> Observation, status "final", but carrying the
17
+ upstream device's own confidence
18
+ inferred state -> Observation, status "preliminary", with an
19
+ explicit method, the full posterior as
20
+ components, and derivedFrom provenance
21
+ algorithmic alert -> DetectedIssue, never an Observation
22
+
23
+ Every exported resource carries provenance: what produced it, from what, when,
24
+ and with what quality. Inferred resources additionally carry the coverage and
25
+ attribution behind them, and a note recording that they are algorithmic
26
+ output from a research toolkit.
27
+
28
+ .. warning::
29
+
30
+ This is a **FHIR-style** export for interoperability prototyping. It is not
31
+ a validated FHIR profile, it has not been conformance-tested against a FHIR
32
+ server, and none of the codes below come from a recognised terminology. Do
33
+ not present its output as clinically validated data.
34
+ """
35
+
36
+ from __future__ import annotations
37
+
38
+ import logging
39
+ from collections.abc import Iterable, Mapping, Sequence
40
+ from datetime import datetime
41
+ from typing import Any
42
+
43
+ from ..alerts.alert import Alert, AlertKind, AlertSeverity
44
+ from ..fusion.estimate import StateEstimate
45
+ from ..observations.observation import Observation
46
+ from ..observations.registry import SensorRegistry
47
+ from ..observations.types import ObservationKind
48
+
49
+ logger = logging.getLogger(__name__)
50
+
51
+ #: Local code system for this toolkit's own vocabulary. Deliberately a
52
+ #: project URI rather than a borrowed clinical one: these codes have no
53
+ #: standardised meaning and must not be mistaken for LOINC or SNOMED.
54
+ CODE_SYSTEM = "https://github.com/DiogoRibeiro7/behavioral-sensing-research/codes"
55
+
56
+ #: Fixed disclaimer attached to every inferred resource.
57
+ RESEARCH_NOTE = (
58
+ "Algorithmically inferred from ambient sensor data by the sensor-modeling "
59
+ "research toolkit. Not a measured clinical observation, not a diagnosis, "
60
+ "and not the output of a medical device."
61
+ )
62
+
63
+ #: How an exported resource was produced.
64
+ PROVENANCE_MEASURED = "measured"
65
+ PROVENANCE_DERIVED_FEATURE = "derived-feature"
66
+ PROVENANCE_INFERRED = "inferred"
67
+
68
+ _SEVERITY_TO_FHIR = {
69
+ AlertSeverity.INFORMATION: "low",
70
+ AlertSeverity.ATTENTION: "moderate",
71
+ AlertSeverity.URGENT: "high",
72
+ }
73
+
74
+
75
+ def _instant(moment: datetime) -> str:
76
+ """Render a timezone-aware instant in the FHIR ``instant`` format."""
77
+ return moment.isoformat()
78
+
79
+
80
+ def _coding(code: str, display: str) -> dict[str, Any]:
81
+ """Build a CodeableConcept in this toolkit's own code system."""
82
+ return {
83
+ "coding": [{"system": CODE_SYSTEM, "code": code, "display": display}],
84
+ "text": display,
85
+ }
86
+
87
+
88
+ def _provenance_extension(kind: str, detail: str) -> dict[str, Any]:
89
+ """Build the extension that records how a resource was produced.
90
+
91
+ This is the load-bearing piece of the export. A reader that understands
92
+ nothing else about these resources can still read this and know whether
93
+ they are looking at a measurement or at a conclusion.
94
+ """
95
+ return {
96
+ "url": f"{CODE_SYSTEM}/provenance",
97
+ "extension": [
98
+ {"url": "kind", "valueCode": kind},
99
+ {"url": "detail", "valueString": detail},
100
+ ],
101
+ }
102
+
103
+
104
+ def observation_resource(
105
+ observation: Observation, registry: SensorRegistry | None = None
106
+ ) -> dict[str, Any]:
107
+ """Export one sensor observation as a FHIR-style ``Observation``.
108
+
109
+ A record whose ``confidence`` is below one is exported as a *derived
110
+ feature* rather than a measurement, because something upstream computed
111
+ it and attached its own uncertainty.
112
+ """
113
+ spec = registry.get(observation.sensor_id) if registry is not None else None
114
+ derived = observation.confidence < 1.0
115
+ kind = PROVENANCE_DERIVED_FEATURE if derived else PROVENANCE_MEASURED
116
+ description = (
117
+ spec.description
118
+ if spec is not None and spec.description
119
+ else f"{observation.modality.value} sensor reading"
120
+ )
121
+
122
+ resource: dict[str, Any] = {
123
+ "resourceType": "Observation",
124
+ "id": f"obs-{observation.sensor_id}-{int(observation.timestamp.timestamp())}",
125
+ "status": "final",
126
+ "category": [_coding("device-measurement", "Device measurement")],
127
+ "code": _coding(
128
+ f"sensor.{observation.modality.value}",
129
+ description,
130
+ ),
131
+ "effectiveDateTime": _instant(observation.timestamp),
132
+ "valueQuantity": {
133
+ "value": observation.value,
134
+ "unit": observation.unit.value,
135
+ "system": f"{CODE_SYSTEM}/units",
136
+ "code": observation.unit.value,
137
+ },
138
+ "device": {"display": observation.sensor_id},
139
+ "extension": [
140
+ _provenance_extension(
141
+ kind,
142
+ (
143
+ "Feature computed by the reporting device; the value is an "
144
+ "estimate, not a direct measurement."
145
+ if derived
146
+ else "Direct sensor measurement."
147
+ ),
148
+ ),
149
+ {
150
+ "url": f"{CODE_SYSTEM}/quality",
151
+ "extension": [
152
+ {"url": "quality", "valueDecimal": observation.quality},
153
+ {"url": "confidence", "valueDecimal": observation.confidence},
154
+ {"url": "kind", "valueCode": observation.kind.value},
155
+ {"url": "source", "valueString": observation.source},
156
+ ],
157
+ },
158
+ ],
159
+ }
160
+ if spec is not None and spec.room:
161
+ resource["bodySite"] = _coding(f"room.{spec.room}", spec.room)
162
+ if observation.flags:
163
+ resource["extension"].append(
164
+ {
165
+ "url": f"{CODE_SYSTEM}/ingestion-flags",
166
+ "valueString": ",".join(
167
+ sorted(flag.value for flag in observation.flags)
168
+ ),
169
+ }
170
+ )
171
+ # An event stream's silence is not a zero, so the export says so rather
172
+ # than leaving a reader to infer a value for the gaps.
173
+ if observation.kind is ObservationKind.EVENT:
174
+ resource["extension"].append(
175
+ {
176
+ "url": f"{CODE_SYSTEM}/event-semantics",
177
+ "valueString": (
178
+ "Event stream: absence of a record is absence of evidence, "
179
+ "not an observation of zero."
180
+ ),
181
+ }
182
+ )
183
+ return resource
184
+
185
+
186
+ def state_resource(
187
+ estimate: StateEstimate,
188
+ *,
189
+ subject: str = "Patient/example",
190
+ attribution: float | None = None,
191
+ ) -> dict[str, Any]:
192
+ """Export an inferred behavioural state as a FHIR-style ``Observation``.
193
+
194
+ Marked ``preliminary`` rather than ``final``, given an explicit ``method``
195
+ describing the inference, and accompanied by the whole posterior as
196
+ components. A reader is never shown only the winning label.
197
+
198
+ An abstaining estimate exports a ``dataAbsentReason`` instead of a value,
199
+ which is the correct FHIR idiom for "we do not know" and stops
200
+ ``unknown`` from being read as a behavioural finding.
201
+ """
202
+ resource: dict[str, Any] = {
203
+ "resourceType": "Observation",
204
+ "id": f"state-{int(estimate.at.timestamp())}",
205
+ "status": "preliminary",
206
+ "category": [_coding("activity", "Activity")],
207
+ "code": _coding("behavioural-state", "Inferred behavioural state"),
208
+ "subject": {"reference": subject},
209
+ "effectiveDateTime": _instant(estimate.at),
210
+ "method": _coding(
211
+ "multimodal-bayes-filter",
212
+ "Recursive Bayesian fusion of multimodal ambient sensor evidence",
213
+ ),
214
+ "note": [{"text": RESEARCH_NOTE}],
215
+ "extension": [
216
+ _provenance_extension(
217
+ PROVENANCE_INFERRED,
218
+ "Posterior over latent behavioural states; not a measurement.",
219
+ ),
220
+ {
221
+ "url": f"{CODE_SYSTEM}/inference-quality",
222
+ "extension": [
223
+ {"url": "confidence", "valueDecimal": estimate.confidence},
224
+ {"url": "completeness", "valueDecimal": estimate.completeness},
225
+ {
226
+ "url": "normalisedEntropy",
227
+ "valueDecimal": estimate.normalised_entropy,
228
+ },
229
+ {"url": "abstained", "valueBoolean": estimate.abstained},
230
+ ],
231
+ },
232
+ ],
233
+ "component": [
234
+ {
235
+ "code": _coding(f"state.{state.value}", state.value),
236
+ "valueQuantity": {"value": probability, "unit": "probability"},
237
+ }
238
+ for state, probability in estimate.probabilities.items()
239
+ ],
240
+ }
241
+
242
+ if estimate.abstained:
243
+ resource["dataAbsentReason"] = _coding(
244
+ "insufficient-evidence",
245
+ "Insufficient or unreliable sensor evidence to infer a state",
246
+ )
247
+ else:
248
+ resource["valueCodeableConcept"] = _coding(
249
+ f"state.{estimate.state.value}", estimate.state.value
250
+ )
251
+
252
+ if attribution is not None:
253
+ resource["extension"].append(
254
+ {
255
+ "url": f"{CODE_SYSTEM}/attribution",
256
+ "extension": [
257
+ {"url": "residentProbability", "valueDecimal": attribution},
258
+ {
259
+ "url": "note",
260
+ "valueString": (
261
+ "Probability that the monitored resident, rather "
262
+ "than another person present, generated the "
263
+ "ambient evidence behind this estimate."
264
+ ),
265
+ },
266
+ ],
267
+ }
268
+ )
269
+
270
+ contributing = [c.sensor_id for c in estimate.evidence if c.informative]
271
+ if contributing:
272
+ resource["derivedFrom"] = [
273
+ {"display": sensor_id} for sensor_id in sorted(contributing)
274
+ ]
275
+ if estimate.missing:
276
+ resource["extension"].append(
277
+ {
278
+ "url": f"{CODE_SYSTEM}/absent-evidence",
279
+ "valueString": ",".join(sorted(estimate.missing)),
280
+ }
281
+ )
282
+ return resource
283
+
284
+
285
+ def alert_resource(alert: Alert, *, subject: str = "Patient/example") -> dict[str, Any]:
286
+ """Export an alert as a FHIR-style ``DetectedIssue``.
287
+
288
+ Deliberately **not** an ``Observation``. An alert is an algorithmic
289
+ judgement that something warrants attention, not a record of anything
290
+ being observed, and giving it the same resource type as a measurement is
291
+ precisely the conflation this module exists to prevent.
292
+ """
293
+ resource: dict[str, Any] = {
294
+ "resourceType": "DetectedIssue",
295
+ "id": f"alert-{alert.identifier}",
296
+ "status": "preliminary",
297
+ "severity": _SEVERITY_TO_FHIR[alert.severity],
298
+ "code": _coding(f"alert.{alert.kind.value}", alert.kind.value),
299
+ "identifiedDateTime": _instant(alert.at),
300
+ "detail": alert.summary,
301
+ "author": {"display": "sensor-modeling research toolkit"},
302
+ "extension": [
303
+ _provenance_extension(
304
+ PROVENANCE_INFERRED,
305
+ "Algorithmic alert derived from inferred behaviour.",
306
+ ),
307
+ {
308
+ "url": f"{CODE_SYSTEM}/alert-grading",
309
+ "extension": [
310
+ {"url": "score", "valueDecimal": alert.score},
311
+ {"url": "confidence", "valueDecimal": alert.confidence},
312
+ {"url": "subjectOfAlert", "valueString": alert.subject},
313
+ ],
314
+ },
315
+ ],
316
+ "note": [{"text": RESEARCH_NOTE}],
317
+ }
318
+
319
+ if alert.kind is AlertKind.SYSTEM_HEALTH:
320
+ # A failing sensor is an equipment issue. Attaching it to a patient
321
+ # would make a maintenance problem look like a clinical finding.
322
+ resource["code"] = _coding("alert.system-health", "Sensing system health")
323
+ else:
324
+ resource["patient"] = {"reference": subject}
325
+
326
+ if alert.caveats:
327
+ resource["mitigation"] = [
328
+ {"action": _coding("caveat", caveat)} for caveat in alert.caveats
329
+ ]
330
+ return resource
331
+
332
+
333
+ def bundle(
334
+ *,
335
+ observations: Iterable[Observation] = (),
336
+ estimates: Iterable[StateEstimate] = (),
337
+ alerts: Iterable[Alert] = (),
338
+ registry: SensorRegistry | None = None,
339
+ subject: str = "Patient/example",
340
+ attribution: Mapping[datetime, float] | None = None,
341
+ ) -> dict[str, Any]:
342
+ """Assemble a FHIR-style ``Bundle`` of the requested resources.
343
+
344
+ Parameters
345
+ ----------
346
+ observations, estimates, alerts
347
+ The measurements, inferences and alerts to export. Each is rendered
348
+ with the provenance appropriate to its kind.
349
+ registry
350
+ Sensor declarations, used to label observations with their room and
351
+ documented semantics.
352
+ subject
353
+ Reference for the monitored person.
354
+ attribution
355
+ Optional per-estimate attribution probability, keyed by estimate time.
356
+ """
357
+ entries: list[dict[str, Any]] = []
358
+ for observation in observations:
359
+ entries.append({"resource": observation_resource(observation, registry)})
360
+ for estimate in estimates:
361
+ share = attribution.get(estimate.at) if attribution is not None else None
362
+ entries.append(
363
+ {"resource": state_resource(estimate, subject=subject, attribution=share)}
364
+ )
365
+ for alert in alerts:
366
+ entries.append({"resource": alert_resource(alert, subject=subject)})
367
+
368
+ logger.info("Assembled FHIR-style bundle with %d entries", len(entries))
369
+ return {
370
+ "resourceType": "Bundle",
371
+ "type": "collection",
372
+ "entry": entries,
373
+ "meta": {
374
+ "tag": [
375
+ {
376
+ "system": CODE_SYSTEM,
377
+ "code": "research-prototype",
378
+ "display": (
379
+ "FHIR-style export from a research toolkit. Not a "
380
+ "validated profile and not clinically validated data."
381
+ ),
382
+ }
383
+ ]
384
+ },
385
+ }
386
+
387
+
388
+ def summarise_provenance(payload: Mapping[str, Any]) -> dict[str, int]:
389
+ """Count how many resources in a bundle are measured versus inferred.
390
+
391
+ Provided so a consumer can assert, in one line, that it has not been
392
+ handed inferences dressed as measurements.
393
+ """
394
+ counts: dict[str, int] = {}
395
+ for entry in payload.get("entry", []):
396
+ resource = entry.get("resource", {})
397
+ for extension in resource.get("extension", []):
398
+ if extension.get("url") != f"{CODE_SYSTEM}/provenance":
399
+ continue
400
+ for inner in extension.get("extension", []):
401
+ if inner.get("url") == "kind":
402
+ kind = str(inner.get("valueCode"))
403
+ counts[kind] = counts.get(kind, 0) + 1
404
+ return counts
405
+
406
+
407
+ def measured_only(payload: Mapping[str, Any]) -> Sequence[Mapping[str, Any]]:
408
+ """Return only the genuinely measured resources from a bundle."""
409
+ return [
410
+ entry["resource"]
411
+ for entry in payload.get("entry", [])
412
+ if any(
413
+ inner.get("valueCode") == PROVENANCE_MEASURED
414
+ for extension in entry.get("resource", {}).get("extension", [])
415
+ if extension.get("url") == f"{CODE_SYSTEM}/provenance"
416
+ for inner in extension.get("extension", [])
417
+ )
418
+ ]