margin 0.8.2__tar.gz → 0.9.0__tar.gz

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.

Potentially problematic release.


This version of margin might be problematic. Click here for more details.

Files changed (85) hide show
  1. {margin-0.8.2/margin.egg-info → margin-0.9.0}/PKG-INFO +1 -1
  2. {margin-0.8.2 → margin-0.9.0}/margin/__init__.py +5 -2
  3. {margin-0.8.2 → margin-0.9.0}/margin/calibrate.py +135 -0
  4. {margin-0.8.2 → margin-0.9.0}/margin/observation.py +15 -2
  5. {margin-0.8.2 → margin-0.9.0}/margin/persist.py +41 -8
  6. {margin-0.8.2 → margin-0.9.0}/margin/streaming.py +38 -6
  7. {margin-0.8.2 → margin-0.9.0/margin.egg-info}/PKG-INFO +1 -1
  8. {margin-0.8.2 → margin-0.9.0}/pyproject.toml +1 -1
  9. {margin-0.8.2 → margin-0.9.0}/LICENSE +0 -0
  10. {margin-0.8.2 → margin-0.9.0}/NOTICE +0 -0
  11. {margin-0.8.2 → margin-0.9.0}/README.md +0 -0
  12. {margin-0.8.2 → margin-0.9.0}/margin/__main__.py +0 -0
  13. {margin-0.8.2 → margin-0.9.0}/margin/algebra.py +0 -0
  14. {margin-0.8.2 → margin-0.9.0}/margin/anomaly.py +0 -0
  15. {margin-0.8.2 → margin-0.9.0}/margin/bridge.py +0 -0
  16. {margin-0.8.2 → margin-0.9.0}/margin/causal.py +0 -0
  17. {margin-0.8.2 → margin-0.9.0}/margin/composite.py +0 -0
  18. {margin-0.8.2 → margin-0.9.0}/margin/confidence.py +0 -0
  19. {margin-0.8.2 → margin-0.9.0}/margin/config.py +0 -0
  20. {margin-0.8.2 → margin-0.9.0}/margin/contract.py +0 -0
  21. {margin-0.8.2 → margin-0.9.0}/margin/correlate.py +0 -0
  22. {margin-0.8.2 → margin-0.9.0}/margin/diff.py +0 -0
  23. {margin-0.8.2 → margin-0.9.0}/margin/drift.py +0 -0
  24. {margin-0.8.2 → margin-0.9.0}/margin/events.py +0 -0
  25. {margin-0.8.2 → margin-0.9.0}/margin/forecast.py +0 -0
  26. {margin-0.8.2 → margin-0.9.0}/margin/health.py +0 -0
  27. {margin-0.8.2 → margin-0.9.0}/margin/intent.py +0 -0
  28. {margin-0.8.2 → margin-0.9.0}/margin/ledger.py +0 -0
  29. {margin-0.8.2 → margin-0.9.0}/margin/loop.py +0 -0
  30. {margin-0.8.2 → margin-0.9.0}/margin/policy/__init__.py +0 -0
  31. {margin-0.8.2 → margin-0.9.0}/margin/policy/compose.py +0 -0
  32. {margin-0.8.2 → margin-0.9.0}/margin/policy/core.py +0 -0
  33. {margin-0.8.2 → margin-0.9.0}/margin/policy/temporal.py +0 -0
  34. {margin-0.8.2 → margin-0.9.0}/margin/policy/trace.py +0 -0
  35. {margin-0.8.2 → margin-0.9.0}/margin/policy/tuning.py +0 -0
  36. {margin-0.8.2 → margin-0.9.0}/margin/policy/validate.py +0 -0
  37. {margin-0.8.2 → margin-0.9.0}/margin/predicates.py +0 -0
  38. {margin-0.8.2 → margin-0.9.0}/margin/provenance.py +0 -0
  39. {margin-0.8.2 → margin-0.9.0}/margin/transitions.py +0 -0
  40. {margin-0.8.2 → margin-0.9.0}/margin/uncertain.py +0 -0
  41. {margin-0.8.2 → margin-0.9.0}/margin/validity.py +0 -0
  42. {margin-0.8.2 → margin-0.9.0}/margin.egg-info/SOURCES.txt +0 -0
  43. {margin-0.8.2 → margin-0.9.0}/margin.egg-info/dependency_links.txt +0 -0
  44. {margin-0.8.2 → margin-0.9.0}/margin.egg-info/top_level.txt +0 -0
  45. {margin-0.8.2 → margin-0.9.0}/setup.cfg +0 -0
  46. {margin-0.8.2 → margin-0.9.0}/tests/test_algebra.py +0 -0
  47. {margin-0.8.2 → margin-0.9.0}/tests/test_anomaly.py +0 -0
  48. {margin-0.8.2 → margin-0.9.0}/tests/test_bridge.py +0 -0
  49. {margin-0.8.2 → margin-0.9.0}/tests/test_calibrate.py +0 -0
  50. {margin-0.8.2 → margin-0.9.0}/tests/test_causal.py +0 -0
  51. {margin-0.8.2 → margin-0.9.0}/tests/test_composite.py +0 -0
  52. {margin-0.8.2 → margin-0.9.0}/tests/test_confidence.py +0 -0
  53. {margin-0.8.2 → margin-0.9.0}/tests/test_config.py +0 -0
  54. {margin-0.8.2 → margin-0.9.0}/tests/test_contract.py +0 -0
  55. {margin-0.8.2 → margin-0.9.0}/tests/test_correlate.py +0 -0
  56. {margin-0.8.2 → margin-0.9.0}/tests/test_diff.py +0 -0
  57. {margin-0.8.2 → margin-0.9.0}/tests/test_drift.py +0 -0
  58. {margin-0.8.2 → margin-0.9.0}/tests/test_events.py +0 -0
  59. {margin-0.8.2 → margin-0.9.0}/tests/test_forecast.py +0 -0
  60. {margin-0.8.2 → margin-0.9.0}/tests/test_health.py +0 -0
  61. {margin-0.8.2 → margin-0.9.0}/tests/test_healthcare.py +0 -0
  62. {margin-0.8.2 → margin-0.9.0}/tests/test_intent.py +0 -0
  63. {margin-0.8.2 → margin-0.9.0}/tests/test_ledger.py +0 -0
  64. {margin-0.8.2 → margin-0.9.0}/tests/test_loop.py +0 -0
  65. {margin-0.8.2 → margin-0.9.0}/tests/test_neuro.py +0 -0
  66. {margin-0.8.2 → margin-0.9.0}/tests/test_observation.py +0 -0
  67. {margin-0.8.2 → margin-0.9.0}/tests/test_persist.py +0 -0
  68. {margin-0.8.2 → margin-0.9.0}/tests/test_policy.py +0 -0
  69. {margin-0.8.2 → margin-0.9.0}/tests/test_policy_compose.py +0 -0
  70. {margin-0.8.2 → margin-0.9.0}/tests/test_policy_temporal.py +0 -0
  71. {margin-0.8.2 → margin-0.9.0}/tests/test_policy_trace.py +0 -0
  72. {margin-0.8.2 → margin-0.9.0}/tests/test_policy_tuning.py +0 -0
  73. {margin-0.8.2 → margin-0.9.0}/tests/test_policy_validate.py +0 -0
  74. {margin-0.8.2 → margin-0.9.0}/tests/test_predicates.py +0 -0
  75. {margin-0.8.2 → margin-0.9.0}/tests/test_printer3d.py +0 -0
  76. {margin-0.8.2 → margin-0.9.0}/tests/test_provenance.py +0 -0
  77. {margin-0.8.2 → margin-0.9.0}/tests/test_reverse_bridge.py +0 -0
  78. {margin-0.8.2 → margin-0.9.0}/tests/test_ros2.py +0 -0
  79. {margin-0.8.2 → margin-0.9.0}/tests/test_staleness.py +0 -0
  80. {margin-0.8.2 → margin-0.9.0}/tests/test_streaming.py +0 -0
  81. {margin-0.8.2 → margin-0.9.0}/tests/test_transformer.py +0 -0
  82. {margin-0.8.2 → margin-0.9.0}/tests/test_transitions.py +0 -0
  83. {margin-0.8.2 → margin-0.9.0}/tests/test_uncertain.py +0 -0
  84. {margin-0.8.2 → margin-0.9.0}/tests/test_validity.py +0 -0
  85. {margin-0.8.2 → margin-0.9.0}/tests/test_windowing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: margin
