margin 0.9.24__tar.gz → 0.9.26__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 (92) hide show
  1. {margin-0.9.24/margin.egg-info → margin-0.9.26}/PKG-INFO +1 -1
  2. {margin-0.9.24 → margin-0.9.26}/margin/anomaly.py +14 -8
  3. {margin-0.9.24 → margin-0.9.26}/margin/calibrate.py +3 -1
  4. {margin-0.9.24 → margin-0.9.26}/margin/fingerprint.py +5 -2
  5. {margin-0.9.24 → margin-0.9.26}/margin/streaming.py +2 -1
  6. {margin-0.9.24 → margin-0.9.26/margin.egg-info}/PKG-INFO +1 -1
  7. {margin-0.9.24 → margin-0.9.26}/margin.egg-info/SOURCES.txt +1 -0
  8. {margin-0.9.24 → margin-0.9.26}/pyproject.toml +1 -1
  9. margin-0.9.26/tests/test_float_tolerance.py +116 -0
  10. {margin-0.9.24 → margin-0.9.26}/LICENSE +0 -0
  11. {margin-0.9.24 → margin-0.9.26}/NOTICE +0 -0
  12. {margin-0.9.24 → margin-0.9.26}/README.md +0 -0
  13. {margin-0.9.24 → margin-0.9.26}/margin/__init__.py +0 -0
  14. {margin-0.9.24 → margin-0.9.26}/margin/__main__.py +0 -0
  15. {margin-0.9.24 → margin-0.9.26}/margin/algebra.py +0 -0
  16. {margin-0.9.24 → margin-0.9.26}/margin/bridge.py +0 -0
  17. {margin-0.9.24 → margin-0.9.26}/margin/causal.py +0 -0
  18. {margin-0.9.24 → margin-0.9.26}/margin/composite.py +0 -0
  19. {margin-0.9.24 → margin-0.9.26}/margin/confidence.py +0 -0
  20. {margin-0.9.24 → margin-0.9.26}/margin/config.py +0 -0
  21. {margin-0.9.24 → margin-0.9.26}/margin/contract.py +0 -0
  22. {margin-0.9.24 → margin-0.9.26}/margin/controller.py +0 -0
  23. {margin-0.9.24 → margin-0.9.26}/margin/correlate.py +0 -0
  24. {margin-0.9.24 → margin-0.9.26}/margin/diff.py +0 -0
  25. {margin-0.9.24 → margin-0.9.26}/margin/drift.py +0 -0
  26. {margin-0.9.24 → margin-0.9.26}/margin/events.py +0 -0
  27. {margin-0.9.24 → margin-0.9.26}/margin/forecast.py +0 -0
  28. {margin-0.9.24 → margin-0.9.26}/margin/health.py +0 -0
  29. {margin-0.9.24 → margin-0.9.26}/margin/intent.py +0 -0
  30. {margin-0.9.24 → margin-0.9.26}/margin/ledger.py +0 -0
  31. {margin-0.9.24 → margin-0.9.26}/margin/loop.py +0 -0
  32. {margin-0.9.24 → margin-0.9.26}/margin/observation.py +0 -0
  33. {margin-0.9.24 → margin-0.9.26}/margin/persist.py +0 -0
  34. {margin-0.9.24 → margin-0.9.26}/margin/policy/__init__.py +0 -0
  35. {margin-0.9.24 → margin-0.9.26}/margin/policy/compose.py +0 -0
  36. {margin-0.9.24 → margin-0.9.26}/margin/policy/core.py +0 -0
  37. {margin-0.9.24 → margin-0.9.26}/margin/policy/temporal.py +0 -0
  38. {margin-0.9.24 → margin-0.9.26}/margin/policy/trace.py +0 -0
  39. {margin-0.9.24 → margin-0.9.26}/margin/policy/tuning.py +0 -0
  40. {margin-0.9.24 → margin-0.9.26}/margin/policy/validate.py +0 -0
  41. {margin-0.9.24 → margin-0.9.26}/margin/predicates.py +0 -0
  42. {margin-0.9.24 → margin-0.9.26}/margin/provenance.py +0 -0
  43. {margin-0.9.24 → margin-0.9.26}/margin/transitions.py +0 -0
  44. {margin-0.9.24 → margin-0.9.26}/margin/uncertain.py +0 -0
  45. {margin-0.9.24 → margin-0.9.26}/margin/validity.py +0 -0
  46. {margin-0.9.24 → margin-0.9.26}/margin.egg-info/dependency_links.txt +0 -0
  47. {margin-0.9.24 → margin-0.9.26}/margin.egg-info/top_level.txt +0 -0
  48. {margin-0.9.24 → margin-0.9.26}/setup.cfg +0 -0
  49. {margin-0.9.24 → margin-0.9.26}/tests/test_absence.py +0 -0
  50. {margin-0.9.24 → margin-0.9.26}/tests/test_algebra.py +0 -0
  51. {margin-0.9.24 → margin-0.9.26}/tests/test_anomaly.py +0 -0
  52. {margin-0.9.24 → margin-0.9.26}/tests/test_bridge.py +0 -0
  53. {margin-0.9.24 → margin-0.9.26}/tests/test_calibrate.py +0 -0
  54. {margin-0.9.24 → margin-0.9.26}/tests/test_causal.py +0 -0
  55. {margin-0.9.24 → margin-0.9.26}/tests/test_composite.py +0 -0
  56. {margin-0.9.24 → margin-0.9.26}/tests/test_confidence.py +0 -0
  57. {margin-0.9.24 → margin-0.9.26}/tests/test_config.py +0 -0
  58. {margin-0.9.24 → margin-0.9.26}/tests/test_contract.py +0 -0
  59. {margin-0.9.24 → margin-0.9.26}/tests/test_controller.py +0 -0
  60. {margin-0.9.24 → margin-0.9.26}/tests/test_correlate.py +0 -0
  61. {margin-0.9.24 → margin-0.9.26}/tests/test_diff.py +0 -0
  62. {margin-0.9.24 → margin-0.9.26}/tests/test_drift.py +0 -0
  63. {margin-0.9.24 → margin-0.9.26}/tests/test_events.py +0 -0
  64. {margin-0.9.24 → margin-0.9.26}/tests/test_fingerprint_ext.py +0 -0
  65. {margin-0.9.24 → margin-0.9.26}/tests/test_forecast.py +0 -0
  66. {margin-0.9.24 → margin-0.9.26}/tests/test_health.py +0 -0
  67. {margin-0.9.24 → margin-0.9.26}/tests/test_healthcare.py +0 -0
  68. {margin-0.9.24 → margin-0.9.26}/tests/test_intent.py +0 -0
  69. {margin-0.9.24 → margin-0.9.26}/tests/test_ledger.py +0 -0
  70. {margin-0.9.24 → margin-0.9.26}/tests/test_loop.py +0 -0
  71. {margin-0.9.24 → margin-0.9.26}/tests/test_neuro.py +0 -0
  72. {margin-0.9.24 → margin-0.9.26}/tests/test_observation.py +0 -0
  73. {margin-0.9.24 → margin-0.9.26}/tests/test_persist.py +0 -0
  74. {margin-0.9.24 → margin-0.9.26}/tests/test_pipeline_features.py +0 -0
  75. {margin-0.9.24 → margin-0.9.26}/tests/test_policy.py +0 -0
  76. {margin-0.9.24 → margin-0.9.26}/tests/test_policy_compose.py +0 -0
  77. {margin-0.9.24 → margin-0.9.26}/tests/test_policy_temporal.py +0 -0
  78. {margin-0.9.24 → margin-0.9.26}/tests/test_policy_trace.py +0 -0
  79. {margin-0.9.24 → margin-0.9.26}/tests/test_policy_tuning.py +0 -0
  80. {margin-0.9.24 → margin-0.9.26}/tests/test_policy_validate.py +0 -0
  81. {margin-0.9.24 → margin-0.9.26}/tests/test_predicates.py +0 -0
  82. {margin-0.9.24 → margin-0.9.26}/tests/test_printer3d.py +0 -0
  83. {margin-0.9.24 → margin-0.9.26}/tests/test_provenance.py +0 -0
  84. {margin-0.9.24 → margin-0.9.26}/tests/test_reverse_bridge.py +0 -0
  85. {margin-0.9.24 → margin-0.9.26}/tests/test_ros2.py +0 -0
  86. {margin-0.9.24 → margin-0.9.26}/tests/test_staleness.py +0 -0
  87. {margin-0.9.24 → margin-0.9.26}/tests/test_streaming.py +0 -0
  88. {margin-0.9.24 → margin-0.9.26}/tests/test_transformer.py +0 -0
  89. {margin-0.9.24 → margin-0.9.26}/tests/test_transitions.py +0 -0
  90. {margin-0.9.24 → margin-0.9.26}/tests/test_uncertain.py +0 -0
  91. {margin-0.9.24 → margin-0.9.26}/tests/test_validity.py +0 -0
  92. {margin-0.9.24 → margin-0.9.26}/tests/test_windowing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: margin
