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.
- sensor_modeling/__init__.py +45 -0
- sensor_modeling/alerts/__init__.py +26 -0
- sensor_modeling/alerts/alert.py +532 -0
- sensor_modeling/analysis/__init__.py +43 -0
- sensor_modeling/analysis/_frame.py +19 -0
- sensor_modeling/analysis/behavioral_analysis.py +57 -0
- sensor_modeling/analysis/behavioral_metrics.py +66 -0
- sensor_modeling/analysis/comparison.py +164 -0
- sensor_modeling/analysis/dependency_network.py +408 -0
- sensor_modeling/analysis/granger_causality.py +314 -0
- sensor_modeling/analysis/pipeline.py +168 -0
- sensor_modeling/analysis/reporting.py +109 -0
- sensor_modeling/baseline/__init__.py +30 -0
- sensor_modeling/baseline/adaptive.py +520 -0
- sensor_modeling/baseline/features.py +224 -0
- sensor_modeling/change_point/__init__.py +13 -0
- sensor_modeling/change_point/_validation.py +31 -0
- sensor_modeling/change_point/adaptive_normalization.py +55 -0
- sensor_modeling/change_point/embedding_cpd.py +60 -0
- sensor_modeling/change_point/energy_efficient.py +57 -0
- sensor_modeling/change_point/genetic_optimization.py +65 -0
- sensor_modeling/cli.py +416 -0
- sensor_modeling/context/__init__.py +33 -0
- sensor_modeling/context/occupancy.py +529 -0
- sensor_modeling/data/__init__.py +5 -0
- sensor_modeling/data/loaders.py +146 -0
- sensor_modeling/data/preprocessing.py +83 -0
- sensor_modeling/data/synthetic.py +121 -0
- sensor_modeling/data/validation.py +81 -0
- sensor_modeling/evaluation/__init__.py +92 -0
- sensor_modeling/evaluation/ablation.py +303 -0
- sensor_modeling/evaluation/attribution.py +474 -0
- sensor_modeling/evaluation/detection.py +297 -0
- sensor_modeling/evaluation/metrics.py +541 -0
- sensor_modeling/evaluation/provenance.py +309 -0
- sensor_modeling/examples/__init__.py +1 -0
- sensor_modeling/examples/demos/__init__.py +1 -0
- sensor_modeling/examples/demos/ambient_pipeline_demo.py +418 -0
- sensor_modeling/examples/demos/bernoulli_ar_demo.py +356 -0
- sensor_modeling/examples/demos/cpd_ar_demo.py +25 -0
- sensor_modeling/examples/demos/cpd_benchmark.py +42 -0
- sensor_modeling/examples/demos/hmm_granger_demo.py +30 -0
- sensor_modeling/examples/demos/nhpp_pelt_demo.py +80 -0
- sensor_modeling/examples/tutorials/__init__.py +1 -0
- sensor_modeling/fusion/__init__.py +46 -0
- sensor_modeling/fusion/defaults.py +296 -0
- sensor_modeling/fusion/emissions.py +339 -0
- sensor_modeling/fusion/estimate.py +375 -0
- sensor_modeling/fusion/filter.py +323 -0
- sensor_modeling/health/__init__.py +31 -0
- sensor_modeling/health/monitor.py +590 -0
- sensor_modeling/health/status.py +74 -0
- sensor_modeling/hmm/__init__.py +15 -0
- sensor_modeling/hmm/adaptive_hmm.py +22 -0
- sensor_modeling/hmm/base.py +134 -0
- sensor_modeling/hmm/circadian_hmm.py +22 -0
- sensor_modeling/hmm/heterogeneous_hmm.py +22 -0
- sensor_modeling/hmm/hierarchical_hmm.py +35 -0
- sensor_modeling/hmm/scaled_dirichlet_hmm.py +23 -0
- sensor_modeling/interop/__init__.py +57 -0
- sensor_modeling/interop/fhir.py +418 -0
- sensor_modeling/interop/privacy.py +308 -0
- sensor_modeling/models/__init__.py +12 -0
- sensor_modeling/models/bernoulli_ar/__init__.py +6 -0
- sensor_modeling/models/bernoulli_ar/base_model.py +569 -0
- sensor_modeling/models/bernoulli_ar/multivariate_model.py +411 -0
- sensor_modeling/models/change_point_detection/__init__.py +10 -0
- sensor_modeling/models/change_point_detection/deep.py +65 -0
- sensor_modeling/models/change_point_detection/pelt.py +159 -0
- sensor_modeling/models/nhpp_pelt/__init__.py +5 -0
- sensor_modeling/models/nhpp_pelt/bspline.py +96 -0
- sensor_modeling/models/nhpp_pelt/cli.py +243 -0
- sensor_modeling/models/nhpp_pelt/diagnostics.py +234 -0
- sensor_modeling/models/nhpp_pelt/io.py +58 -0
- sensor_modeling/models/nhpp_pelt/model.py +408 -0
- sensor_modeling/models/nhpp_pelt/optimizer.py +142 -0
- sensor_modeling/models/nhpp_pelt/plotting.py +218 -0
- sensor_modeling/models/nhpp_pelt/quad.py +72 -0
- sensor_modeling/models/nhpp_pelt/regularization.py +121 -0
- sensor_modeling/models/nhpp_pelt/utils.py +174 -0
- sensor_modeling/observations/__init__.py +59 -0
- sensor_modeling/observations/adapters.py +195 -0
- sensor_modeling/observations/ingest.py +269 -0
- sensor_modeling/observations/observation.py +270 -0
- sensor_modeling/observations/registry.py +262 -0
- sensor_modeling/observations/stream.py +342 -0
- sensor_modeling/observations/types.py +107 -0
- sensor_modeling/observations/units.py +117 -0
- sensor_modeling/online/__init__.py +36 -0
- sensor_modeling/online/benchmarks.py +242 -0
- sensor_modeling/online/pipeline.py +485 -0
- sensor_modeling/simulation/__init__.py +54 -0
- sensor_modeling/simulation/faults.py +191 -0
- sensor_modeling/simulation/household.py +862 -0
- sensor_modeling/states/__init__.py +23 -0
- sensor_modeling/states/markov.py +105 -0
- sensor_modeling/states/ontology.py +238 -0
- sensor_modeling/utils/__init__.py +41 -0
- sensor_modeling/utils/data_io.py +199 -0
- sensor_modeling/utils/logging_config.py +10 -0
- sensor_modeling/utils/missing.py +188 -0
- sensor_modeling/utils/plotting.py +98 -0
- sensor_modeling/utils/validation.py +117 -0
- sensor_modeling/visualization/__init__.py +3 -0
- sensor_modeling/visualization/clinical.py +67 -0
- sensor_modeling/visualization/interactive.py +208 -0
- sensor_modeling/visualization/research.py +60 -0
- sensor_modeling/visualization/web_app.py +137 -0
- sensor_modeling-0.2.0.dist-info/METADATA +683 -0
- sensor_modeling-0.2.0.dist-info/RECORD +114 -0
- sensor_modeling-0.2.0.dist-info/WHEEL +5 -0
- sensor_modeling-0.2.0.dist-info/entry_points.txt +18 -0
- sensor_modeling-0.2.0.dist-info/licenses/LICENSE +21 -0
- 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
|
+
)
|