fable-engine 1.3.1__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.
- fable_compressor.py +356 -0
- fable_engine/__init__.py +1 -0
- fable_engine/actions/__init__.py +291 -0
- fable_engine/actions/cas.py +182 -0
- fable_engine/actions/deliberation.py +523 -0
- fable_engine/actions/fleet.py +807 -0
- fable_engine/actions/lifecycle.py +298 -0
- fable_engine/actions/scrapers.py +116 -0
- fable_engine/actions/system3.py +815 -0
- fable_engine/browser.py +824 -0
- fable_engine/cas.py +974 -0
- fable_engine/fable_session.json +510 -0
- fable_engine/guards.py +283 -0
- fable_engine/schema.py +714 -0
- fable_engine/scrapers/__init__.py +32 -0
- fable_engine/scrapers/arxiv.py +115 -0
- fable_engine/scrapers/base.py +386 -0
- fable_engine/scrapers/github.py +129 -0
- fable_engine/scrapers/reddit.py +154 -0
- fable_engine/scrapers/web.py +120 -0
- fable_engine/scrapers/x.py +125 -0
- fable_engine/scrapers/youtube.py +132 -0
- fable_engine/server.py +414 -0
- fable_engine/session.py +1819 -0
- fable_engine/test_server.py +1362 -0
- fable_engine/updater.py +541 -0
- fable_engine-1.3.1.dist-info/LICENSE +22 -0
- fable_engine-1.3.1.dist-info/METADATA +173 -0
- fable_engine-1.3.1.dist-info/RECORD +104 -0
- fable_engine-1.3.1.dist-info/WHEEL +5 -0
- fable_engine-1.3.1.dist-info/entry_points.txt +5 -0
- fable_engine-1.3.1.dist-info/top_level.txt +6 -0
- fable_mode/__init__.py +3 -0
- fable_mode/__main__.py +4 -0
- fable_mode/adapters.py +1014 -0
- fable_mode/installer.py +553 -0
- fable_mode/launcher.py +437 -0
- fable_mode/manifest.py +142 -0
- fable_mode/resources.json +114 -0
- fable_mode/safety.py +103 -0
- fable_mode_entry.py +10 -0
- fable_v2/__init__.py +146 -0
- fable_v2/adapters.py +151 -0
- fable_v2/coder_fleet/__init__.py +100 -0
- fable_v2/coder_fleet/ast_tools.py +158 -0
- fable_v2/coder_fleet/compute.py +199 -0
- fable_v2/coder_fleet/design_engine.py +1316 -0
- fable_v2/coder_fleet/diagnostics.py +293 -0
- fable_v2/coder_fleet/fleet_dispatcher.py +214 -0
- fable_v2/coder_fleet/mock_auditor.py +306 -0
- fable_v2/coder_fleet/mutation.py +216 -0
- fable_v2/coder_fleet/property_oracle.py +260 -0
- fable_v2/coder_fleet/receipt_attestor.py +122 -0
- fable_v2/coder_fleet/red_team_swarm.py +908 -0
- fable_v2/coder_fleet/test_harness.py +198 -0
- fable_v2/coder_fleet/vector_engine.py +1287 -0
- fable_v2/coder_fleet/visual.py +357 -0
- fable_v2/coder_fleet/workspace.py +153 -0
- fable_v2/cortical/__init__.py +20 -0
- fable_v2/cortical/plasticity_engine.py +992 -0
- fable_v2/execution_broker.py +811 -0
- fable_v2/proof_engine.py +1141 -0
- fable_v2/protocol.py +485 -0
- fable_v2/runtime.py +1010 -0
- fable_v2/system3/__init__.py +204 -0
- fable_v2/system3/causal.py +558 -0
- fable_v2/system3/dialectical.py +577 -0
- fable_v2/system3/evolution.py +503 -0
- fable_v2/system3/executive.py +338 -0
- fable_v2/system3/free_energy.py +479 -0
- fable_v2/system3/hyperbolic.py +555 -0
- fable_v2/system3/induction.py +336 -0
- fable_v2/system3/kripke.py +548 -0
- fable_v2/system3/oracle.py +745 -0
- fable_v2/verifiers.py +72 -0
- tests/__init__.py +1 -0
- tests/test_anti_loop_circuit_breaker.py +64 -0
- tests/test_auto_updater.py +407 -0
- tests/test_coder_fleet.py +535 -0
- tests/test_delegation_compiler.py +54 -0
- tests/test_descriptor_boundaries.py +126 -0
- tests/test_design_engine.py +603 -0
- tests/test_epistemic_evidence_validator.py +66 -0
- tests/test_execution_broker.py +233 -0
- tests/test_fable_v2.py +406 -0
- tests/test_fleet_transitions.py +116 -0
- tests/test_fsm_redteam_evolution.py +406 -0
- tests/test_goal_rubric_and_pipeline.py +367 -0
- tests/test_hebbian_plasticity.py +585 -0
- tests/test_packaging_runtime.py +194 -0
- tests/test_proof_engine.py +259 -0
- tests/test_red_team_swarm.py +645 -0
- tests/test_redteam_remediation.py +169 -0
- tests/test_registration_transaction.py +375 -0
- tests/test_requested_regressions.py +467 -0
- tests/test_scrapers.py +370 -0
- tests/test_server_actions.py +93 -0
- tests/test_server_frontier_actions.py +269 -0
- tests/test_server_protocol.py +88 -0
- tests/test_stealth_browser.py +970 -0
- tests/test_system3.py +381 -0
- tests/test_system3_deep_integration.py +385 -0
- tests/test_system3_frontier.py +436 -0
- tests/test_vector_engine.py +608 -0
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
"""System 3 Kripke Modal Model Checker & Multi-World Branching Semantics.
|
|
2
|
+
|
|
3
|
+
Implements Kripke multi-world structures, modal logic operators (Box, Diamond),
|
|
4
|
+
and Computational Tree Logic (CTL/CTL*) temporal operators (AG, EF, AF, AX, EX, EG, AU, EU)
|
|
5
|
+
with fixed-point state satisfaction, witness/counterexample path generation, and
|
|
6
|
+
AST formula parsing in pure standard library Python. Zero external dependencies.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field, asdict
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
|
|
14
|
+
import copy
|
|
15
|
+
import json
|
|
16
|
+
import re
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CTLOperator(str, Enum):
|
|
20
|
+
"""CTL Temporal and Modal Logic Operators."""
|
|
21
|
+
ATOM = "ATOM"
|
|
22
|
+
TRUE = "TRUE"
|
|
23
|
+
FALSE = "FALSE"
|
|
24
|
+
NOT = "NOT"
|
|
25
|
+
AND = "AND"
|
|
26
|
+
OR = "OR"
|
|
27
|
+
IMPLIES = "IMPLIES"
|
|
28
|
+
EX = "EX" # Exists Next
|
|
29
|
+
AX = "AX" # All Next
|
|
30
|
+
EF = "EF" # Exists Finally (Reachability)
|
|
31
|
+
AF = "AF" # All Finally (Liveness)
|
|
32
|
+
EG = "EG" # Exists Globally (Persistence)
|
|
33
|
+
AG = "AG" # All Globally (Safety Invariant)
|
|
34
|
+
EU = "EU" # Exists Until (E[p U q])
|
|
35
|
+
AU = "AU" # All Until (A[p U q])
|
|
36
|
+
BOX = "BOX" # Modal Necessity (Box p)
|
|
37
|
+
DIAMOND = "DIAMOND"# Modal Possibility (Diamond p)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class FormulaNode:
|
|
42
|
+
"""Abstract Syntax Tree node for CTL / Modal logic formulas."""
|
|
43
|
+
op: CTLOperator
|
|
44
|
+
name: str = "" # Atomic proposition name if ATOM
|
|
45
|
+
left: Optional["FormulaNode"] = None # Subformula / Left operand
|
|
46
|
+
right: Optional["FormulaNode"] = None # Right operand (for AND, OR, IMPLIES, EU, AU)
|
|
47
|
+
|
|
48
|
+
def to_string(self) -> str:
|
|
49
|
+
if self.op == CTLOperator.ATOM:
|
|
50
|
+
return self.name
|
|
51
|
+
if self.op == CTLOperator.TRUE:
|
|
52
|
+
return "true"
|
|
53
|
+
if self.op == CTLOperator.FALSE:
|
|
54
|
+
return "false"
|
|
55
|
+
if self.op == CTLOperator.NOT and self.left:
|
|
56
|
+
return f"not({self.left.to_string()})"
|
|
57
|
+
if self.op in (CTLOperator.AND, CTLOperator.OR, CTLOperator.IMPLIES) and self.left and self.right:
|
|
58
|
+
return f"{self.op.value.lower()}({self.left.to_string()}, {self.right.to_string()})"
|
|
59
|
+
if self.op in (CTLOperator.EX, CTLOperator.AX, CTLOperator.EF, CTLOperator.AF, CTLOperator.EG, CTLOperator.AG, CTLOperator.BOX, CTLOperator.DIAMOND) and self.left:
|
|
60
|
+
return f"{self.op.value}({self.left.to_string()})"
|
|
61
|
+
if self.op in (CTLOperator.EU, CTLOperator.AU) and self.left and self.right:
|
|
62
|
+
return f"{self.op.value[0]}[{self.left.to_string()} U {self.right.to_string()}]"
|
|
63
|
+
return f"{self.op.value}"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class KripkeWorld:
|
|
68
|
+
"""A world/state in the Kripke structure."""
|
|
69
|
+
world_id: str
|
|
70
|
+
name: str = ""
|
|
71
|
+
propositions: Set[str] = field(default_factory=set)
|
|
72
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
73
|
+
|
|
74
|
+
def has_prop(self, prop: str) -> bool:
|
|
75
|
+
return prop in self.propositions
|
|
76
|
+
|
|
77
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
78
|
+
return {
|
|
79
|
+
"world_id": self.world_id,
|
|
80
|
+
"name": self.name or self.world_id,
|
|
81
|
+
"propositions": sorted(list(self.propositions)),
|
|
82
|
+
"metadata": self.metadata,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def from_dict(cls, data: Dict[str, Any]) -> "KripkeWorld":
|
|
87
|
+
return cls(
|
|
88
|
+
world_id=data["world_id"],
|
|
89
|
+
name=data.get("name", data["world_id"]),
|
|
90
|
+
propositions=set(data.get("propositions", [])),
|
|
91
|
+
metadata=data.get("metadata", {}),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class ModelCheckResult:
|
|
97
|
+
"""Result of Kripke model checking evaluation."""
|
|
98
|
+
formula: str
|
|
99
|
+
is_satisfied: bool
|
|
100
|
+
initial_world: str
|
|
101
|
+
satisfied_worlds: List[str] = field(default_factory=list)
|
|
102
|
+
violated_worlds: List[str] = field(default_factory=list)
|
|
103
|
+
total_worlds: int = 0
|
|
104
|
+
counterexample_path: Optional[List[str]] = None
|
|
105
|
+
witness_path: Optional[List[str]] = None
|
|
106
|
+
details: str = ""
|
|
107
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
108
|
+
|
|
109
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
110
|
+
return asdict(self)
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
def from_dict(cls, data: Dict[str, Any]) -> "ModelCheckResult":
|
|
114
|
+
return cls(**data)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class KripkeStructure:
|
|
118
|
+
"""
|
|
119
|
+
Kripke Model M = (W, R, L, W0)
|
|
120
|
+
- W: Set of Worlds/States
|
|
121
|
+
- R: Transition / Accessibility Relation W -> P(W)
|
|
122
|
+
- L: Valuation / Labeling Function W -> P(AP)
|
|
123
|
+
- W0: Initial Worlds
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
def __init__(self, name: str = "KripkeModel"):
|
|
127
|
+
self.name = name
|
|
128
|
+
self.worlds: Dict[str, KripkeWorld] = {}
|
|
129
|
+
self.transitions: Dict[str, Set[str]] = {}
|
|
130
|
+
self.inverse_transitions: Dict[str, Set[str]] = {}
|
|
131
|
+
self.initial_worlds: Set[str] = set()
|
|
132
|
+
|
|
133
|
+
def add_world(
|
|
134
|
+
self,
|
|
135
|
+
world_id: str,
|
|
136
|
+
propositions: Optional[Sequence[str]] = None,
|
|
137
|
+
name: str = "",
|
|
138
|
+
is_initial: bool = False,
|
|
139
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
140
|
+
) -> KripkeWorld:
|
|
141
|
+
"""Add a world state with its atomic propositions."""
|
|
142
|
+
props = set(propositions or [])
|
|
143
|
+
world = KripkeWorld(
|
|
144
|
+
world_id=world_id,
|
|
145
|
+
name=name or world_id,
|
|
146
|
+
propositions=props,
|
|
147
|
+
metadata=metadata or {},
|
|
148
|
+
)
|
|
149
|
+
self.worlds[world_id] = world
|
|
150
|
+
if world_id not in self.transitions:
|
|
151
|
+
self.transitions[world_id] = set()
|
|
152
|
+
if world_id not in self.inverse_transitions:
|
|
153
|
+
self.inverse_transitions[world_id] = set()
|
|
154
|
+
if is_initial or not self.initial_worlds:
|
|
155
|
+
self.initial_worlds.add(world_id)
|
|
156
|
+
return world
|
|
157
|
+
|
|
158
|
+
def add_transition(self, source: str, target: str):
|
|
159
|
+
"""Add directed transition / accessibility relation from source to target."""
|
|
160
|
+
if source not in self.worlds:
|
|
161
|
+
self.add_world(source)
|
|
162
|
+
if target not in self.worlds:
|
|
163
|
+
self.add_world(target)
|
|
164
|
+
self.transitions[source].add(target)
|
|
165
|
+
self.inverse_transitions[target].add(source)
|
|
166
|
+
|
|
167
|
+
def get_successors(self, world_id: str) -> Set[str]:
|
|
168
|
+
return self.transitions.get(world_id, set())
|
|
169
|
+
|
|
170
|
+
def get_predecessors(self, world_id: str) -> Set[str]:
|
|
171
|
+
return self.inverse_transitions.get(world_id, set())
|
|
172
|
+
|
|
173
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
174
|
+
return {
|
|
175
|
+
"name": self.name,
|
|
176
|
+
"worlds": {k: v.to_dict() for k, v in self.worlds.items()},
|
|
177
|
+
"transitions": {k: sorted(list(v)) for k, v in self.transitions.items()},
|
|
178
|
+
"initial_worlds": sorted(list(self.initial_worlds)),
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
@classmethod
|
|
182
|
+
def from_dict(cls, data: Dict[str, Any]) -> "KripkeStructure":
|
|
183
|
+
ks = cls(name=data.get("name", "KripkeModel"))
|
|
184
|
+
for wid, wdata in data.get("worlds", {}).items():
|
|
185
|
+
ks.add_world(
|
|
186
|
+
world_id=wid,
|
|
187
|
+
propositions=wdata.get("propositions", []),
|
|
188
|
+
name=wdata.get("name", wid),
|
|
189
|
+
metadata=wdata.get("metadata", {}),
|
|
190
|
+
)
|
|
191
|
+
for src, targets in data.get("transitions", {}).items():
|
|
192
|
+
for tgt in targets:
|
|
193
|
+
ks.add_transition(src, tgt)
|
|
194
|
+
if "initial_worlds" in data:
|
|
195
|
+
ks.initial_worlds = set(data["initial_worlds"])
|
|
196
|
+
return ks
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class FormulaParser:
|
|
200
|
+
"""
|
|
201
|
+
Recursive parser for CTL and Modal logic formula strings.
|
|
202
|
+
Supported syntaxes:
|
|
203
|
+
- Atomic: "safe", "error", "p"
|
|
204
|
+
- Constants: "true", "false"
|
|
205
|
+
- Boolean: "not(p)", "and(p, q)", "or(p, q)", "implies(p, q)"
|
|
206
|
+
- Modal: "box(p)", "diamond(p)", "[](p)", "<>(p)"
|
|
207
|
+
- Temporal: "AG(p)", "AF(p)", "EF(p)", "EG(p)", "AX(p)", "EX(p)"
|
|
208
|
+
- Until: "E[p U q]", "A[p U q]", "EU(p, q)", "AU(p, q)"
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
@classmethod
|
|
212
|
+
def parse(cls, expr: str) -> FormulaNode:
|
|
213
|
+
s = expr.strip()
|
|
214
|
+
if not s:
|
|
215
|
+
return FormulaNode(CTLOperator.TRUE)
|
|
216
|
+
|
|
217
|
+
# True / False
|
|
218
|
+
if s.lower() == "true":
|
|
219
|
+
return FormulaNode(CTLOperator.TRUE)
|
|
220
|
+
if s.lower() == "false":
|
|
221
|
+
return FormulaNode(CTLOperator.FALSE)
|
|
222
|
+
|
|
223
|
+
# Modal shortcut symbols
|
|
224
|
+
if s.startswith("[](") and s.endswith(")"):
|
|
225
|
+
return FormulaNode(CTLOperator.BOX, left=cls.parse(s[3:-1]))
|
|
226
|
+
if s.startswith("<>(") and s.endswith(")"):
|
|
227
|
+
return FormulaNode(CTLOperator.DIAMOND, left=cls.parse(s[3:-1]))
|
|
228
|
+
|
|
229
|
+
# Functional syntax: AG(...), EF(...), etc.
|
|
230
|
+
m_func = re.match(r"^([A-Za-z0-9_]+)\s*\((.*)\)$", s, re.DOTALL)
|
|
231
|
+
if m_func:
|
|
232
|
+
op_name = m_func.group(1).upper()
|
|
233
|
+
inner = m_func.group(2).strip()
|
|
234
|
+
|
|
235
|
+
if op_name in ("NOT", "NEG"):
|
|
236
|
+
return FormulaNode(CTLOperator.NOT, left=cls.parse(inner))
|
|
237
|
+
if op_name in ("BOX", "NECESSARY"):
|
|
238
|
+
return FormulaNode(CTLOperator.BOX, left=cls.parse(inner))
|
|
239
|
+
if op_name in ("DIAMOND", "POSSIBLE"):
|
|
240
|
+
return FormulaNode(CTLOperator.DIAMOND, left=cls.parse(inner))
|
|
241
|
+
if op_name in ("AG", "AF", "EF", "EG", "AX", "EX"):
|
|
242
|
+
return FormulaNode(CTLOperator[op_name], left=cls.parse(inner))
|
|
243
|
+
|
|
244
|
+
if op_name in ("AND", "OR", "IMPLIES", "EU", "AU"):
|
|
245
|
+
args = cls._split_top_level_args(inner)
|
|
246
|
+
if len(args) == 2:
|
|
247
|
+
return FormulaNode(
|
|
248
|
+
CTLOperator[op_name],
|
|
249
|
+
left=cls.parse(args[0]),
|
|
250
|
+
right=cls.parse(args[1]),
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# Bracketed Until syntax: E[p U q] or A[p U q]
|
|
254
|
+
m_until = re.match(r"^([EA])\s*\[\s*(.*?)\s+[Uu]\s+(.*?)\s*\]$", s)
|
|
255
|
+
if m_until:
|
|
256
|
+
quant = m_until.group(1).upper()
|
|
257
|
+
left_expr = m_until.group(2)
|
|
258
|
+
right_expr = m_until.group(3)
|
|
259
|
+
op = CTLOperator.EU if quant == "E" else CTLOperator.AU
|
|
260
|
+
return FormulaNode(op, left=cls.parse(left_expr), right=cls.parse(right_expr))
|
|
261
|
+
|
|
262
|
+
# Default: Atomic Proposition
|
|
263
|
+
return FormulaNode(CTLOperator.ATOM, name=s)
|
|
264
|
+
|
|
265
|
+
@classmethod
|
|
266
|
+
def _split_top_level_args(cls, s: str) -> List[str]:
|
|
267
|
+
args = []
|
|
268
|
+
depth_paren = 0
|
|
269
|
+
depth_bracket = 0
|
|
270
|
+
curr = []
|
|
271
|
+
for ch in s:
|
|
272
|
+
if ch == "(":
|
|
273
|
+
depth_paren += 1
|
|
274
|
+
curr.append(ch)
|
|
275
|
+
elif ch == ")":
|
|
276
|
+
depth_paren -= 1
|
|
277
|
+
curr.append(ch)
|
|
278
|
+
elif ch == "[":
|
|
279
|
+
depth_bracket += 1
|
|
280
|
+
curr.append(ch)
|
|
281
|
+
elif ch == "]":
|
|
282
|
+
depth_bracket -= 1
|
|
283
|
+
curr.append(ch)
|
|
284
|
+
elif ch == "," and depth_paren == 0 and depth_bracket == 0:
|
|
285
|
+
args.append("".join(curr).strip())
|
|
286
|
+
curr = []
|
|
287
|
+
else:
|
|
288
|
+
curr.append(ch)
|
|
289
|
+
if curr:
|
|
290
|
+
args.append("".join(curr).strip())
|
|
291
|
+
return args
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
class KripkeModelChecker:
|
|
295
|
+
"""
|
|
296
|
+
Fixed-point CTL and Modal Logic Model Checker for Kripke Structures.
|
|
297
|
+
Computes exact state satisfaction sets Sat(phi) and generates witness/counterexample traces.
|
|
298
|
+
"""
|
|
299
|
+
|
|
300
|
+
def __init__(self, structure: KripkeStructure):
|
|
301
|
+
self.kripke = structure
|
|
302
|
+
|
|
303
|
+
def check(
|
|
304
|
+
self,
|
|
305
|
+
formula: Union[str, FormulaNode],
|
|
306
|
+
initial_world: Optional[str] = None,
|
|
307
|
+
) -> ModelCheckResult:
|
|
308
|
+
"""
|
|
309
|
+
Verify if Kripke structure satisfies formula starting from initial world.
|
|
310
|
+
"""
|
|
311
|
+
if isinstance(formula, str):
|
|
312
|
+
ast = FormulaParser.parse(formula)
|
|
313
|
+
formula_str = formula
|
|
314
|
+
else:
|
|
315
|
+
ast = formula
|
|
316
|
+
formula_str = ast.to_string()
|
|
317
|
+
|
|
318
|
+
sat_worlds = self.sat(ast)
|
|
319
|
+
all_worlds = set(self.kripke.worlds.keys())
|
|
320
|
+
violated_worlds = sorted(list(all_worlds - sat_worlds))
|
|
321
|
+
|
|
322
|
+
init_w = initial_world or (list(self.kripke.initial_worlds)[0] if self.kripke.initial_worlds else None)
|
|
323
|
+
if init_w is None and self.kripke.worlds:
|
|
324
|
+
init_w = list(self.kripke.worlds.keys())[0]
|
|
325
|
+
|
|
326
|
+
is_satisfied = init_w in sat_worlds if init_w else False
|
|
327
|
+
|
|
328
|
+
# Generate Counterexample or Witness paths
|
|
329
|
+
counterexample = None
|
|
330
|
+
witness = None
|
|
331
|
+
|
|
332
|
+
if not is_satisfied and init_w:
|
|
333
|
+
counterexample = self._generate_counterexample(ast, init_w, sat_worlds)
|
|
334
|
+
elif is_satisfied and init_w:
|
|
335
|
+
witness = self._generate_witness(ast, init_w, sat_worlds)
|
|
336
|
+
|
|
337
|
+
details = (
|
|
338
|
+
f"Formula '{formula_str}' is {'SATISFIED' if is_satisfied else 'VIOLATED'} "
|
|
339
|
+
f"at initial world '{init_w}'. ({len(sat_worlds)}/{len(all_worlds)} worlds satisfied)"
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
return ModelCheckResult(
|
|
343
|
+
formula=formula_str,
|
|
344
|
+
is_satisfied=is_satisfied,
|
|
345
|
+
initial_world=init_w or "",
|
|
346
|
+
satisfied_worlds=sorted(list(sat_worlds)),
|
|
347
|
+
violated_worlds=violated_worlds,
|
|
348
|
+
total_worlds=len(all_worlds),
|
|
349
|
+
counterexample_path=counterexample,
|
|
350
|
+
witness_path=witness,
|
|
351
|
+
details=details,
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
def sat(self, node: FormulaNode) -> Set[str]:
|
|
355
|
+
"""Compute set of all worlds satisfying subformula node via fixed points."""
|
|
356
|
+
all_w = set(self.kripke.worlds.keys())
|
|
357
|
+
|
|
358
|
+
if node.op == CTLOperator.TRUE:
|
|
359
|
+
return set(all_w)
|
|
360
|
+
|
|
361
|
+
if node.op == CTLOperator.FALSE:
|
|
362
|
+
return set()
|
|
363
|
+
|
|
364
|
+
if node.op == CTLOperator.ATOM:
|
|
365
|
+
return {w for w, obj in self.kripke.worlds.items() if obj.has_prop(node.name)}
|
|
366
|
+
|
|
367
|
+
if node.op == CTLOperator.NOT and node.left:
|
|
368
|
+
return all_w - self.sat(node.left)
|
|
369
|
+
|
|
370
|
+
if node.op == CTLOperator.AND and node.left and node.right:
|
|
371
|
+
return self.sat(node.left) & self.sat(node.right)
|
|
372
|
+
|
|
373
|
+
if node.op == CTLOperator.OR and node.left and node.right:
|
|
374
|
+
return self.sat(node.left) | self.sat(node.right)
|
|
375
|
+
|
|
376
|
+
if node.op == CTLOperator.IMPLIES and node.left and node.right:
|
|
377
|
+
sat_l = self.sat(node.left)
|
|
378
|
+
sat_r = self.sat(node.right)
|
|
379
|
+
return (all_w - sat_l) | sat_r
|
|
380
|
+
|
|
381
|
+
# Modal Logic Operators
|
|
382
|
+
if node.op == CTLOperator.BOX and node.left:
|
|
383
|
+
# Box p: true in w iff for all accessible w', p holds
|
|
384
|
+
sat_p = self.sat(node.left)
|
|
385
|
+
return {w for w in all_w if self.kripke.get_successors(w).issubset(sat_p)}
|
|
386
|
+
|
|
387
|
+
if node.op == CTLOperator.DIAMOND and node.left:
|
|
388
|
+
# Diamond p: true in w iff exists accessible w' where p holds
|
|
389
|
+
sat_p = self.sat(node.left)
|
|
390
|
+
return {w for w in all_w if bool(self.kripke.get_successors(w) & sat_p)}
|
|
391
|
+
|
|
392
|
+
# CTL Next State Operators
|
|
393
|
+
if node.op == CTLOperator.EX and node.left:
|
|
394
|
+
sat_p = self.sat(node.left)
|
|
395
|
+
return self._pre_exists(sat_p)
|
|
396
|
+
|
|
397
|
+
if node.op == CTLOperator.AX and node.left:
|
|
398
|
+
sat_p = self.sat(node.left)
|
|
399
|
+
return self._pre_all(sat_p)
|
|
400
|
+
|
|
401
|
+
# CTL Fixed-Point Operators
|
|
402
|
+
if node.op == CTLOperator.EF and node.left:
|
|
403
|
+
# EF(p) = mu Z. (p | EX(Z)) [Least Fixed Point]
|
|
404
|
+
sat_p = self.sat(node.left)
|
|
405
|
+
z = set(sat_p)
|
|
406
|
+
while True:
|
|
407
|
+
nxt = z | self._pre_exists(z)
|
|
408
|
+
if nxt == z:
|
|
409
|
+
return z
|
|
410
|
+
z = nxt
|
|
411
|
+
|
|
412
|
+
if node.op == CTLOperator.AF and node.left:
|
|
413
|
+
# AF(p) = mu Z. (p | AX(Z)) [Least Fixed Point]
|
|
414
|
+
sat_p = self.sat(node.left)
|
|
415
|
+
z = set(sat_p)
|
|
416
|
+
while True:
|
|
417
|
+
nxt = z | self._pre_all(z)
|
|
418
|
+
if nxt == z:
|
|
419
|
+
return z
|
|
420
|
+
z = nxt
|
|
421
|
+
|
|
422
|
+
if node.op == CTLOperator.EG and node.left:
|
|
423
|
+
# EG(p) = nu Z. (p & EX(Z)) [Greatest Fixed Point]
|
|
424
|
+
sat_p = self.sat(node.left)
|
|
425
|
+
z = set(sat_p)
|
|
426
|
+
while True:
|
|
427
|
+
nxt = z & self._pre_exists(z)
|
|
428
|
+
if nxt == z:
|
|
429
|
+
return z
|
|
430
|
+
z = nxt
|
|
431
|
+
|
|
432
|
+
if node.op == CTLOperator.AG and node.left:
|
|
433
|
+
# AG(p) = nu Z. (p & AX(Z)) [Greatest Fixed Point]
|
|
434
|
+
sat_p = self.sat(node.left)
|
|
435
|
+
z = set(sat_p)
|
|
436
|
+
while True:
|
|
437
|
+
nxt = z & self._pre_all(z)
|
|
438
|
+
if nxt == z:
|
|
439
|
+
return z
|
|
440
|
+
z = nxt
|
|
441
|
+
|
|
442
|
+
if node.op == CTLOperator.EU and node.left and node.right:
|
|
443
|
+
# E[p U q] = mu Z. (q | (p & EX(Z))) [Least Fixed Point]
|
|
444
|
+
sat_p = self.sat(node.left)
|
|
445
|
+
sat_q = self.sat(node.right)
|
|
446
|
+
z = set(sat_q)
|
|
447
|
+
while True:
|
|
448
|
+
nxt = z | (sat_p & self._pre_exists(z))
|
|
449
|
+
if nxt == z:
|
|
450
|
+
return z
|
|
451
|
+
z = nxt
|
|
452
|
+
|
|
453
|
+
if node.op == CTLOperator.AU and node.left and node.right:
|
|
454
|
+
# A[p U q] = mu Z. (q | (p & AX(Z))) [Least Fixed Point]
|
|
455
|
+
sat_p = self.sat(node.left)
|
|
456
|
+
sat_q = self.sat(node.right)
|
|
457
|
+
z = set(sat_q)
|
|
458
|
+
while True:
|
|
459
|
+
nxt = z | (sat_p & self._pre_all(z))
|
|
460
|
+
if nxt == z:
|
|
461
|
+
return z
|
|
462
|
+
z = nxt
|
|
463
|
+
|
|
464
|
+
return set()
|
|
465
|
+
|
|
466
|
+
def _pre_exists(self, target_set: Set[str]) -> Set[str]:
|
|
467
|
+
"""Compute Pre_exists(S) = { w in W | exists w' in S. (w, w') in R }."""
|
|
468
|
+
pre = set()
|
|
469
|
+
for w in self.kripke.worlds:
|
|
470
|
+
if bool(self.kripke.get_successors(w) & target_set):
|
|
471
|
+
pre.add(w)
|
|
472
|
+
return pre
|
|
473
|
+
|
|
474
|
+
def _pre_all(self, target_set: Set[str]) -> Set[str]:
|
|
475
|
+
"""Compute Pre_all(S) = { w in W | forall w'. (w, w') in R => w' in S }."""
|
|
476
|
+
pre = set()
|
|
477
|
+
for w in self.kripke.worlds:
|
|
478
|
+
succ = self.kripke.get_successors(w)
|
|
479
|
+
if succ and succ.issubset(target_set):
|
|
480
|
+
pre.add(w)
|
|
481
|
+
return pre
|
|
482
|
+
|
|
483
|
+
def _generate_counterexample(
|
|
484
|
+
self,
|
|
485
|
+
node: FormulaNode,
|
|
486
|
+
start_world: str,
|
|
487
|
+
sat_worlds: Set[str],
|
|
488
|
+
) -> Optional[List[str]]:
|
|
489
|
+
"""Find shortest counterexample trace demonstrating violation."""
|
|
490
|
+
if node.op == CTLOperator.AG and node.left:
|
|
491
|
+
# Violation of AG(p): find shortest path to a world w not in Sat(p)
|
|
492
|
+
target_violators = set(self.kripke.worlds.keys()) - self.sat(node.left)
|
|
493
|
+
return self._find_shortest_path(start_world, target_violators)
|
|
494
|
+
|
|
495
|
+
if node.op == CTLOperator.AX and node.left:
|
|
496
|
+
sat_p = self.sat(node.left)
|
|
497
|
+
for succ in self.kripke.get_successors(start_world):
|
|
498
|
+
if succ not in sat_p:
|
|
499
|
+
return [start_world, succ]
|
|
500
|
+
|
|
501
|
+
if node.op == CTLOperator.AF and node.left:
|
|
502
|
+
# Violation of AF(p): world never reaches p
|
|
503
|
+
return [start_world]
|
|
504
|
+
|
|
505
|
+
return [start_world]
|
|
506
|
+
|
|
507
|
+
def _generate_witness(
|
|
508
|
+
self,
|
|
509
|
+
node: FormulaNode,
|
|
510
|
+
start_world: str,
|
|
511
|
+
sat_worlds: Set[str],
|
|
512
|
+
) -> Optional[List[str]]:
|
|
513
|
+
"""Find shortest witness trace demonstrating satisfiability."""
|
|
514
|
+
if node.op == CTLOperator.EF and node.left:
|
|
515
|
+
target_sat = self.sat(node.left)
|
|
516
|
+
return self._find_shortest_path(start_world, target_sat)
|
|
517
|
+
|
|
518
|
+
if node.op == CTLOperator.EX and node.left:
|
|
519
|
+
sat_p = self.sat(node.left)
|
|
520
|
+
for succ in self.kripke.get_successors(start_world):
|
|
521
|
+
if succ in sat_p:
|
|
522
|
+
return [start_world, succ]
|
|
523
|
+
|
|
524
|
+
if node.op == CTLOperator.EU and node.left and node.right:
|
|
525
|
+
target_q = self.sat(node.right)
|
|
526
|
+
return self._find_shortest_path(start_world, target_q)
|
|
527
|
+
|
|
528
|
+
return [start_world]
|
|
529
|
+
|
|
530
|
+
def _find_shortest_path(self, start_world: str, targets: Set[str]) -> Optional[List[str]]:
|
|
531
|
+
"""BFS shortest path from start_world to any node in targets."""
|
|
532
|
+
if start_world in targets:
|
|
533
|
+
return [start_world]
|
|
534
|
+
|
|
535
|
+
queue: List[List[str]] = [[start_world]]
|
|
536
|
+
visited: Set[str] = {start_world}
|
|
537
|
+
|
|
538
|
+
while queue:
|
|
539
|
+
path = queue.pop(0)
|
|
540
|
+
curr = path[-1]
|
|
541
|
+
for succ in self.kripke.get_successors(curr):
|
|
542
|
+
if succ in targets:
|
|
543
|
+
return path + [succ]
|
|
544
|
+
if succ not in visited:
|
|
545
|
+
visited.add(succ)
|
|
546
|
+
queue.append(path + [succ])
|
|
547
|
+
|
|
548
|
+
return None
|