margin 0.9.16__tar.gz → 0.9.17__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 (88) hide show
  1. {margin-0.9.16/margin.egg-info → margin-0.9.17}/PKG-INFO +1 -1
  2. {margin-0.9.16 → margin-0.9.17}/margin/fingerprint.py +18 -4
  3. {margin-0.9.16 → margin-0.9.17}/margin/streaming.py +4 -1
  4. {margin-0.9.16 → margin-0.9.17/margin.egg-info}/PKG-INFO +1 -1
  5. {margin-0.9.16 → margin-0.9.17}/pyproject.toml +1 -1
  6. {margin-0.9.16 → margin-0.9.17}/tests/test_controller.py +72 -0
  7. {margin-0.9.16 → margin-0.9.17}/LICENSE +0 -0
  8. {margin-0.9.16 → margin-0.9.17}/NOTICE +0 -0
  9. {margin-0.9.16 → margin-0.9.17}/README.md +0 -0
  10. {margin-0.9.16 → margin-0.9.17}/margin/__init__.py +0 -0
  11. {margin-0.9.16 → margin-0.9.17}/margin/__main__.py +0 -0
  12. {margin-0.9.16 → margin-0.9.17}/margin/algebra.py +0 -0
  13. {margin-0.9.16 → margin-0.9.17}/margin/anomaly.py +0 -0
  14. {margin-0.9.16 → margin-0.9.17}/margin/bridge.py +0 -0
  15. {margin-0.9.16 → margin-0.9.17}/margin/calibrate.py +0 -0
  16. {margin-0.9.16 → margin-0.9.17}/margin/causal.py +0 -0
  17. {margin-0.9.16 → margin-0.9.17}/margin/composite.py +0 -0
  18. {margin-0.9.16 → margin-0.9.17}/margin/confidence.py +0 -0
  19. {margin-0.9.16 → margin-0.9.17}/margin/config.py +0 -0
  20. {margin-0.9.16 → margin-0.9.17}/margin/contract.py +0 -0
  21. {margin-0.9.16 → margin-0.9.17}/margin/controller.py +0 -0
  22. {margin-0.9.16 → margin-0.9.17}/margin/correlate.py +0 -0
  23. {margin-0.9.16 → margin-0.9.17}/margin/diff.py +0 -0
  24. {margin-0.9.16 → margin-0.9.17}/margin/drift.py +0 -0
  25. {margin-0.9.16 → margin-0.9.17}/margin/events.py +0 -0
  26. {margin-0.9.16 → margin-0.9.17}/margin/forecast.py +0 -0
  27. {margin-0.9.16 → margin-0.9.17}/margin/health.py +0 -0
  28. {margin-0.9.16 → margin-0.9.17}/margin/intent.py +0 -0
  29. {margin-0.9.16 → margin-0.9.17}/margin/ledger.py +0 -0
  30. {margin-0.9.16 → margin-0.9.17}/margin/loop.py +0 -0
  31. {margin-0.9.16 → margin-0.9.17}/margin/observation.py +0 -0
  32. {margin-0.9.16 → margin-0.9.17}/margin/persist.py +0 -0
  33. {margin-0.9.16 → margin-0.9.17}/margin/policy/__init__.py +0 -0
  34. {margin-0.9.16 → margin-0.9.17}/margin/policy/compose.py +0 -0
  35. {margin-0.9.16 → margin-0.9.17}/margin/policy/core.py +0 -0
  36. {margin-0.9.16 → margin-0.9.17}/margin/policy/temporal.py +0 -0
  37. {margin-0.9.16 → margin-0.9.17}/margin/policy/trace.py +0 -0
  38. {margin-0.9.16 → margin-0.9.17}/margin/policy/tuning.py +0 -0
  39. {margin-0.9.16 → margin-0.9.17}/margin/policy/validate.py +0 -0
  40. {margin-0.9.16 → margin-0.9.17}/margin/predicates.py +0 -0
  41. {margin-0.9.16 → margin-0.9.17}/margin/provenance.py +0 -0
  42. {margin-0.9.16 → margin-0.9.17}/margin/transitions.py +0 -0
  43. {margin-0.9.16 → margin-0.9.17}/margin/uncertain.py +0 -0
  44. {margin-0.9.16 → margin-0.9.17}/margin/validity.py +0 -0
  45. {margin-0.9.16 → margin-0.9.17}/margin.egg-info/SOURCES.txt +0 -0
  46. {margin-0.9.16 → margin-0.9.17}/margin.egg-info/dependency_links.txt +0 -0
  47. {margin-0.9.16 → margin-0.9.17}/margin.egg-info/top_level.txt +0 -0
  48. {margin-0.9.16 → margin-0.9.17}/setup.cfg +0 -0
  49. {margin-0.9.16 → margin-0.9.17}/tests/test_algebra.py +0 -0
  50. {margin-0.9.16 → margin-0.9.17}/tests/test_anomaly.py +0 -0
  51. {margin-0.9.16 → margin-0.9.17}/tests/test_bridge.py +0 -0
  52. {margin-0.9.16 → margin-0.9.17}/tests/test_calibrate.py +0 -0
  53. {margin-0.9.16 → margin-0.9.17}/tests/test_causal.py +0 -0
  54. {margin-0.9.16 → margin-0.9.17}/tests/test_composite.py +0 -0
  55. {margin-0.9.16 → margin-0.9.17}/tests/test_confidence.py +0 -0
  56. {margin-0.9.16 → margin-0.9.17}/tests/test_config.py +0 -0
  57. {margin-0.9.16 → margin-0.9.17}/tests/test_contract.py +0 -0
  58. {margin-0.9.16 → margin-0.9.17}/tests/test_correlate.py +0 -0
  59. {margin-0.9.16 → margin-0.9.17}/tests/test_diff.py +0 -0
  60. {margin-0.9.16 → margin-0.9.17}/tests/test_drift.py +0 -0
  61. {margin-0.9.16 → margin-0.9.17}/tests/test_events.py +0 -0
  62. {margin-0.9.16 → margin-0.9.17}/tests/test_forecast.py +0 -0
  63. {margin-0.9.16 → margin-0.9.17}/tests/test_health.py +0 -0
  64. {margin-0.9.16 → margin-0.9.17}/tests/test_healthcare.py +0 -0
  65. {margin-0.9.16 → margin-0.9.17}/tests/test_intent.py +0 -0
  66. {margin-0.9.16 → margin-0.9.17}/tests/test_ledger.py +0 -0
  67. {margin-0.9.16 → margin-0.9.17}/tests/test_loop.py +0 -0
  68. {margin-0.9.16 → margin-0.9.17}/tests/test_neuro.py +0 -0
  69. {margin-0.9.16 → margin-0.9.17}/tests/test_observation.py +0 -0
  70. {margin-0.9.16 → margin-0.9.17}/tests/test_persist.py +0 -0
  71. {margin-0.9.16 → margin-0.9.17}/tests/test_policy.py +0 -0
  72. {margin-0.9.16 → margin-0.9.17}/tests/test_policy_compose.py +0 -0
  73. {margin-0.9.16 → margin-0.9.17}/tests/test_policy_temporal.py +0 -0
  74. {margin-0.9.16 → margin-0.9.17}/tests/test_policy_trace.py +0 -0
  75. {margin-0.9.16 → margin-0.9.17}/tests/test_policy_tuning.py +0 -0
  76. {margin-0.9.16 → margin-0.9.17}/tests/test_policy_validate.py +0 -0
  77. {margin-0.9.16 → margin-0.9.17}/tests/test_predicates.py +0 -0
  78. {margin-0.9.16 → margin-0.9.17}/tests/test_printer3d.py +0 -0
  79. {margin-0.9.16 → margin-0.9.17}/tests/test_provenance.py +0 -0
  80. {margin-0.9.16 → margin-0.9.17}/tests/test_reverse_bridge.py +0 -0
  81. {margin-0.9.16 → margin-0.9.17}/tests/test_ros2.py +0 -0
  82. {margin-0.9.16 → margin-0.9.17}/tests/test_staleness.py +0 -0
  83. {margin-0.9.16 → margin-0.9.17}/tests/test_streaming.py +0 -0
  84. {margin-0.9.16 → margin-0.9.17}/tests/test_transformer.py +0 -0
  85. {margin-0.9.16 → margin-0.9.17}/tests/test_transitions.py +0 -0
  86. {margin-0.9.16 → margin-0.9.17}/tests/test_uncertain.py +0 -0
  87. {margin-0.9.16 → margin-0.9.17}/tests/test_validity.py +0 -0
  88. {margin-0.9.16 → margin-0.9.17}/tests/test_windowing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: margin
