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
sensor_modeling/cli.py ADDED
@@ -0,0 +1,416 @@
1
+ """Unified command-line interface for sensor modeling."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import logging
7
+ from datetime import timedelta
8
+ from pathlib import Path
9
+
10
+ from .models.bernoulli_ar.base_model import BernoulliAutoregressiveModel
11
+ from .models.nhpp_pelt.model import NHPPPELT, NHPPConfig
12
+ from .utils.data_io import SensorDataset
13
+ from .utils.logging_config import setup_logging
14
+
15
+
16
+ def _add_model_parsers(sub: argparse._SubParsersAction) -> None:
17
+ """Register the single-model fitting commands."""
18
+ ar_p = sub.add_parser("bernoulli-ar", help="Run Bernoulli autoregressive model")
19
+ ar_p.add_argument("data", help="Path to CSV sensor data")
20
+ ar_p.add_argument("target", help="Target sensor column")
21
+
22
+ nhpp_p = sub.add_parser("nhpp-pelt", help="Run NHPP-PELT changepoint model")
23
+ nhpp_p.add_argument("data", help="Path to CSV sensor data")
24
+ nhpp_p.add_argument("sensor", help="Sensor column to use")
25
+
26
+
27
+ def _add_demo_parser(sub: argparse._SubParsersAction) -> None:
28
+ """Register the reproducible end-to-end demonstration."""
29
+ demo = sub.add_parser(
30
+ "demo",
31
+ help="Run the end-to-end ambient sensing demonstration",
32
+ description=(
33
+ "Simulate a synthetic household with visitors, sensor faults and a "
34
+ "known behavioural change, run the full inference pipeline over the "
35
+ "degraded record, and report what was and was not recovered."
36
+ ),
37
+ )
38
+ demo.add_argument("--days", type=int, default=90, help="Days to simulate")
39
+ demo.add_argument("--seed", type=int, default=20240304, help="Random seed")
40
+ demo.add_argument(
41
+ "--step-minutes", type=int, default=10, help="Inference step in minutes"
42
+ )
43
+ demo.add_argument(
44
+ "--output", type=Path, default=None, help="Write structured results as JSON"
45
+ )
46
+
47
+
48
+ def _add_ablation_parser(sub: argparse._SubParsersAction) -> None:
49
+ """Register the paired sensor-ablation experiment."""
50
+ ablate = sub.add_parser(
51
+ "ablate",
52
+ help="Run the paired sensor-ablation experiment",
53
+ description=(
54
+ "Evaluate several sensor configurations on identical simulated "
55
+ "households and report how much each modality contributes."
56
+ ),
57
+ )
58
+ ablate.add_argument("--days", type=int, default=14, help="Days per household")
59
+ ablate.add_argument(
60
+ "--seeds",
61
+ type=int,
62
+ nargs="+",
63
+ default=[11, 22, 33, 44],
64
+ help="Seeds to pair over",
65
+ )
66
+ ablate.add_argument(
67
+ "--step-minutes", type=int, default=10, help="Inference step in minutes"
68
+ )
69
+ ablate.add_argument(
70
+ "--metric",
71
+ default="balanced_accuracy",
72
+ help="Metric to compare configurations on",
73
+ )
74
+ ablate.add_argument(
75
+ "--output", type=Path, default=None, help="Write structured results as JSON"
76
+ )
77
+
78
+
79
+ def _record(
80
+ experiment: str,
81
+ *,
82
+ configuration: dict[str, object],
83
+ seeds: list[int],
84
+ results: dict[str, object],
85
+ notes: list[str] | None = None,
86
+ ) -> object:
87
+ """Wrap a result in the provenance needed to interpret it later."""
88
+ from .evaluation.provenance import ExperimentRecord
89
+
90
+ return ExperimentRecord(
91
+ experiment=experiment,
92
+ configuration=configuration,
93
+ seeds=seeds,
94
+ results=results,
95
+ notes=notes or [],
96
+ )
97
+
98
+
99
+ def _add_attribution_parser(sub: argparse._SubParsersAction) -> None:
100
+ """Register the attribution comparison experiment."""
101
+ study = sub.add_parser(
102
+ "attribution",
103
+ help="Compare naive against occupancy-aware activity attribution",
104
+ description=(
105
+ "Run the same simulated households twice -- once attributing all "
106
+ "ambient activity to the resident, once discounting it by the "
107
+ "probability the resident generated it -- and report the difference."
108
+ ),
109
+ )
110
+ study.add_argument("--days", type=int, default=10, help="Days per scenario")
111
+ study.add_argument("--seed", type=int, default=4242, help="Random seed")
112
+ study.add_argument(
113
+ "--seeds",
114
+ type=int,
115
+ nargs="+",
116
+ default=None,
117
+ help=(
118
+ "Two or more seeds to replicate over. A single seed demonstrates "
119
+ "the mechanism; estimating the effect needs replication, see "
120
+ "docs/SIMULATION_PROTOCOLS.md"
121
+ ),
122
+ )
123
+ study.add_argument(
124
+ "--step-minutes", type=int, default=15, help="Inference step in minutes"
125
+ )
126
+ study.add_argument(
127
+ "--output", type=Path, default=None, help="Write structured results as JSON"
128
+ )
129
+
130
+
131
+ def _run_replicated_attribution(args: argparse.Namespace) -> None:
132
+ """Estimate attribution's effect across independent seeds."""
133
+ from .evaluation.attribution import run_replicated_attribution_study
134
+
135
+ study = run_replicated_attribution_study(
136
+ args.seeds,
137
+ days=args.days,
138
+ step=timedelta(minutes=args.step_minutes),
139
+ )
140
+
141
+ print()
142
+ print(f"Attribution across {len(study.seeds)} paired seeds per scenario")
143
+ print("Intervals are bootstrap; mcse is the Monte Carlo standard error.")
144
+ print()
145
+ print(
146
+ f"{'scenario':<28}{'contam':>8}{'acc gain':>10}"
147
+ f"{'95% CI':>20}{'calib gain':>12}{'visRec':>8}"
148
+ )
149
+ for aggregate in study.aggregates:
150
+ gain = aggregate.balanced_accuracy_gain
151
+ calibration = aggregate.calibration_gain
152
+ interval = f"[{gain.ci_low:+.4f},{gain.ci_high:+.4f}]"
153
+ print(
154
+ f"{aggregate.scenario:<28}"
155
+ f"{aggregate.contaminated_fraction['mean']:>8.2f}"
156
+ f"{gain.mean_difference:>+10.4f}"
157
+ f"{interval:>20}"
158
+ f"{calibration.mean_difference:>+12.4f}"
159
+ f"{aggregate.visitor_recall['mean']:>8.2f}"
160
+ )
161
+ print()
162
+ print(
163
+ "Attribution is not free. Discounting ambient evidence costs accuracy "
164
+ "wherever the discount is wrong, and over 100 seeds the cost is "
165
+ "measurable even in an empty home. Read the calibration column with "
166
+ "the accuracy one: the two do not move together."
167
+ )
168
+
169
+ if args.output is not None:
170
+ _record(
171
+ "attribution_replicated",
172
+ configuration={
173
+ "days": args.days,
174
+ "step_minutes": args.step_minutes,
175
+ "replications": len(study.seeds),
176
+ "scenarios": [a.scenario for a in study.aggregates],
177
+ },
178
+ seeds=list(study.seeds),
179
+ results=study.to_dict(),
180
+ notes=[
181
+ "Both arms see identical households, visitors and sensor "
182
+ "records, so any difference is attributable to attribution.",
183
+ "Report the MCSE with any effect quoted from this artefact.",
184
+ ],
185
+ ).write(
186
+ args.output
187
+ ) # type: ignore[attr-defined]
188
+ print(f"Structured results written to {args.output}")
189
+
190
+
191
+ def _run_attribution(args: argparse.Namespace) -> None:
192
+ """Dispatch the attribution comparison experiment."""
193
+ from .evaluation.attribution import run_attribution_study, standard_scenarios
194
+
195
+ if getattr(args, "seeds", None):
196
+ if len(args.seeds) == 1:
197
+ args.seed = args.seeds[0]
198
+ else:
199
+ _run_replicated_attribution(args)
200
+ return
201
+
202
+ study = run_attribution_study(
203
+ standard_scenarios(days=args.days, seed=args.seed),
204
+ step=timedelta(minutes=args.step_minutes),
205
+ )
206
+
207
+ print("\nNaive against occupancy-aware attribution")
208
+ print("Both arms see identical households, visitors and sensor records.\n")
209
+ print(
210
+ f"{'scenario':<28}{'contam':>8}{'naive':>8}{'aware':>8}"
211
+ f"{'gain':>8}{'calib':>8}{'visF1':>7}"
212
+ )
213
+ for comparison in study.comparisons:
214
+ print(
215
+ f"{comparison.scenario:<28}"
216
+ f"{comparison.contaminated_fraction:>8.3f}"
217
+ f"{comparison.naive.states.balanced_accuracy:>8.3f}"
218
+ f"{comparison.occupancy_aware.states.balanced_accuracy:>8.3f}"
219
+ f"{comparison.balanced_accuracy_gain:>+8.3f}"
220
+ f"{comparison.calibration_gain:>+8.3f}"
221
+ f"{comparison.visitor_detection.f1:>7.2f}"
222
+ )
223
+
224
+ contaminated = study.contaminated
225
+ if contaminated:
226
+ mean_gain = sum(c.balanced_accuracy_gain for c in contaminated) / len(
227
+ contaminated
228
+ )
229
+ print(
230
+ "\nMean balanced-accuracy gain where another person was present: "
231
+ f"{mean_gain:+.4f}"
232
+ )
233
+ print(
234
+ "\nA gain of zero in an uncontaminated scenario is the expected result: "
235
+ "attribution should be a no-op when nobody else is in the home."
236
+ )
237
+
238
+ if args.output is not None:
239
+ _record(
240
+ "attribution_comparison",
241
+ configuration={
242
+ "days": args.days,
243
+ "step_minutes": args.step_minutes,
244
+ "scenarios": [s.scenario for s in study.comparisons],
245
+ },
246
+ seeds=[args.seed],
247
+ results=study.to_dict(),
248
+ notes=[
249
+ "Both arms see identical households, visitors and sensor "
250
+ "records, so any difference is attributable to attribution.",
251
+ ],
252
+ ).write(
253
+ args.output
254
+ ) # type: ignore[attr-defined]
255
+ print(f"Structured results written to {args.output}")
256
+
257
+
258
+ def _run_demo(args: argparse.Namespace) -> None:
259
+ """Dispatch the end-to-end demonstration."""
260
+ from .examples.demos.ambient_pipeline_demo import run_demo
261
+
262
+ run_demo(
263
+ days=args.days,
264
+ seed=args.seed,
265
+ step=timedelta(minutes=args.step_minutes),
266
+ output=args.output,
267
+ )
268
+
269
+
270
+ #: Sensor configurations compared by the ``ablate`` command. Each is a
271
+ #: plausible real deployment rather than an arbitrary subset, so the
272
+ #: comparison answers a question a designer would actually ask.
273
+ ABLATION_SUBSETS: dict[str, tuple[str, ...]] = {
274
+ "all_modalities": (
275
+ "front_door",
276
+ "bedroom_motion",
277
+ "bathroom_motion",
278
+ "kitchen_motion",
279
+ "living_motion",
280
+ "fridge_contact",
281
+ "bed_pressure",
282
+ "living_radar",
283
+ "wearable_motion",
284
+ "resident_beacon",
285
+ ),
286
+ "object_sensors_only": (
287
+ "front_door",
288
+ "bedroom_motion",
289
+ "bathroom_motion",
290
+ "kitchen_motion",
291
+ "living_motion",
292
+ "fridge_contact",
293
+ ),
294
+ "objects_plus_wearable": (
295
+ "front_door",
296
+ "bedroom_motion",
297
+ "bathroom_motion",
298
+ "kitchen_motion",
299
+ "living_motion",
300
+ "fridge_contact",
301
+ "wearable_motion",
302
+ "resident_beacon",
303
+ ),
304
+ "radar_door_bed": ("front_door", "living_radar", "bed_pressure"),
305
+ "radar_door_bed_wearable": (
306
+ "front_door",
307
+ "living_radar",
308
+ "bed_pressure",
309
+ "wearable_motion",
310
+ "resident_beacon",
311
+ ),
312
+ "minimal_door_bed": ("front_door", "bed_pressure"),
313
+ }
314
+
315
+
316
+ def _run_ablation(args: argparse.Namespace) -> None:
317
+ """Dispatch the paired sensor-ablation experiment."""
318
+ from .evaluation.ablation import named_subsets, run_ablation
319
+ from .simulation.household import HouseholdConfig
320
+
321
+ report = run_ablation(
322
+ named_subsets(ABLATION_SUBSETS),
323
+ seeds=args.seeds,
324
+ household=HouseholdConfig(days=args.days),
325
+ step=timedelta(minutes=args.step_minutes),
326
+ )
327
+
328
+ summary = report.summary(args.metric)
329
+ print(f"\nPaired sensor ablation over seeds {report.seeds}")
330
+ print(f"Metric: {args.metric}\n")
331
+ print(
332
+ f"{'configuration':<26}{'sensors':>8}{'mean':>9}{'sd':>8}{'min':>8}{'max':>8}"
333
+ )
334
+ for name, stats in summary.items():
335
+ print(
336
+ f"{name:<26}{int(stats['n_sensors']):>8}{stats['mean']:>9.3f}"
337
+ f"{stats['sd']:>8.3f}{stats['min']:>8.3f}{stats['max']:>8.3f}"
338
+ )
339
+
340
+ reference = next(iter(summary))
341
+ print(f"\nPaired differences against '{reference}' (same households):")
342
+ for name in report.configurations:
343
+ if name == reference:
344
+ continue
345
+ difference = report.compare(reference, name, metric=args.metric)
346
+ verdict = "clear" if difference.excludes_zero else "not distinguishable"
347
+ print(
348
+ f" {reference} - {name:<26} {difference.mean_difference:+.3f} "
349
+ f"95% CI [{difference.ci_low:+.3f}, {difference.ci_high:+.3f}] "
350
+ f"dz={difference.effect_size:+.2f} {verdict}"
351
+ )
352
+
353
+ print(
354
+ "\nDifferences are paired: every configuration was evaluated on the same\n"
355
+ "simulated households, so these compare sensing rather than residents."
356
+ )
357
+ if args.output is not None:
358
+ _record(
359
+ "sensor_ablation",
360
+ configuration={
361
+ "days": args.days,
362
+ "step_minutes": args.step_minutes,
363
+ "metric": args.metric,
364
+ "subsets": {k: list(v) for k, v in ABLATION_SUBSETS.items()},
365
+ },
366
+ seeds=report.seeds,
367
+ results=report.to_dict(args.metric),
368
+ notes=[
369
+ "Configurations are evaluated on identical simulated "
370
+ "households, so differences compare sensing rather than "
371
+ "residents.",
372
+ ],
373
+ ).write(
374
+ args.output
375
+ ) # type: ignore[attr-defined]
376
+ print(f"Structured results written to {args.output}")
377
+
378
+
379
+ def _run_model(args: argparse.Namespace, parser: argparse.ArgumentParser) -> None:
380
+ """Dispatch the single-model fitting commands."""
381
+ dataset = SensorDataset.from_csv(args.data)
382
+ if args.model == "bernoulli-ar":
383
+ model = BernoulliAutoregressiveModel(list(dataset.data.columns), args.target)
384
+ model.fit(dataset)
385
+ elif args.model == "nhpp-pelt":
386
+ nhpp = NHPPPELT(NHPPConfig())
387
+ nhpp.fit(dataset, sensor=args.sensor)
388
+ else: # pragma: no cover - argparse rejects unknown commands first
389
+ parser.print_help()
390
+
391
+
392
+ def main() -> None:
393
+ """Run the sensor modeling command-line interface."""
394
+ parser = argparse.ArgumentParser(description="Sensor modeling CLI")
395
+ sub = parser.add_subparsers(dest="model", required=True)
396
+ _add_model_parsers(sub)
397
+ _add_demo_parser(sub)
398
+ _add_ablation_parser(sub)
399
+ _add_attribution_parser(sub)
400
+
401
+ args = parser.parse_args()
402
+ setup_logging()
403
+ logging.getLogger("sensor_modeling").setLevel(logging.WARNING)
404
+
405
+ if args.model == "demo":
406
+ _run_demo(args)
407
+ elif args.model == "ablate":
408
+ _run_ablation(args)
409
+ elif args.model == "attribution":
410
+ _run_attribution(args)
411
+ else:
412
+ _run_model(args, parser)
413
+
414
+
415
+ if __name__ == "__main__":
416
+ main()
@@ -0,0 +1,33 @@
1
+ """Occupancy, visitors, and uncertainty-aware attribution of activity.
2
+
3
+ Ambient sensors observe a home, not a person. This package estimates who is
4
+ present and converts that into the probability that the monitored resident --
5
+ rather than a visitor or carer -- generated what each sensor saw. It uses
6
+ only anonymous evidence, and deliberately never attempts biometric identity.
7
+ """
8
+
9
+ from .occupancy import (
10
+ CONTEXTS,
11
+ DEFAULT_BEACON_PRESENCE,
12
+ DEFAULT_CONTEXT_DWELL,
13
+ DEFAULT_RESIDENT_SHARE,
14
+ DEFAULT_TRACK_COUNTS,
15
+ ContextConfig,
16
+ ContextEstimate,
17
+ OccupancyContext,
18
+ ResidentContextEstimator,
19
+ rooms_active_at,
20
+ )
21
+
22
+ __all__ = [
23
+ "CONTEXTS",
24
+ "DEFAULT_BEACON_PRESENCE",
25
+ "DEFAULT_CONTEXT_DWELL",
26
+ "DEFAULT_RESIDENT_SHARE",
27
+ "DEFAULT_TRACK_COUNTS",
28
+ "ContextConfig",
29
+ "ContextEstimate",
30
+ "OccupancyContext",
31
+ "ResidentContextEstimator",
32
+ "rooms_active_at",
33
+ ]