3
- Version: 0.8.2
3
+ Version: 0.9.0
4
4
  Summary: Typed health classification, uncertainty algebra, and correction auditing for any system that measures things and needs to explain what happened.
5
5
  Author: Cope Labs LLC
6
6
  License: MIT
@@ -29,7 +29,10 @@ from .ledger import Record, Ledger
29
29
 
30
30
  # Observability
31
31
  from .bridge import observe, observe_many, delta, to_uncertain
32
- from .calibrate import CalibrationResult, calibrate, calibrate_many, parser_from_calibration
32
+ from .calibrate import (
33
+ CalibrationResult, calibrate, calibrate_many,
34
+ parser_from_calibration, recalibrate_parser, needs_recalibration,
35
+ )
33
36
  from .composite import CompositeObservation, AggregateStrategy
34
37
  from .diff import ComponentChange, Diff, diff
35
38
  from .events import EventBus
@@ -95,7 +98,7 @@ from .causal import (
95
98
  )
96
99
 
97
100
  # Streaming
98
- from .streaming import DriftTracker, AnomalyTracker, CorrelationTracker, Monitor
101
+ from .streaming import DriftTracker, AnomalyTracker, CorrelationTracker, Monitor, WindowConfig
99
102
 
100
103
  # Config
101
104
  from .config import from_config, load_config