3
- Version: 0.9.24
3
+ Version: 0.9.26
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
@@ -243,7 +243,7 @@ def _std(xs: list[float], mean: float) -> float:
243
243
 
244
244
  def _kurtosis_excess(xs: list[float], mean: float, std: float) -> float:
245
245
  """Excess kurtosis (0 for normal distribution)."""
246
- if std == 0 or len(xs) < 4:
246
+ if std < 1e-12 or len(xs) < 4:
247
247
  return 0.0
248
248
  n = len(xs)
249
249
  m4 = sum((x - mean) ** 4 for x in xs) / n
@@ -252,7 +252,7 @@ def _kurtosis_excess(xs: list[float], mean: float, std: float) -> float:
252
252
 
253
253
  def _skewness(xs: list[float], mean: float, std: float) -> float:
254
254
  """Skewness (0 for symmetric distribution)."""
255
- if std == 0 or len(xs) < 3:
255
+ if std < 1e-12 or len(xs) < 3:
256
256
  return 0.0
257
257
  n = len(xs)
258
258
  m3 = sum((x - mean) ** 3 for x in xs) / n
@@ -306,8 +306,9 @@ def classify_anomaly(
306
306
  ref_max = max(reference)
307
307
  n = len(reference)
308
308
 
309
- # Z-score
310
- if ref_std > 0:
309
+ # Z-score — 1e-12 tolerance guards against floating-point noise
310
+ # (e.g. sum([0.1]*20)/20 yields std ≈ 1.4e-17, not exactly 0.0)
311
+ if ref_std > 1e-12:
311
312
  z = (value - ref_mean) / ref_std
312
313
  else:
313
314
  # All reference values identical — any deviation is novel
@@ -396,16 +397,21 @@ def check_distribution(
396
397
  rec_mean = _mean(recent)
397
398
  rec_std = _std(recent, rec_mean)
398
399
 
399
- # Mean shift (relative to reference)
400
+ # Mean shift (relative to reference). Tolerances guard against
401
+ # floating-point noise from all-constant inputs (std can be ~1e-17 on
402
+ # some Python versions instead of exactly 0.0).
400
403
  if abs(ref_mean) > 1e-10:
401
404
  mean_shift = abs(rec_mean - ref_mean) / abs(ref_mean)
402
- elif ref_std > 0:
405
+ elif ref_std > 1e-12:
403
406
  mean_shift = abs(rec_mean - ref_mean) / ref_std
404
407
  else:
405
408
  mean_shift = 0.0 if rec_mean == ref_mean else float('inf')
406
409
 
407
- # Std ratio
408
- std_ratio = rec_std / ref_std if ref_std > 0 else (1.0 if rec_std == 0 else float('inf'))
410
+ # Std ratio — treat near-zero std as effectively zero
411
+ if ref_std > 1e-12:
412
+ std_ratio = rec_std / ref_std
413
+ else:
414
+ std_ratio = 1.0 if rec_std < 1e-12 else float('inf')
409
415
 
410
416
  # Shape metrics
411
417
  ref_kurt = _kurtosis_excess(reference, ref_mean, ref_std)
@@ -120,7 +120,9 @@ def calibrate(
120
120
  std = math.sqrt(variance)
121
121
 
122
122
  if use_std:
123
- if std == 0:
123
+ # 1e-12 tolerance catches both true zero and floating-point noise
124
+ # from all-constant inputs (std ≈ 1.4e-17 on some Python versions)
125
+ if std < 1e-12:
124
126
  raise ValueError(
125
127
  "Cannot use use_std=True with zero-variance data — all values are identical. "
126
128
  "Use fraction mode (use_std=False) instead.")
@@ -176,7 +176,10 @@ class Fingerprint(dict):
176
176
  stats = self.get(component, {})
177
177
  mean = stats.get("mean", 0.0)
178
178
  std = stats.get("std", 0.0)
179
- if std != 0.0:
179
+ # Tolerance guard: std accumulated from all-constant values can be 1e-17
180
+ # on some Python versions (floating-point noise), which would otherwise
181
+ # amplify the ratio by 15+ orders of magnitude.
182
+ if std > 1e-12:
180
183
  return (value - mean) / std
181
184
  if mean == 0.0:
182
185
  return value
@@ -199,7 +202,7 @@ class Fingerprint(dict):
199
202
  q25 = self.percentile(component, 25)
200
203
  q75 = self.percentile(component, 75)
201
204
  iqr = q75 - q25
202
- if iqr == 0.0:
205
+ if abs(iqr) < 1e-12:
203
206
  return self.sigma(component, value)
204
207
  return (value - median) / iqr
205
208
 
@@ -795,7 +795,8 @@ class Monitor:
795
795
  z_scores = []
796
796
  for m in _metrics:
797
797
  ref_std = reference_fp.get(m, {}).get("std", 0.0)
798
- if ref_std == 0.0:
798
+ # 1e-12 tolerance: reject floating-point noise from constant windows
799
+ if ref_std < 1e-12:
799
800
  continue
800
801
  cur_mean = current_fp.get(m, {}).get("mean", 0.0)
801
802
  ref_mean = reference_fp.get(m, {}).get("mean", 0.0)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: margin
3
- Version: 0.9.24
3
+ Version: 0.9.26
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
@@ -59,6 +59,7 @@ tests/test_diff.py
59
59
  tests/test_drift.py
60
60
  tests/test_events.py
61
61
  tests/test_fingerprint_ext.py
62
+ tests/test_float_tolerance.py
62
63
  tests/test_forecast.py
63
64
  tests/test_health.py
64
65
  tests/test_healthcare.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "margin"
7
- version = "0.9.24"
7
+ version = "0.9.26"
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"}
@@ -0,0 +1,116 @@
1
+ """
2
+ Regression tests for floating-point tolerance in std-based division.
3
+
4
+ On Python 3.10, sum([0.1]*20)/20 returns 0.10000000000000002 and the
5
+ resulting std accumulates to ~1.4e-17 instead of exactly 0.0. Every
6
+ site that divides by std must treat that as effectively zero or it
7
+ amplifies results by 15+ orders of magnitude.
8
+
9
+ Sites covered:
10
+ Fingerprint.sigma (v0.9.25)
11
+ Fingerprint.robust_sigma (v0.9.25)
12
+ anomaly.check_distribution (v0.9.25)
13
+ anomaly.classify_anomaly (v0.9.26)
14
+ anomaly._kurtosis_excess (v0.9.26)
15
+ anomaly._skewness (v0.9.26)
16
+ Monitor.anomaly_score (v0.9.26)
17
+ calibrate.calibrate (use_std) (v0.9.26)
18
+ """
19
+ import math
20
+ import pytest
21
+
22
+ from margin import (
23
+ Absence, AnomalyState, Confidence, Fingerprint, Monitor, Parser, Thresholds,
24
+ calibrate, check_distribution, classify_anomaly,
25
+ )
26
+
27
+
28
+ CONSTANT = [0.1] * 20 # sum/n = 0.10000000000000002, std ≈ 1.4e-17 on 3.10
29
+
30
+
31
+ def _fp(values: list[float], name: str = "x") -> Fingerprint:
32
+ n = len(values)
33
+ mean = sum(values) / n
34
+ std = math.sqrt(sum((v - mean) ** 2 for v in values) / (n - 1))
35
+ return Fingerprint(
36
+ stats={name: {"mean": mean, "std": std, "n": n, "trend": "STABLE"}},
37
+ values={name: list(values)},
38
+ )
39
+
40
+
41
+ class TestFingerprintSigma:
42
+ def test_constant_window_does_not_amplify(self):
43
+ fp = _fp(CONSTANT)
44
+ # value-mean = 0.05, std accumulated ≈ 1.4e-17 — bogus division would
45
+ # give ~3.5e15. Tolerance guard routes to the |mean| branch instead.
46
+ result = fp.sigma("x", 0.15)
47
+ assert abs(result) < 100 # bounded by real signal, not FP noise
48
+
49
+ def test_constant_window_robust_sigma_does_not_amplify(self):
50
+ fp = _fp(CONSTANT)
51
+ result = fp.robust_sigma("x", 0.15)
52
+ assert abs(result) < 100
53
+
54
+
55
+ class TestAnomalyClassifyAnomaly:
56
+ def test_constant_reference_does_not_saturate(self):
57
+ # value very close to mean should not classify as NOVEL just because
58
+ # dividing by 1e-17 produces a "huge" z-score.
59
+ ac = classify_anomaly(0.15, CONSTANT, component="rr")
60
+ assert ac is not None
61
+ # z-score is either 0 (equal-means branch) or inf (zero-std branch),
62
+ # never a bogus finite value in the 1e15 range.
63
+ assert ac.z_score == 0 or math.isinf(ac.z_score) or abs(ac.z_score) < 1e6
64
+
65
+
66
+ class TestCheckDistribution:
67
+ def test_both_constant_windows_no_inf_std_ratio(self):
68
+ ref = [70.0] * 20
69
+ recent = [70.2] * 10
70
+ ds = check_distribution(recent, ref)
71
+ # Pre-fix, std_ratio was inf. Post-fix, it's 1.0 (both effectively zero-std).
72
+ assert math.isfinite(ds.std_ratio) or ds.std_ratio == 1.0
73
+
74
+ def test_stable_calibration_no_recalibration(self):
75
+ from margin import needs_recalibration_many
76
+ cal = {"cpu": [50.0] * 20, "mem": [70.0] * 20}
77
+ recent = {"cpu": [50.5] * 10, "mem": [70.2] * 10}
78
+ flags = needs_recalibration_many(cal, recent)
79
+ # Small shifts on constant data must NOT trigger recalibration.
80
+ assert flags["cpu"] is False
81
+ assert flags["mem"] is False
82
+
83
+
84
+ class TestMonitorAnomalyScore:
85
+ def test_constant_reference_fp_skips_noise_metrics(self):
86
+ parser = Parser(baselines={"rr": 0.1}, thresholds=Thresholds(intact=0.8, ablated=0.2))
87
+ m = Monitor(parser, window=50)
88
+ for _ in range(20):
89
+ m.update({"rr": 0.1})
90
+ ref_fp = m.fingerprint()
91
+ # Feed slightly different value, score should be small, not 1e15.
92
+ for _ in range(20):
93
+ m.update({"rr": 0.11})
94
+ score = m.anomaly_score(ref_fp)
95
+ assert math.isfinite(score)
96
+ assert score < 1e6
97
+
98
+
99
+ class TestCalibrateUseStd:
100
+ def test_constant_input_raises_clear_error(self):
101
+ # Pre-fix: std = 1.4e-17 passed the `std == 0` check, producing
102
+ # thresholds that were essentially baseline ± 1e-17 — all values
103
+ # classified INTACT. Post-fix: raises the same clear error as true zero.
104
+ with pytest.raises(ValueError, match="zero-variance"):
105
+ calibrate(CONSTANT, use_std=True)
106
+
107
+
108
+ class TestSkewKurtosis:
109
+ def test_constant_input_kurtosis_is_zero(self):
110
+ from margin.anomaly import _kurtosis_excess, _skewness, _mean, _std
111
+ xs = CONSTANT
112
+ mean = _mean(xs)
113
+ std = _std(xs, mean)
114
+ # Pre-fix these would divide by ~1e-17 and return nonsense.
115
+ assert _kurtosis_excess(xs, mean, std) == 0.0
116
+ assert _skewness(xs, mean, std) == 0.0
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