arbiter-engine 0.1.0__py3-none-any.whl
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.
- arbiter_engine/__init__.py +31 -0
- arbiter_engine/api.py +473 -0
- arbiter_engine/axiom_thresholds.py +118 -0
- arbiter_engine/envelope.py +270 -0
- arbiter_engine/examples/water_tank.yaml +112 -0
- arbiter_engine/fire_frequency.py +252 -0
- arbiter_engine/history/__init__.py +12 -0
- arbiter_engine/history/observation.py +596 -0
- arbiter_engine/history/observation_production.py +335 -0
- arbiter_engine/history/observation_source_wiring.py +120 -0
- arbiter_engine/history/readiness.py +288 -0
- arbiter_engine/interfaces.py +1353 -0
- arbiter_engine/mcp/__init__.py +1 -0
- arbiter_engine/mcp/server.py +221 -0
- arbiter_engine/ontology/__init__.py +18 -0
- arbiter_engine/ontology/axiom_verdicts_production.py +332 -0
- arbiter_engine/ontology/axioms/__init__.py +26 -0
- arbiter_engine/ontology/axioms/boundedness.py +340 -0
- arbiter_engine/ontology/axioms/connectivity.py +396 -0
- arbiter_engine/ontology/axioms/conservation.py +266 -0
- arbiter_engine/ontology/axioms/consistency.py +422 -0
- arbiter_engine/ontology/axioms/extensions.py +106 -0
- arbiter_engine/ontology/axioms/homeostasis.py +783 -0
- arbiter_engine/ontology/axioms/monotonicity.py +444 -0
- arbiter_engine/ontology/axioms/responsiveness.py +752 -0
- arbiter_engine/ontology/axioms/roles.py +207 -0
- arbiter_engine/ontology/axioms/stability.py +327 -0
- arbiter_engine/ontology/domain_loader.py +485 -0
- arbiter_engine/ontology/loader.py +802 -0
- arbiter_engine/ontology/reasoner.py +1069 -0
- arbiter_engine/propagation/__init__.py +1 -0
- arbiter_engine/propagation/impact_estimator.py +305 -0
- arbiter_engine/propagation/lp_confidence.py +123 -0
- arbiter_engine/propagation/mcts_root_cause.py +266 -0
- arbiter_engine/propagation/root_cause.py +559 -0
- arbiter_engine/propagation/weight_learner.py +216 -0
- arbiter_engine/rca/__init__.py +4 -0
- arbiter_engine/rca/greedy_set_cover.py +291 -0
- arbiter_engine/residual/__init__.py +1 -0
- arbiter_engine/residual/predict_vs_mirror.py +542 -0
- arbiter_engine/temporal/__init__.py +1 -0
- arbiter_engine/temporal/temporal_edge.py +584 -0
- arbiter_engine/temporal/trend_projection.py +430 -0
- arbiter_engine/twin/__init__.py +23 -0
- arbiter_engine/twin/action_clears_problem.py +217 -0
- arbiter_engine/twin/builder.py +479 -0
- arbiter_engine/twin/gap.py +132 -0
- arbiter_engine/twin/hypothesis_generator.py +530 -0
- arbiter_engine/twin/hypothesis_production.py +236 -0
- arbiter_engine/twin/kernel_pipeline_executor.py +571 -0
- arbiter_engine/twin/monte_carlo_predictor.py +1001 -0
- arbiter_engine/twin/optimization_production.py +233 -0
- arbiter_engine/twin/pipeline_production.py +176 -0
- arbiter_engine/twin/topology.py +450 -0
- arbiter_engine/twin/topology_optimizer.py +412 -0
- arbiter_engine/twin/traverser.py +1260 -0
- arbiter_engine/twin/traverser_production.py +283 -0
- arbiter_engine/types.py +427 -0
- arbiter_engine-0.1.0.dist-info/METADATA +278 -0
- arbiter_engine-0.1.0.dist-info/RECORD +63 -0
- arbiter_engine-0.1.0.dist-info/WHEEL +4 -0
- arbiter_engine-0.1.0.dist-info/licenses/LICENSE +202 -0
- arbiter_engine-0.1.0.dist-info/licenses/NOTICE +18 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Axiom Readiness Tracking.
|
|
3
|
+
|
|
4
|
+
Tracks which axioms are ready to be checked for each entity
|
|
5
|
+
based on available observation history.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Any, Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
from ..interfaces import Entity, ObservationHistory
|
|
13
|
+
from ..types import Axiom, AxiomReadiness, AXIOM_MINIMUMS # noqa: F401
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Minimum observations required for each axiom.
|
|
19
|
+
#
|
|
20
|
+
# the canonical table moved to ``arbiter_engine/types.py``, beside the
|
|
21
|
+
# enum it keys on. Two of its eight values had drifted from the copies in
|
|
22
|
+
# ``ontology/reasoner.py``; one definition removes the class of bug.
|
|
23
|
+
# Re-exported above so existing ``...readiness import AXIOM_MINIMUMS``
|
|
24
|
+
# imports keep working.
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AxiomReadinessTracker:
|
|
28
|
+
"""
|
|
29
|
+
Track and report axiom readiness across all entities.
|
|
30
|
+
|
|
31
|
+
Axiom readiness depends on:
|
|
32
|
+
- Number of historical observations
|
|
33
|
+
- Time span of observations
|
|
34
|
+
- Quality of data (no gaps, consistent sampling)
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, custom_minimums: Optional[Dict[Axiom, int]] = None,
|
|
38
|
+
sample_interval_seconds: float = 1.0):
|
|
39
|
+
self.minimums = AXIOM_MINIMUMS.copy()
|
|
40
|
+
if custom_minimums:
|
|
41
|
+
self.minimums.update(custom_minimums)
|
|
42
|
+
|
|
43
|
+
# Adjust minimums based on observation sample interval.
|
|
44
|
+
# Slower-sampling domains need fewer observations (each carries more info).
|
|
45
|
+
if sample_interval_seconds > 1.0:
|
|
46
|
+
self.minimums = {
|
|
47
|
+
axiom: self._get_adjusted_minimum(base, sample_interval_seconds)
|
|
48
|
+
for axiom, base in self.minimums.items()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def _get_adjusted_minimum(base: int, sample_interval: float) -> int:
|
|
53
|
+
"""Adjust axiom minimum based on sample interval.
|
|
54
|
+
|
|
55
|
+
Fast sampling (<=1s): use base minimums (K8s standard).
|
|
56
|
+
Moderate (1-10s): halve minimums (each obs is more informative).
|
|
57
|
+
Slow (>10s): third of minimums (hardware, network domains).
|
|
58
|
+
"""
|
|
59
|
+
if sample_interval <= 1.0:
|
|
60
|
+
return base
|
|
61
|
+
elif sample_interval <= 10.0:
|
|
62
|
+
return max(3, base // 2)
|
|
63
|
+
else:
|
|
64
|
+
return max(2, base // 3)
|
|
65
|
+
|
|
66
|
+
def get_entity_readiness(
|
|
67
|
+
self,
|
|
68
|
+
entity: Entity,
|
|
69
|
+
history: ObservationHistory,
|
|
70
|
+
properties: Optional[List[str]] = None
|
|
71
|
+
) -> Dict[Axiom, AxiomReadiness]:
|
|
72
|
+
"""
|
|
73
|
+
Get axiom readiness for a single entity.
|
|
74
|
+
|
|
75
|
+
Uses per-property observation timestamps to infer actual
|
|
76
|
+
sample interval instead of relying on the global default. Mixed-rate
|
|
77
|
+
entities (e.g., temperature at 1s, firmware version at 60s) get
|
|
78
|
+
per-property adjusted minimums.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
entity: Entity to check
|
|
82
|
+
history: Observation history
|
|
83
|
+
properties: Properties to check (all if None)
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Dict mapping axiom to readiness status
|
|
87
|
+
"""
|
|
88
|
+
readiness = {}
|
|
89
|
+
|
|
90
|
+
# Get observation counts for entity properties
|
|
91
|
+
max_counts: Dict[Axiom, int] = {axiom: 0 for axiom in Axiom}
|
|
92
|
+
|
|
93
|
+
# Properties to check
|
|
94
|
+
if properties is None:
|
|
95
|
+
properties = self._extract_property_names(entity)
|
|
96
|
+
|
|
97
|
+
# Estimate per-property sample interval from observation history.
|
|
98
|
+
# Use the interval to apply property-specific adjusted minimums for
|
|
99
|
+
# mixed-rate entities where different properties sample at different rates.
|
|
100
|
+
_prop_intervals: Dict[str, float] = {}
|
|
101
|
+
if hasattr(history, 'get_observation_timestamps'):
|
|
102
|
+
for prop in (properties or []):
|
|
103
|
+
try:
|
|
104
|
+
timestamps = history.get_observation_timestamps(entity.id, prop, limit=10)
|
|
105
|
+
if len(timestamps) >= 2:
|
|
106
|
+
deltas = [
|
|
107
|
+
(timestamps[i] - timestamps[i - 1]).total_seconds()
|
|
108
|
+
for i in range(1, len(timestamps))
|
|
109
|
+
if (timestamps[i] - timestamps[i - 1]).total_seconds() > 0
|
|
110
|
+
]
|
|
111
|
+
if deltas:
|
|
112
|
+
_prop_intervals[prop] = sum(deltas) / len(deltas)
|
|
113
|
+
except Exception:
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
for prop in properties:
|
|
117
|
+
count = history.get_observation_count(entity.id, prop)
|
|
118
|
+
|
|
119
|
+
# Assign count to relevant axioms based on property type
|
|
120
|
+
value = entity.get_property(prop)
|
|
121
|
+
|
|
122
|
+
if isinstance(value, (int, float)):
|
|
123
|
+
# Numeric: relevant for BOUNDEDNESS, HOMEOSTASIS, and — added
|
|
124
|
+
# after the KeyError fix exposed it — CONSERVATION and
|
|
125
|
+
# MONOTONICITY. Both are numeric-series axioms, but neither was
|
|
126
|
+
# ever assigned a count here, so both sat at 0 and reported
|
|
127
|
+
# never-ready for every entity. That silently and permanently
|
|
128
|
+
# downweighted every CONSERVATION/MONOTONICITY problem the
|
|
129
|
+
# LayeredDetector raised, via _adjust_confidence. Adding the
|
|
130
|
+
# minimums un-crashed the pass; it did not make the table true.
|
|
131
|
+
max_counts[Axiom.BOUNDEDNESS] = max(max_counts[Axiom.BOUNDEDNESS], count)
|
|
132
|
+
max_counts[Axiom.HOMEOSTASIS] = max(max_counts[Axiom.HOMEOSTASIS], count)
|
|
133
|
+
max_counts[Axiom.CONSERVATION] = max(max_counts[Axiom.CONSERVATION], count)
|
|
134
|
+
max_counts[Axiom.MONOTONICITY] = max(max_counts[Axiom.MONOTONICITY], count)
|
|
135
|
+
elif isinstance(value, str):
|
|
136
|
+
# State: relevant for STABILITY, CONSISTENCY
|
|
137
|
+
max_counts[Axiom.STABILITY] = max(max_counts[Axiom.STABILITY], count)
|
|
138
|
+
max_counts[Axiom.CONSISTENCY] = max(max_counts[Axiom.CONSISTENCY], count)
|
|
139
|
+
|
|
140
|
+
# All properties count for RESPONSIVENESS
|
|
141
|
+
max_counts[Axiom.RESPONSIVENESS] = max(max_counts[Axiom.RESPONSIVENESS], count)
|
|
142
|
+
|
|
143
|
+
# CONNECTIVITY doesn't need history
|
|
144
|
+
max_counts[Axiom.CONNECTIVITY] = 1
|
|
145
|
+
|
|
146
|
+
# Build readiness report
|
|
147
|
+
for axiom in Axiom:
|
|
148
|
+
# Degrade-with-surfacing rather than KeyError: a future axiom added
|
|
149
|
+
# to the enum without a minimums entry should narrow readiness for
|
|
150
|
+
# that one axiom, not crash the entire detection pass.
|
|
151
|
+
if axiom not in self.minimums:
|
|
152
|
+
logger.warning(
|
|
153
|
+
"AxiomReadinessTracker: no minimum configured for %s — "
|
|
154
|
+
"treating it as never-ready. Add it to AXIOM_MINIMUMS.",
|
|
155
|
+
axiom,
|
|
156
|
+
)
|
|
157
|
+
required = self.minimums.get(axiom, 2 ** 31)
|
|
158
|
+
count = max_counts[axiom]
|
|
159
|
+
is_ready = count >= required
|
|
160
|
+
ratio = min(1.0, count / required) if required > 0 else 1.0
|
|
161
|
+
|
|
162
|
+
readiness[axiom] = AxiomReadiness(
|
|
163
|
+
axiom=axiom,
|
|
164
|
+
entity_id=entity.id,
|
|
165
|
+
entity_type=entity.type,
|
|
166
|
+
observations_count=count,
|
|
167
|
+
required_count=required,
|
|
168
|
+
is_ready=is_ready,
|
|
169
|
+
readiness_ratio=ratio,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
return readiness
|
|
173
|
+
|
|
174
|
+
def _extract_property_names(self, entity: Entity) -> List[str]:
|
|
175
|
+
"""Extract all property names from entity."""
|
|
176
|
+
properties = []
|
|
177
|
+
|
|
178
|
+
def extract(props: dict, prefix: str = ''):
|
|
179
|
+
for key, value in props.items():
|
|
180
|
+
path = f"{prefix}.{key}" if prefix else key
|
|
181
|
+
if isinstance(value, dict):
|
|
182
|
+
extract(value, path)
|
|
183
|
+
else:
|
|
184
|
+
properties.append(path)
|
|
185
|
+
|
|
186
|
+
extract(entity.properties)
|
|
187
|
+
return properties
|
|
188
|
+
|
|
189
|
+
def get_readiness_report(
|
|
190
|
+
self,
|
|
191
|
+
entities: List[Entity],
|
|
192
|
+
history: ObservationHistory
|
|
193
|
+
) -> Dict:
|
|
194
|
+
"""
|
|
195
|
+
Generate system-wide readiness report.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Dict with overall readiness statistics
|
|
199
|
+
"""
|
|
200
|
+
total_by_axiom = {axiom: {'ready': 0, 'not_ready': 0} for axiom in Axiom}
|
|
201
|
+
entity_reports = {}
|
|
202
|
+
|
|
203
|
+
for entity in entities:
|
|
204
|
+
readiness = self.get_entity_readiness(entity, history)
|
|
205
|
+
entity_reports[entity.id] = readiness
|
|
206
|
+
|
|
207
|
+
for axiom, status in readiness.items():
|
|
208
|
+
if status.is_ready:
|
|
209
|
+
total_by_axiom[axiom]['ready'] += 1
|
|
210
|
+
else:
|
|
211
|
+
total_by_axiom[axiom]['not_ready'] += 1
|
|
212
|
+
|
|
213
|
+
# Calculate percentages
|
|
214
|
+
summary = {}
|
|
215
|
+
for axiom in Axiom:
|
|
216
|
+
ready = total_by_axiom[axiom]['ready']
|
|
217
|
+
not_ready = total_by_axiom[axiom]['not_ready']
|
|
218
|
+
total = ready + not_ready
|
|
219
|
+
|
|
220
|
+
summary[axiom.value] = {
|
|
221
|
+
'ready_count': ready,
|
|
222
|
+
'not_ready_count': not_ready,
|
|
223
|
+
'ready_percentage': (ready / total * 100) if total > 0 else 0,
|
|
224
|
+
#.get() for the same reason as the per-entity loop: a 9th enum
|
|
225
|
+
# member added without a minimums entry should narrow the report,
|
|
226
|
+
# not KeyError the summary. The per-entity path was guarded and
|
|
227
|
+
# this one was missed — the guard is only as good as its coverage.
|
|
228
|
+
'minimum_required': self.minimums.get(axiom),
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
'entity_count': len(entities),
|
|
233
|
+
'axiom_summary': summary,
|
|
234
|
+
'timestamp': datetime.utcnow().isoformat(),
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
def get_warmup_status(
|
|
238
|
+
self,
|
|
239
|
+
entities: List[Entity],
|
|
240
|
+
history: ObservationHistory
|
|
241
|
+
) -> Dict[str, Dict[str, Any]]:
|
|
242
|
+
"""Get observable warmup progress per entity per axiom.
|
|
243
|
+
|
|
244
|
+
Returns a dict keyed by entity_id with per-axiom warmup progress.
|
|
245
|
+
Operators can distinguish "healthy" from "still warming up".
|
|
246
|
+
"""
|
|
247
|
+
status = {}
|
|
248
|
+
for entity in entities:
|
|
249
|
+
readiness = self.get_entity_readiness(entity, history)
|
|
250
|
+
entity_status = {}
|
|
251
|
+
for axiom, r in readiness.items():
|
|
252
|
+
entity_status[axiom.value] = {
|
|
253
|
+
'is_ready': r.is_ready,
|
|
254
|
+
'observations': r.observations_count,
|
|
255
|
+
'required': r.required_count,
|
|
256
|
+
'progress_pct': min(100, int(
|
|
257
|
+
r.observations_count / r.required_count * 100
|
|
258
|
+
)) if r.required_count > 0 else 100,
|
|
259
|
+
}
|
|
260
|
+
status[entity.id] = entity_status
|
|
261
|
+
return status
|
|
262
|
+
|
|
263
|
+
def adjust_detection_confidence(
|
|
264
|
+
self,
|
|
265
|
+
problem: 'Problem',
|
|
266
|
+
readiness: AxiomReadiness
|
|
267
|
+
) -> 'Problem':
|
|
268
|
+
"""
|
|
269
|
+
Adjust problem confidence based on axiom readiness.
|
|
270
|
+
|
|
271
|
+
Problems detected with insufficient history get lower confidence.
|
|
272
|
+
"""
|
|
273
|
+
if readiness.is_ready:
|
|
274
|
+
return problem
|
|
275
|
+
|
|
276
|
+
# Scale confidence by readiness ratio
|
|
277
|
+
adjusted_confidence = problem.confidence * readiness.readiness_ratio
|
|
278
|
+
|
|
279
|
+
# Update evidence
|
|
280
|
+
problem.evidence['axiom_readiness'] = {
|
|
281
|
+
'observations': readiness.observations_count,
|
|
282
|
+
'required': readiness.required_count,
|
|
283
|
+
'ratio': readiness.readiness_ratio,
|
|
284
|
+
}
|
|
285
|
+
problem.confidence = adjusted_confidence
|
|
286
|
+
problem.metadata['confidence_adjusted'] = True
|
|
287
|
+
|
|
288
|
+
return problem
|