@@ -3,6 +3,9 @@ Threshold derivation from historical data.
3
3
 
4
4
  Takes a set of "known healthy" measurements and derives baselines and
5
5
  thresholds automatically.
6
+
7
+ Also provides runtime recalibration: detect when a baseline has drifted
8
+ and construct a replacement Parser with updated baselines/thresholds.
6
9
  """
7
10
 
8
11
  from __future__ import annotations
@@ -151,6 +154,138 @@ def calibrate_many(
151
154
  return baselines, thresholds
152
155
 
153
156
 
157
+ def needs_recalibration(
158
+ calibration_samples: list[float],
159
+ recent_samples: list[float],
160
+ mean_shift_threshold: float = 0.2,
161
+ std_ratio_high: float = 2.0,
162
+ std_ratio_low: float = 0.5,
163
+ min_recent: int = 5,
164
+ ) -> bool:
165
+ """
166
+ Return True if recent values suggest the baseline has drifted.
167
+
168
+ Uses distribution comparison between calibration-time healthy data and
169
+ a recent window to detect two signals:
170
+
171
+ Signal A — mean shift: the operating centre has moved by more than
172
+ `mean_shift_threshold` (relative fraction, default 20%).
173
+
174
+ Signal B — spread change: variance has doubled (`std_ratio > 2.0`)
175
+ or halved (`std_ratio < 0.5`) relative to the calibration period.
176
+
177
+ Returns True when either signal fires and there are enough recent samples.
178
+ A third signal (AnomalyState.ANOMALOUS at healthy sigma) must be checked
179
+ externally via AnomalyTracker — see `needs_recalibration` docs.
180
+
181
+ Args:
182
+ calibration_samples: original healthy measurements used to build the Parser
183
+ recent_samples: recent window of observed values
184
+ mean_shift_threshold: relative mean shift that triggers recalibration (default 0.2)
185
+ std_ratio_high: std ratio above which spread is considered changed (default 2.0)
186
+ std_ratio_low: std ratio below which spread is considered changed (default 0.5)
187
+ min_recent: minimum recent samples required to make a judgment (default 5)
188
+ """
189
+ if len(recent_samples) < min_recent or len(calibration_samples) < 2:
190
+ return False
191
+
192
+ from .anomaly import check_distribution
193
+ ds = check_distribution(recent_samples, calibration_samples)
194
+
195
+ mean_shifted = abs(ds.mean_shift) > mean_shift_threshold
196
+ spread_changed = ds.std_ratio > std_ratio_high or ds.std_ratio < std_ratio_low
197
+
198
+ return mean_shifted or spread_changed
199
+
200
+
201
+ def recalibrate_parser(
202
+ parser: Parser,
203
+ new_healthy_data: dict[str, list[float]],
204
+ polarities: Optional[dict[str, bool]] = None,
205
+ components: Optional[list[str]] = None,
206
+ intact_fraction: float = 0.70,
207
+ ablated_fraction: float = 0.30,
208
+ active_min: float = 0.05,
209
+ ) -> tuple[Parser, dict[str, CalibrationResult]]:
210
+ """
211
+ Return a new Parser with updated baselines for components whose healthy
212
+ operating range has drifted.
213
+
214
+ Does NOT mutate the input parser — returns a replacement. Components not
215
+ in `new_healthy_data` (or not in `components`) keep their existing
216
+ baselines and thresholds unchanged.
217
+
218
+ Use after `needs_recalibration()` signals that a component's baseline
219
+ is stale, or when you have a new labeled window of known-healthy data.
220
+
221
+ Args:
222
+ parser: the existing Parser to replace
223
+ new_healthy_data: {component: [new_healthy_measurements]}
224
+ polarities: {component: higher_is_better} overrides (defaults
225
+ to existing parser polarity for each component)
226
+ components: allowlist of components to recalibrate; if None,
227
+ all components in new_healthy_data are recalibrated
228
+ intact_fraction: fraction of baseline for intact threshold
229
+ ablated_fraction: fraction of baseline for ablated threshold
230
+ active_min: minimum correction magnitude
231
+
232
+ Returns:
233
+ (new_parser, results) where results is {component: CalibrationResult}
234
+ for each recalibrated component.
235
+
236
+ Transition notes:
237
+ - Historical Ledger Observations remain anchored to the baseline that was
238
+ active when they were recorded. Recalibration does not rewrite history.
239
+ - After recalibrating, rebuild Monitor from the new Parser:
240
+ save_monitor(old_monitor, "checkpoint.json") # optional audit trail
241
+ new_monitor = Monitor(new_parser, drift_window=..., anomaly_window=...,
242
+ correlation_window=...)
243
+ - Per-tracker windows should match the old Monitor's windows for continuity.
244
+ """
245
+ polarities = polarities or {}
246
+ to_recalibrate = set(components) if components else set(new_healthy_data.keys())
247
+
248
+ results: dict[str, CalibrationResult] = {}
249
+ new_baselines: dict[str, float] = {}
250
+ new_component_thresholds: dict[str, Thresholds] = {}
251
+
252
+ for name in to_recalibrate:
253
+ if name not in new_healthy_data:
254
+ continue
255
+ vals = new_healthy_data[name]
256
+ # Resolve polarity: explicit override → existing parser polarity → True
257
+ existing_t = parser.component_thresholds.get(name) or parser.thresholds
258
+ hib = polarities.get(name, existing_t.higher_is_better)
259
+ result = calibrate(vals, hib, intact_fraction, ablated_fraction, active_min)
260
+ results[name] = result
261
+ new_baselines[name] = result.baseline
262
+ new_component_thresholds[name] = result.thresholds
263
+
264
+ # Merge: new overrides old; unrecalibrated components are preserved verbatim
265
+ merged_baselines = {**parser.baselines, **new_baselines}
266
+ merged_component_thresholds = {**parser.component_thresholds, **new_component_thresholds}
267
+
268
+ # Default thresholds: recalibrate if the first component was recalibrated,
269
+ # otherwise keep the existing default.
270
+ first_name = list(parser.baselines.keys())[0] if parser.baselines else None
271
+ if first_name and first_name in new_component_thresholds:
272
+ default_thresholds = new_component_thresholds.pop(first_name)
273
+ merged_component_thresholds.pop(first_name, None)
274
+ else:
275
+ default_thresholds = parser.thresholds
276
+ # Remove first component from component_thresholds if it was there
277
+ if first_name:
278
+ merged_component_thresholds.pop(first_name, None)
279
+
280
+ new_parser = Parser(
281
+ baselines=merged_baselines,
282
+ thresholds=default_thresholds,
283
+ component_thresholds=merged_component_thresholds,
284
+ )
285
+
286
+ return new_parser, results
287
+
288
+
154
289
  def parser_from_calibration(
155
290
  component_values: dict[str, list[float]],
156
291
  polarities: Optional[dict[str, bool]] = None,
@@ -10,6 +10,7 @@ Expression: composed snapshot — all observations + corrections at one moment
10
10
  from __future__ import annotations
11
11
 
12
12
  import json
13
+ import warnings
13
14
  from dataclasses import dataclass, field
14
15
  from datetime import datetime
15
16
  from enum import Enum
@@ -161,8 +162,8 @@ class Correction:
161
162
  return {
162
163
  "target": self.target,
163
164
  "op": self.op.value,
164
- "alpha": round(self.alpha, 4),
165
- "magnitude": round(self.magnitude, 4),
165
+ "alpha": self.alpha,
166
+ "magnitude": self.magnitude,
166
167
  "triggered_by": self.triggered_by,
167
168
  "provenance": self.provenance,
168
169
  }
@@ -198,6 +199,18 @@ class Expression:
198
199
  label: str = ""
199
200
  step: Optional[int] = None
200
201
 
202
+ def __post_init__(self) -> None:
203
+ if self.observations:
204
+ weakest = min(o.confidence for o in self.observations)
205
+ if self.confidence > weakest:
206
+ warnings.warn(
207
+ f"Expression.confidence ({self.confidence.value}) exceeds the weakest "
208
+ f"observation confidence ({weakest.value}). This overclaims certainty "
209
+ "and will cause min_confidence policy gates to fire incorrectly. "
210
+ "Use Parser.parse() to construct Expressions with correct net confidence.",
211
+ stacklevel=2,
212
+ )
213
+
201
214
  def health_of(self, name: str) -> Optional[Health]:
202
215
  for o in self.observations:
203
216
  if o.name == name:
@@ -25,6 +25,9 @@ def save_monitor(monitor: Monitor, path: str) -> None:
25
25
  state = {
26
26
  "step": monitor.step,
27
27
  "window": monitor.window,
28
+ "drift_window": monitor.drift_window,
29
+ "anomaly_window": monitor.anomaly_window,
30
+ "correlation_window": monitor.correlation_window,
28
31
  "drift": {},
29
32
  "anomaly": {},
30
33
  "correlation": {
@@ -59,7 +62,16 @@ def load_monitor(path: str, parser: Parser, **kwargs) -> Monitor:
59
62
  data = json.loads(Path(path).read_text())
60
63
 
61
64
  window = data.get("window", kwargs.pop("window", 100))
62
- monitor = Monitor(parser, window=window, **kwargs)
65
+ drift_window = data.get("drift_window", kwargs.pop("drift_window", None))
66
+ anomaly_window = data.get("anomaly_window", kwargs.pop("anomaly_window", None))
67
+ correlation_window = data.get("correlation_window", kwargs.pop("correlation_window", None))
68
+ monitor = Monitor(
69
+ parser, window=window,
70
+ drift_window=drift_window,
71
+ anomaly_window=anomaly_window,
72
+ correlation_window=correlation_window,
73
+ **kwargs,
74
+ )
63
75
  monitor._step = data.get("step", 0)
64
76
 
65
77
  # Restore drift tracker observations
@@ -97,24 +109,36 @@ def replay(
97
109
  data: list[dict[str, float]],
98
110
  timestamps: Optional[list[datetime]] = None,
99
111
  window: int = 100,
112
+ drift_window: Optional[int] = None,
113
+ anomaly_window: Optional[int] = None,
114
+ correlation_window: Optional[int] = None,
100
115
  **kwargs,
101
116
  ) -> tuple[Monitor, list[dict]]:
102
117
  """
