margin 0.9.16__tar.gz → 0.9.18__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.16/margin.egg-info → margin-0.9.18}/PKG-INFO +1 -1
- {margin-0.9.16 → margin-0.9.18}/margin/fingerprint.py +34 -12
- {margin-0.9.16 → margin-0.9.18}/margin/streaming.py +4 -1
- {margin-0.9.16 → margin-0.9.18/margin.egg-info}/PKG-INFO +1 -1
- {margin-0.9.16 → margin-0.9.18}/pyproject.toml +1 -1
- {margin-0.9.16 → margin-0.9.18}/tests/test_controller.py +88 -2
- {margin-0.9.16 → margin-0.9.18}/LICENSE +0 -0
- {margin-0.9.16 → margin-0.9.18}/NOTICE +0 -0
- {margin-0.9.16 → margin-0.9.18}/README.md +0 -0
- {margin-0.9.16 → margin-0.9.18}/margin/__init__.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/margin/__main__.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/margin/algebra.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/margin/anomaly.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/margin/bridge.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/margin/calibrate.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/margin/causal.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/margin/composite.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/margin/confidence.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/margin/config.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/margin/contract.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/margin/controller.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/margin/correlate.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/margin/diff.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/margin/drift.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/margin/events.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/margin/forecast.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/margin/health.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/margin/intent.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/margin/ledger.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/margin/loop.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/margin/observation.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/margin/persist.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/margin/policy/__init__.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/margin/policy/compose.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/margin/policy/core.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/margin/policy/temporal.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/margin/policy/trace.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/margin/policy/tuning.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/margin/policy/validate.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/margin/predicates.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/margin/provenance.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/margin/transitions.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/margin/uncertain.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/margin/validity.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/margin.egg-info/SOURCES.txt +0 -0
- {margin-0.9.16 → margin-0.9.18}/margin.egg-info/dependency_links.txt +0 -0
- {margin-0.9.16 → margin-0.9.18}/margin.egg-info/top_level.txt +0 -0
- {margin-0.9.16 → margin-0.9.18}/setup.cfg +0 -0
- {margin-0.9.16 → margin-0.9.18}/tests/test_algebra.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/tests/test_anomaly.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/tests/test_bridge.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/tests/test_calibrate.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/tests/test_causal.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/tests/test_composite.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/tests/test_confidence.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/tests/test_config.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/tests/test_contract.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/tests/test_correlate.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/tests/test_diff.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/tests/test_drift.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/tests/test_events.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/tests/test_forecast.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/tests/test_health.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/tests/test_healthcare.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/tests/test_intent.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/tests/test_ledger.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/tests/test_loop.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/tests/test_neuro.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/tests/test_observation.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/tests/test_persist.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/tests/test_policy.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/tests/test_policy_compose.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/tests/test_policy_temporal.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/tests/test_policy_trace.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/tests/test_policy_tuning.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/tests/test_policy_validate.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/tests/test_predicates.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/tests/test_printer3d.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/tests/test_provenance.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/tests/test_reverse_bridge.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/tests/test_ros2.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/tests/test_staleness.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/tests/test_streaming.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/tests/test_transformer.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/tests/test_transitions.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/tests/test_uncertain.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/tests/test_validity.py +0 -0
- {margin-0.9.16 → margin-0.9.18}/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.18
|
|
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
|
|
@@ -87,11 +87,16 @@ class Fingerprint(dict):
|
|
|
87
87
|
``method="trimmed"`` — 10% trimmed mean
|
|
88
88
|
``method="mean"`` — plain mean (same as ``fp[component]["mean"]``)
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
When no raw values are stored, checks for pre-computed ``"median"`` in
|
|
91
|
+
the stats dict (present when fingerprint came from ``Monitor.fingerprint()``),
|
|
92
|
+
then falls back to ``"mean"``.
|
|
91
93
|
"""
|
|
92
94
|
vals = self._values.get(component)
|
|
93
95
|
if not vals:
|
|
94
|
-
|
|
96
|
+
stats = self.get(component, {})
|
|
97
|
+
if method == "median":
|
|
98
|
+
return stats.get("median", stats.get("mean", 0.0))
|
|
99
|
+
return stats.get("mean", 0.0)
|
|
95
100
|
if method == "median":
|
|
96
101
|
return _percentile(vals, 50)
|
|
97
102
|
if method == "trimmed":
|
|
@@ -103,32 +108,49 @@ class Fingerprint(dict):
|
|
|
103
108
|
"""
|
|
104
109
|
Return the p-th percentile (0–100) of observed values for ``component``.
|
|
105
110
|
|
|
106
|
-
|
|
111
|
+
When no raw values are stored, checks for pre-computed ``"q25"`` / ``"q75"`` /
|
|
112
|
+
``"median"`` in the stats dict (present when fingerprint came from
|
|
113
|
+
``Monitor.fingerprint()``), then falls back to ``"mean"``.
|
|
107
114
|
"""
|
|
108
115
|
vals = self._values.get(component)
|
|
109
116
|
if not vals:
|
|
110
|
-
|
|
117
|
+
stats = self.get(component, {})
|
|
118
|
+
if p == 25 and "q25" in stats:
|
|
119
|
+
return stats["q25"]
|
|
120
|
+
if p == 75 and "q75" in stats:
|
|
121
|
+
return stats["q75"]
|
|
122
|
+
if p == 50 and "median" in stats:
|
|
123
|
+
return stats["median"]
|
|
124
|
+
return stats.get("mean", 0.0)
|
|
111
125
|
return _percentile(vals, p)
|
|
112
126
|
|
|
113
127
|
def sigma(self, component: str, value: float) -> float:
|
|
114
128
|
"""
|
|
115
|
-
|
|
129
|
+
Z-score of ``value`` against the fingerprint's empirical distribution.
|
|
116
130
|
|
|
117
|
-
Returns ``(value − mean) /
|
|
131
|
+
Returns ``(value − mean) / std`` (standard z-score).
|
|
118
132
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
133
|
+
Using ``std`` as denominator is correct for cross-session normalization.
|
|
134
|
+
``|mean|`` would only be appropriate when the coefficient of variation
|
|
135
|
+
(std/|mean|) is small; for high-CV components (e.g. ``std=0.498``,
|
|
136
|
+
``mean=0.070``, CV=7.1) it amplifies by 7×, turning a normal ±0.07
|
|
137
|
+
swing into ±1.0 and saturating the controller.
|
|
122
138
|
|
|
123
|
-
|
|
124
|
-
|
|
139
|
+
Falls back to ``(value − mean) / |mean|`` when ``std`` is zero (constant
|
|
140
|
+
window or not yet stored), and returns ``value`` unchanged when both
|
|
141
|
+
``mean`` and ``std`` are zero.
|
|
125
142
|
|
|
126
143
|
Example::
|
|
127
144
|
|
|
128
145
|
metric = fp.sigma("recovery_ratio", cq.recovery_ratio)
|
|
129
146
|
alpha, reason = ctrl.step(alpha, metric)
|
|
130
147
|
"""
|
|
131
|
-
|
|
148
|
+
stats = self.get(component, {})
|
|
149
|
+
mean = stats.get("mean", 0.0)
|
|
150
|
+
std = stats.get("std", 0.0)
|
|
151
|
+
if std != 0.0:
|
|
152
|
+
return (value - mean) / std
|
|
153
|
+
# std=0: constant distribution — fall back to relative deviation
|
|
132
154
|
if mean == 0.0:
|
|
133
155
|
return value
|
|
134
156
|
return (value - mean) / abs(mean)
|
|
@@ -64,7 +64,7 @@ from .confidence import Confidence
|
|
|
64
64
|
from .health import Health, Thresholds, classify
|
|
65
65
|
from .observation import Observation, Expression
|
|
66
66
|
from .provenance import ProvenanceGraph
|
|
67
|
-
from .fingerprint import Fingerprint
|
|
67
|
+
from .fingerprint import Fingerprint, _percentile as _fp_percentile
|
|
68
68
|
from .drift import (
|
|
69
69
|
DriftState, DriftDirection, DriftClassification,
|
|
70
70
|
classify_drift,
|
|
@@ -606,6 +606,9 @@ class Monitor:
|
|
|
606
606
|
"std": std,
|
|
607
607
|
"n": n,
|
|
608
608
|
"trend": dc.state.value if dc is not None else "UNKNOWN",
|
|
609
|
+
"median": _fp_percentile(values, 50),
|
|
610
|
+
"q25": _fp_percentile(values, 25),
|
|
611
|
+
"q75": _fp_percentile(values, 75),
|
|
609
612
|
}
|
|
610
613
|
raw_values[name] = values
|
|
611
614
|
return Fingerprint(stats=stats, values=raw_values)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: margin
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.18
|
|
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.18"
|
|
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"}
|
|
@@ -552,14 +552,100 @@ class TestFingerprintSigma:
|
|
|
552
552
|
|
|
553
553
|
def test_negative_mean(self):
|
|
554
554
|
fp = Fingerprint(stats={"rr": {"mean": -2.0, "std": 0.5, "n": 10, "trend": "STABLE"}})
|
|
555
|
-
# (value - mean) /
|
|
556
|
-
assert fp.sigma("rr", -1.0) == pytest.approx(0
|
|
555
|
+
# z-score: (value - mean) / std = (-1.0 - (-2.0)) / 0.5 = 2.0
|
|
556
|
+
assert fp.sigma("rr", -1.0) == pytest.approx(2.0)
|
|
557
557
|
|
|
558
558
|
def test_missing_component_returns_value(self):
|
|
559
559
|
fp = _make_fp([1.0] * 10)
|
|
560
560
|
# missing → mean=0.0 → returns value unchanged
|
|
561
561
|
assert fp.sigma("missing", 0.7) == pytest.approx(0.7)
|
|
562
562
|
|
|
563
|
+
def test_uses_std_not_mean_as_denominator(self):
|
|
564
|
+
# Regression: Build #29 failure — high-CV component (std=0.498, mean=0.070)
|
|
565
|
+
# Old formula: (value - mean) / |mean| = 0.07 / 0.07 = 1.0 (7× amplification)
|
|
566
|
+
# New formula: (value - mean) / std = 0.07 / 0.498 ≈ 0.141 (sensible z-score)
|
|
567
|
+
fp = Fingerprint(stats={"rr": {"mean": 0.070, "std": 0.498, "n": 98}})
|
|
568
|
+
result = fp.sigma("rr", 0.14)
|
|
569
|
+
assert result == pytest.approx((0.14 - 0.070) / 0.498, rel=1e-4) # z-score
|
|
570
|
+
assert abs(result) < 0.2 # not ±1.0 (the bang-bang range)
|
|
571
|
+
|
|
572
|
+
def test_std_zero_falls_back_to_mean_relative(self):
|
|
573
|
+
# Constant distribution: std=0 → fall back to (value-mean)/|mean|
|
|
574
|
+
fp = Fingerprint(stats={"rr": {"mean": 1.0, "std": 0.0, "n": 5}})
|
|
575
|
+
assert fp.sigma("rr", 1.5) == pytest.approx(0.5) # (1.5-1.0)/1.0
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
class TestFingerprintPrecomputedPercentiles:
|
|
579
|
+
"""Fingerprints from Monitor.fingerprint() now include median/q25/q75.
|
|
580
|
+
percentile() and robust_target() must use them when no raw values stored."""
|
|
581
|
+
|
|
582
|
+
def test_monitor_fingerprint_includes_median(self):
|
|
583
|
+
monitor = _make_monitor(20)
|
|
584
|
+
fp = monitor.fingerprint()
|
|
585
|
+
assert "median" in fp["alpha"]
|
|
586
|
+
assert "q25" in fp["alpha"]
|
|
587
|
+
assert "q75" in fp["alpha"]
|
|
588
|
+
|
|
589
|
+
def test_robust_sigma_live_after_serialization(self):
|
|
590
|
+
# Simulate the Build #29 scenario:
|
|
591
|
+
# Session 1 fingerprint saved as JSON, loaded in Session 2 — no raw values.
|
|
592
|
+
monitor = _make_monitor(30)
|
|
593
|
+
fp_live = monitor.fingerprint()
|
|
594
|
+
# Round-trip through JSON (loses raw values, keeps precomputed percentiles)
|
|
595
|
+
import json
|
|
596
|
+
fp_reloaded = Fingerprint.from_dict(json.loads(json.dumps(fp_live)))
|
|
597
|
+
# robust_sigma should differ from sigma (IQR != 0, median != mean generally)
|
|
598
|
+
# At minimum, it must not crash and must not return 0.0 for a nonzero input
|
|
599
|
+
result = fp_reloaded.robust_sigma("alpha", 0.55)
|
|
600
|
+
assert isinstance(result, float)
|
|
601
|
+
|
|
602
|
+
def test_robust_sigma_uses_precomputed_iqr(self):
|
|
603
|
+
# Construct fingerprint with known median/q25/q75
|
|
604
|
+
fp = Fingerprint(stats={"rr": {
|
|
605
|
+
"mean": 0.070, "std": 0.498, "n": 98, "trend": "DRIFTING",
|
|
606
|
+
"median": 0.050, "q25": 0.020, "q75": 0.120,
|
|
607
|
+
}})
|
|
608
|
+
# IQR = 0.120 - 0.020 = 0.100
|
|
609
|
+
# robust_sigma(0.14) = (0.14 - 0.050) / 0.100 = 0.9
|
|
610
|
+
assert fp.robust_sigma("rr", 0.14) == pytest.approx(0.9)
|
|
611
|
+
|
|
612
|
+
def test_robust_sigma_differs_from_mean_based(self):
|
|
613
|
+
# When median != mean (skewed distribution), the two should differ
|
|
614
|
+
fp = Fingerprint(stats={"rr": {
|
|
615
|
+
"mean": 0.070, "std": 0.498, "n": 98, "trend": "DRIFTING",
|
|
616
|
+
"median": 0.050, "q25": 0.020, "q75": 0.120,
|
|
617
|
+
}})
|
|
618
|
+
assert fp.robust_sigma("rr", 0.14) != pytest.approx(fp.sigma("rr", 0.14))
|
|
619
|
+
|
|
620
|
+
def test_regression_build29_scenario(self):
|
|
621
|
+
# Exact values from the user's Session 1 meta.json
|
|
622
|
+
fp = Fingerprint(stats={"recovery_ratio": {
|
|
623
|
+
"mean": 0.070, "std": 0.498, "n": 98, "trend": "DRIFTING",
|
|
624
|
+
"median": 0.050, "q25": 0.020, "q75": 0.120,
|
|
625
|
+
}})
|
|
626
|
+
r = fp.robust_sigma("recovery_ratio", 0.14)
|
|
627
|
+
# Previously returned 1.0 (mean-based fallback); now uses IQR
|
|
628
|
+
assert r == pytest.approx(0.9)
|
|
629
|
+
mean_based = fp.sigma("recovery_ratio", 0.14)
|
|
630
|
+
assert r != pytest.approx(mean_based)
|
|
631
|
+
|
|
632
|
+
def test_percentile_uses_precomputed_q25(self):
|
|
633
|
+
fp = Fingerprint(stats={"rr": {
|
|
634
|
+
"mean": 1.0, "std": 0.1, "n": 10, "trend": "STABLE",
|
|
635
|
+
"median": 1.0, "q25": 0.8, "q75": 1.2,
|
|
636
|
+
}})
|
|
637
|
+
assert fp.percentile("rr", 25) == pytest.approx(0.8)
|
|
638
|
+
assert fp.percentile("rr", 75) == pytest.approx(1.2)
|
|
639
|
+
assert fp.percentile("rr", 50) == pytest.approx(1.0)
|
|
640
|
+
|
|
641
|
+
def test_percentile_fallback_to_mean_for_other_p(self):
|
|
642
|
+
# Only 25/50/75 are pre-computed; other p falls back to mean
|
|
643
|
+
fp = Fingerprint(stats={"rr": {
|
|
644
|
+
"mean": 1.0, "std": 0.1, "n": 10, "trend": "STABLE",
|
|
645
|
+
"median": 1.0, "q25": 0.8, "q75": 1.2,
|
|
646
|
+
}})
|
|
647
|
+
assert fp.percentile("rr", 10) == pytest.approx(1.0) # falls back to mean
|
|
648
|
+
|
|
563
649
|
|
|
564
650
|
class TestFingerprintRobustSigma:
|
|
565
651
|
def test_median_centered(self):
|
|
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
|