margin 0.9.25__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.
- {margin-0.9.25/margin.egg-info → margin-0.9.26}/PKG-INFO +1 -1
- {margin-0.9.25 → margin-0.9.26}/margin/anomaly.py +5 -4
- {margin-0.9.25 → margin-0.9.26}/margin/calibrate.py +3 -1
- {margin-0.9.25 → margin-0.9.26}/margin/streaming.py +2 -1
- {margin-0.9.25 → margin-0.9.26/margin.egg-info}/PKG-INFO +1 -1
- {margin-0.9.25 → margin-0.9.26}/margin.egg-info/SOURCES.txt +1 -0
- {margin-0.9.25 → margin-0.9.26}/pyproject.toml +1 -1
- margin-0.9.26/tests/test_float_tolerance.py +116 -0
- {margin-0.9.25 → margin-0.9.26}/LICENSE +0 -0
- {margin-0.9.25 → margin-0.9.26}/NOTICE +0 -0
- {margin-0.9.25 → margin-0.9.26}/README.md +0 -0
- {margin-0.9.25 → margin-0.9.26}/margin/__init__.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/margin/__main__.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/margin/algebra.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/margin/bridge.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/margin/causal.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/margin/composite.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/margin/confidence.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/margin/config.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/margin/contract.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/margin/controller.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/margin/correlate.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/margin/diff.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/margin/drift.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/margin/events.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/margin/fingerprint.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/margin/forecast.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/margin/health.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/margin/intent.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/margin/ledger.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/margin/loop.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/margin/observation.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/margin/persist.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/margin/policy/__init__.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/margin/policy/compose.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/margin/policy/core.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/margin/policy/temporal.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/margin/policy/trace.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/margin/policy/tuning.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/margin/policy/validate.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/margin/predicates.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/margin/provenance.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/margin/transitions.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/margin/uncertain.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/margin/validity.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/margin.egg-info/dependency_links.txt +0 -0
- {margin-0.9.25 → margin-0.9.26}/margin.egg-info/top_level.txt +0 -0
- {margin-0.9.25 → margin-0.9.26}/setup.cfg +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_absence.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_algebra.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_anomaly.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_bridge.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_calibrate.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_causal.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_composite.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_confidence.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_config.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_contract.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_controller.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_correlate.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_diff.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_drift.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_events.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_fingerprint_ext.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_forecast.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_health.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_healthcare.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_intent.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_ledger.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_loop.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_neuro.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_observation.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_persist.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_pipeline_features.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_policy.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_policy_compose.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_policy_temporal.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_policy_trace.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_policy_tuning.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_policy_validate.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_predicates.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_printer3d.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_provenance.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_reverse_bridge.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_ros2.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_staleness.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_streaming.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_transformer.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_transitions.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_uncertain.py +0 -0
- {margin-0.9.25 → margin-0.9.26}/tests/test_validity.py +0 -0
- {margin-0.9.25 → 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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
@@ -120,7 +120,9 @@ def calibrate(
|
|
|
120
120
|
std = math.sqrt(variance)
|
|
121
121
|
|
|
122
122
|
if use_std:
|
|
123
|
-
|
|
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.")
|
|
@@ -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
|
-
|
|
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.
|
|
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
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "margin"
|
|
7
|
-
version = "0.9.
|
|
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
|
|
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
|