103
118
  Replay historical data through a Monitor.
104
119
 
105
120
  Args:
106
- parser: Parser for health classification
107
- data: list of {component: value} dicts, one per step
108
- timestamps: optional timestamps per step (defaults to 1-second intervals)
109
- window: tracker window size
110
- **kwargs: passed to Monitor
121
+ parser: Parser for health classification
122
+ data: list of {component: value} dicts, one per step
123
+ timestamps: optional timestamps per step (defaults to 1-second intervals)
124
+ window: base tracker window size (all trackers inherit if per-tracker not set)
125
+ drift_window: override window for DriftTracker
126
+ anomaly_window: override window for AnomalyTracker
127
+ correlation_window: override window for CorrelationTracker
128
+ **kwargs: passed to Monitor
111
129
 
112
130
  Returns (monitor, snapshots) where snapshots is a list of status dicts,
113
131
  one per step.
114
132
  """
115
133
  from datetime import timedelta
116
134
 
117
- monitor = Monitor(parser, window=window, **kwargs)
135
+ monitor = Monitor(
136
+ parser, window=window,
137
+ drift_window=drift_window,
138
+ anomaly_window=anomaly_window,
139
+ correlation_window=correlation_window,
140
+ **kwargs,
141
+ )
118
142
  snapshots = []
119
143
 
120
144
  t0 = timestamps[0] if timestamps else datetime(2000, 1, 1)
@@ -132,6 +156,9 @@ def replay_csv(
132
156
  path: str,
133
157
  timestamp_column: Optional[str] = None,
134
158
  window: int = 100,
159
+ drift_window: Optional[int] = None,
160
+ anomaly_window: Optional[int] = None,
161
+ correlation_window: Optional[int] = None,
135
162
  **kwargs,
136
163
  ) -> tuple[Monitor, list[dict]]:
137
164
  """
