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,191 @@
1
+ """Degrading a clean sensor record into a realistic one.
2
+
3
+ Fault injection is kept separate from behaviour generation so that a
4
+ robustness study can hold the resident's behaviour fixed and vary only what
5
+ went wrong with the apparatus. That separation is what makes a paired
6
+ comparison possible: the same simulated fortnight, once with a working
7
+ wearable and once without, differs in exactly one thing.
8
+
9
+ The faults available correspond to what actually happens in deployments:
10
+
11
+ .. code-block:: text
12
+
13
+ dropout a sensor stops reporting for a while
14
+ stuck a sensor keeps reporting its last value
15
+ missing records are lost in transit at random
16
+ non_adherence a wearable is not worn
17
+ late_arrival records reach the system out of order
18
+ duplication the same record is delivered twice
19
+ clock_drift one gateway's clock runs fast or slow
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import logging
25
+ from collections.abc import Iterable, Sequence
26
+ from dataclasses import dataclass, field, replace
27
+ from datetime import datetime, timedelta
28
+ from enum import Enum
29
+
30
+ import numpy as np
31
+
32
+ from ..observations.observation import Observation
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+
37
+ class FaultKind(str, Enum):
38
+ """A way in which a sensor record can be degraded."""
39
+
40
+ DROPOUT = "dropout"
41
+ """The sensor stops reporting entirely for a window."""
42
+
43
+ STUCK = "stuck"
44
+ """The sensor keeps reporting the value it had when the fault began."""
45
+
46
+ NON_ADHERENCE = "non_adherence"
47
+ """A wearable is not worn, so it reports nothing about the resident."""
48
+
49
+
50
+ @dataclass(frozen=True)
51
+ class Fault:
52
+ """One injected sensor fault over a known window."""
53
+
54
+ sensor_id: str
55
+ kind: FaultKind
56
+ start: datetime
57
+ end: datetime
58
+
59
+ def covers(self, observation: Observation) -> bool:
60
+ """Whether *observation* falls inside this fault's window."""
61
+ return (
62
+ observation.sensor_id == self.sensor_id
63
+ and self.start <= observation.timestamp < self.end
64
+ )
65
+
66
+ def to_dict(self) -> dict[str, object]:
67
+ """Return a serialisable form of the fault."""
68
+ return {
69
+ "sensor_id": self.sensor_id,
70
+ "kind": self.kind.value,
71
+ "start": self.start.isoformat(),
72
+ "end": self.end.isoformat(),
73
+ }
74
+
75
+
76
+ @dataclass
77
+ class DegradationConfig:
78
+ """How a clean record should be degraded.
79
+
80
+ Parameters
81
+ ----------
82
+ missing_rate
83
+ Probability that any individual record is lost in transit.
84
+ duplication_rate
85
+ Probability that a record is delivered twice.
86
+ late_rate
87
+ Probability that a record arrives materially after its timestamp.
88
+ late_delay
89
+ How late such a record arrives.
90
+ clock_drift
91
+ Per-source timestamp offset to apply, simulating a mis-set gateway
92
+ clock. Keys are source names as they appear on observations.
93
+ faults
94
+ Explicit sensor faults to inject.
95
+ seed
96
+ Random seed, so degradation is reproducible independently of the
97
+ behaviour it is applied to.
98
+ """
99
+
100
+ missing_rate: float = 0.0
101
+ duplication_rate: float = 0.0
102
+ late_rate: float = 0.0
103
+ late_delay: timedelta = timedelta(minutes=15)
104
+ clock_drift: dict[str, timedelta] = field(default_factory=dict)
105
+ faults: tuple[Fault, ...] = ()
106
+ seed: int = 7
107
+
108
+ def __post_init__(self) -> None:
109
+ """Validate the degradation configuration."""
110
+ for name in ("missing_rate", "duplication_rate", "late_rate"):
111
+ value = float(getattr(self, name))
112
+ if not 0.0 <= value <= 1.0:
113
+ raise ValueError(f"{name} must lie in [0, 1]")
114
+ if self.late_delay < timedelta(0):
115
+ raise ValueError("late_delay must be non-negative")
116
+ for fault in self.faults:
117
+ if fault.end <= fault.start:
118
+ raise ValueError("fault windows must have positive duration")
119
+
120
+
121
+ def degrade(
122
+ observations: Iterable[Observation], config: DegradationConfig
123
+ ) -> tuple[list[Observation], list[Observation]]:
124
+ """Apply degradation to a clean record.
125
+
126
+ Returns
127
+ -------
128
+ tuple[list[Observation], list[Observation]]
129
+ The degraded records in arrival order, and the records that were
130
+ dropped. The dropped list exists so that an evaluation can report how
131
+ much evidence was actually withheld rather than having to infer it.
132
+ """
133
+ rng = np.random.default_rng(config.seed)
134
+ kept: list[Observation] = []
135
+ dropped: list[Observation] = []
136
+ stuck_values: dict[str, float] = {}
137
+
138
+ for observation in observations:
139
+ fault = next((f for f in config.faults if f.covers(observation)), None)
140
+ if fault is not None:
141
+ if fault.kind in (FaultKind.DROPOUT, FaultKind.NON_ADHERENCE):
142
+ dropped.append(observation)
143
+ continue
144
+ if fault.kind is FaultKind.STUCK:
145
+ held = stuck_values.setdefault(observation.sensor_id, observation.value)
146
+ observation = replace(observation, value=held)
147
+ else:
148
+ stuck_values.pop(observation.sensor_id, None)
149
+
150
+ if config.missing_rate and rng.random() < config.missing_rate:
151
+ dropped.append(observation)
152
+ continue
153
+
154
+ drift = config.clock_drift.get(observation.source)
155
+ if drift:
156
+ observation = observation.shifted(drift, flag=False)
157
+
158
+ received = observation.timestamp
159
+ if config.late_rate and rng.random() < config.late_rate:
160
+ received = received + config.late_delay
161
+ observation = replace(observation, received_at=received)
162
+
163
+ kept.append(observation)
164
+ if config.duplication_rate and rng.random() < config.duplication_rate:
165
+ kept.append(observation)
166
+
167
+ # Sort by arrival rather than by event time: that is the order a live
168
+ # system genuinely sees, and it is what exercises out-of-order handling.
169
+ kept.sort(key=lambda obs: (obs.received_at or obs.timestamp))
170
+ logger.info("Degraded record: %d delivered, %d withheld", len(kept), len(dropped))
171
+ return kept, dropped
172
+
173
+
174
+ def dropout(sensor_id: str, start: datetime, duration: timedelta) -> Fault:
175
+ """Build a dropout fault of *duration* starting at *start*."""
176
+ return Fault(sensor_id, FaultKind.DROPOUT, start, start + duration)
177
+
178
+
179
+ def stuck(sensor_id: str, start: datetime, duration: timedelta) -> Fault:
180
+ """Build a stuck-value fault of *duration* starting at *start*."""
181
+ return Fault(sensor_id, FaultKind.STUCK, start, start + duration)
182
+
183
+
184
+ def not_worn(
185
+ sensor_ids: Sequence[str], start: datetime, duration: timedelta
186
+ ) -> tuple[Fault, ...]:
187
+ """Build non-adherence faults for every device on the resident's person."""
188
+ return tuple(
189
+ Fault(sensor_id, FaultKind.NON_ADHERENCE, start, start + duration)
190
+ for sensor_id in sensor_ids
191
+ )