3
- Version: 0.9.16
3
+ Version: 0.9.17
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
- Falls back to ``fp[component]["mean"]`` when no raw values are stored.
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
- return self.get(component, {}).get("mean", 0.0)
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,11 +108,20 @@ class Fingerprint(dict):
103
108
  """
104
109
  Return the p-th percentile (0–100) of observed values for ``component``.
105
110
 
106
- Falls back to the stored mean when no raw values are available.
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
- return self.get(component, {}).get("mean", 0.0)
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:
@@ -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.16
3
+ Version: 0.9.17
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.16"
7
+ version = "0.9.17"
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"}
@@ -561,6 +561,78 @@ class TestFingerprintSigma:
561
561
  assert fp.sigma("missing", 0.7) == pytest.approx(0.7)
562
562
 
563
563
 
564
+ class TestFingerprintPrecomputedPercentiles:
565
+ """Fingerprints from Monitor.fingerprint() now include median/q25/q75.
566
+ percentile() and robust_target() must use them when no raw values stored."""
567
+
568
+ def test_monitor_fingerprint_includes_median(self):
569
+ monitor = _make_monitor(20)
570
+ fp = monitor.fingerprint()
571
+ assert "median" in fp["alpha"]
572
+ assert "q25" in fp["alpha"]
573
+ assert "q75" in fp["alpha"]
574
+
575
+ def test_robust_sigma_live_after_serialization(self):
576
+ # Simulate the Build #29 scenario:
577
+ # Session 1 fingerprint saved as JSON, loaded in Session 2 — no raw values.
578
+ monitor = _make_monitor(30)
579
+ fp_live = monitor.fingerprint()
580
+ # Round-trip through JSON (loses raw values, keeps precomputed percentiles)
581
+ import json
582
+ fp_reloaded = Fingerprint.from_dict(json.loads(json.dumps(fp_live)))
583
+ # robust_sigma should differ from sigma (IQR != 0, median != mean generally)
584
+ # At minimum, it must not crash and must not return 0.0 for a nonzero input
585
+ result = fp_reloaded.robust_sigma("alpha", 0.55)
586
+ assert isinstance(result, float)
587
+
588
+ def test_robust_sigma_uses_precomputed_iqr(self):
589
+ # Construct fingerprint with known median/q25/q75
590
+ fp = Fingerprint(stats={"rr": {
591
+ "mean": 0.070, "std": 0.498, "n": 98, "trend": "DRIFTING",
592
+ "median": 0.050, "q25": 0.020, "q75": 0.120,
593
+ }})
594
+ # IQR = 0.120 - 0.020 = 0.100
595
+ # robust_sigma(0.14) = (0.14 - 0.050) / 0.100 = 0.9
596
+ assert fp.robust_sigma("rr", 0.14) == pytest.approx(0.9)
597
+
598
+ def test_robust_sigma_differs_from_mean_based(self):
599
+ # When median != mean (skewed distribution), the two should differ
600
+ fp = Fingerprint(stats={"rr": {
601
+ "mean": 0.070, "std": 0.498, "n": 98, "trend": "DRIFTING",
602
+ "median": 0.050, "q25": 0.020, "q75": 0.120,
603
+ }})
604
+ assert fp.robust_sigma("rr", 0.14) != pytest.approx(fp.sigma("rr", 0.14))
605
+
606
+ def test_regression_build29_scenario(self):
607
+ # Exact values from the user's Session 1 meta.json
608
+ fp = Fingerprint(stats={"recovery_ratio": {
609
+ "mean": 0.070, "std": 0.498, "n": 98, "trend": "DRIFTING",
610
+ "median": 0.050, "q25": 0.020, "q75": 0.120,
611
+ }})
612
+ r = fp.robust_sigma("recovery_ratio", 0.14)
613
+ # Previously returned 1.0 (mean-based fallback); now uses IQR
614
+ assert r == pytest.approx(0.9)
615
+ mean_based = fp.sigma("recovery_ratio", 0.14)
616
+ assert r != pytest.approx(mean_based)
617
+
618
+ def test_percentile_uses_precomputed_q25(self):
619
+ fp = Fingerprint(stats={"rr": {
620
+ "mean": 1.0, "std": 0.1, "n": 10, "trend": "STABLE",
621
+ "median": 1.0, "q25": 0.8, "q75": 1.2,
622
+ }})
623
+ assert fp.percentile("rr", 25) == pytest.approx(0.8)
624
+ assert fp.percentile("rr", 75) == pytest.approx(1.2)
625
+ assert fp.percentile("rr", 50) == pytest.approx(1.0)
626
+
627
+ def test_percentile_fallback_to_mean_for_other_p(self):
628
+ # Only 25/50/75 are pre-computed; other p falls back to mean
629
+ fp = Fingerprint(stats={"rr": {
630
+ "mean": 1.0, "std": 0.1, "n": 10, "trend": "STABLE",
631
+ "median": 1.0, "q25": 0.8, "q75": 1.2,
632
+ }})
633
+ assert fp.percentile("rr", 10) == pytest.approx(1.0) # falls back to mean
634
+
635
+
564
636
  class TestFingerprintRobustSigma:
565
637
  def test_median_centered(self):
566
638
  # Uniform values: median=3.0, IQR=2.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