@@ -166,4 +193,10 @@ def replay_csv(
166
193
  if values:
167
194
  rows.append(values)
168
195
 
169
- return replay(parser, rows, timestamps or None, window, **kwargs)
196
+ return replay(
197
+ parser, rows, timestamps or None, window,
198
+ drift_window=drift_window,
199
+ anomaly_window=anomaly_window,
200
+ correlation_window=correlation_window,
201
+ **kwargs,
202
+ )
@@ -16,6 +16,23 @@ from dataclasses import dataclass, field
16
16
  from datetime import datetime
17
17
  from typing import Optional
18
18
 
19
+
20
+ @dataclass
21
+ class WindowConfig:
22
+ """
23
+ Per-concern window sizes for Monitor.
24
+
25
+ drift: window for DriftTracker (how many steps to classify trajectory)
26
+ anomaly: window for AnomalyTracker (reference window for outlier detection)
27
+ correlation: window for CorrelationTracker (how many steps for pairwise correlation)
28
+
29
+ All default to None, meaning "inherit the Monitor's base `window` parameter."
30
+ Rule of thumb: anomaly ≈ 4× drift, correlation ≈ 10× drift.
31
+ """
32
+ drift: Optional[int] = None
33
+ anomaly: Optional[int] = None
34
+ correlation: Optional[int] = None
35
+
19
36
  from .confidence import Confidence
20
37
  from .health import Health, Thresholds, classify
21
38
  from .observation import Observation, Expression
@@ -340,11 +357,22 @@ class Monitor:
340
357
  self,
341
358
  parser,
342
359
  window: int = 100,
360
+ drift_window: Optional[int] = None,
361
+ anomaly_window: Optional[int] = None,
362
+ correlation_window: Optional[int] = None,
363
+ window_config: Optional[WindowConfig] = None,
343
364
  min_correlation: float = 0.7,
344
365
  anomaly_min_reference: int = 10,
345
366
  ):
346
367
  self.parser = parser
347
368
  self.window = window
369
+
370
+ # Resolve per-tracker windows: window_config > named params > base window
371
+ cfg = window_config or WindowConfig()
372
+ self.drift_window = cfg.drift or drift_window or window
373
+ self.anomaly_window = cfg.anomaly or anomaly_window or window
374
+ self.correlation_window = cfg.correlation or correlation_window or window
375
+
348
376
  self._expression: Optional[Expression] = None
349
377
  self._step = 0
350
378
 
@@ -352,17 +380,16 @@ class Monitor:
352
380
  self._drift_trackers: dict[str, DriftTracker] = {}
353
381
  self._anomaly_trackers: dict[str, AnomalyTracker] = {}
354
382
 
355
- for name, baseline in parser.baselines.items():
356
- thresholds = parser._thresholds_for(name)
357
- self._drift_trackers[name] = DriftTracker(name, window=window)
383
+ for name in parser.baselines:
384
+ self._drift_trackers[name] = DriftTracker(name, window=self.drift_window)
358
385
  self._anomaly_trackers[name] = AnomalyTracker(
359
- name, window=window, min_reference=anomaly_min_reference,
386
+ name, window=self.anomaly_window, min_reference=anomaly_min_reference,
360
387
  )
361
388
 
362
389
  # Correlation tracker across all components
363
390
  self._correlation_tracker = CorrelationTracker(
364
391
  list(parser.baselines.keys()),
365
- window=window,
392
+ window=self.correlation_window,
366
393
  min_correlation=min_correlation,
367
394
  )
368
395
 
@@ -455,4 +482,9 @@ class Monitor:
455
482
  self._correlation_tracker.reset()
456
483
 
457
484
  def __repr__(self) -> str:
458
- return f"Monitor(step={self._step}, {len(self._drift_trackers)} components)"
485
+ windows_uniform = (self.drift_window == self.anomaly_window == self.correlation_window)
486
+ if windows_uniform:
487
+ w = f"window={self.drift_window}"
488
+ else:
489
+ w = f"drift={self.drift_window}, anomaly={self.anomaly_window}, corr={self.correlation_window}"
490
+ return f"Monitor(step={self._step}, {len(self._drift_trackers)} components, {w})"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: margin
3
- Version: 0.8.2
3
+ Version: 0.9.0
4
4
  Summary: Typed health classification, uncertainty algebra, and correction auditing for any system that measures things and needs to explain what happened.
5
5
  Author: Cope Labs LLC
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "margin"
7
- version = "0.8.2"
7
+ version = "0.9.0"
8
8
  description = "Typed health classification, uncertainty algebra, and correction auditing for any system that measures things and needs to explain what happened."
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes