| """ |
| BΓΌchi automata for common LTL spec patterns. |
| Implemented directly β no external tool needed. |
| |
| Supported patterns: |
| G(!p) β safety: never visit p |
| GF(p) β recurrence: visit p infinitely often |
| F(p) β reachability: eventually visit p |
| G(p) β invariance: always be in p |
| |
| Each automaton is a dict-based structure with: |
| states : list of state names |
| initial : initial state name |
| delta : (state, label_frozenset) -> next_state | None (None = sink/stuck) |
| accepting : set of accepting states |
| """ |
|
|
| from typing import Callable, FrozenSet, NamedTuple, Optional |
|
|
|
|
| class BuchiAut: |
| def __init__(self, states, initial, delta_fn, accepting, name=""): |
| self.states = states |
| self.initial = initial |
| self._delta = delta_fn |
| self.accepting = set(accepting) |
| self.name = name |
|
|
| def step(self, state, label: FrozenSet[str]) -> Optional[object]: |
| return self._delta(state, label) |
|
|
| def is_accepting(self, state) -> bool: |
| return state in self.accepting |
|
|
|
|
| |
|
|
| def safety(prop: str, label: str = "") -> BuchiAut: |
| """G(!prop) β automaton rejects if prop is ever true.""" |
| |
| def delta(state, lbl): |
| if state == "q0": |
| return "q_sink" if prop in lbl else "q0" |
| return "q_sink" |
|
|
| return BuchiAut( |
| states=["q0", "q_sink"], |
| initial="q0", |
| delta_fn=delta, |
| accepting=["q0"], |
| name=label or f"G(!{prop})", |
| ) |
|
|
|
|
| def recurrence(prop: str, label: str = "") -> BuchiAut: |
| """GF(prop) β must visit prop infinitely often.""" |
| |
| def delta(state, lbl): |
| if state == "q0": |
| return "q1" if prop in lbl else "q0" |
| |
| return "q0" |
|
|
| return BuchiAut( |
| states=["q0", "q1"], |
| initial="q0", |
| delta_fn=delta, |
| accepting=["q1"], |
| name=label or f"GF({prop})", |
| ) |
|
|
|
|
| def reachability(prop: str, label: str = "") -> BuchiAut: |
| """F(prop) β eventually reach prop (then stay accepting).""" |
| |
| def delta(state, lbl): |
| if state == "q0": |
| return "q1" if prop in lbl else "q0" |
| return "q1" |
|
|
| return BuchiAut( |
| states=["q0", "q1"], |
| initial="q0", |
| delta_fn=delta, |
| accepting=["q1"], |
| name=label or f"F({prop})", |
| ) |
|
|
|
|
| def invariance(prop: str, label: str = "") -> BuchiAut: |
| """G(prop) β always be in prop.""" |
| def delta(state, lbl): |
| if state == "q0": |
| return "q0" if prop in lbl else "q_sink" |
| return "q_sink" |
|
|
| return BuchiAut( |
| states=["q0", "q_sink"], |
| initial="q0", |
| delta_fn=delta, |
| accepting=["q0"], |
| name=label or f"G({prop})", |
| ) |
|
|
|
|
| |
|
|
| def parse_spec(formula: str, label: str = "") -> BuchiAut: |
| """ |
| Parse simple LTL formula string into a BuchiAut. |
| Supported: |
| G(!p) β safety |
| GF(p) β recurrence |
| F(p) β reachability |
| G(p) β invariance |
| """ |
| f = formula.strip().replace(" ", "") |
| lbl = label or formula |
|
|
| if f.startswith("GF(") and f.endswith(")"): |
| return recurrence(f[3:-1], lbl) |
| if f.startswith("G(!") and f.endswith(")"): |
| return safety(f[3:-1], lbl) |
| if f.startswith("G(") and f.endswith(")"): |
| return invariance(f[2:-1], lbl) |
| if f.startswith("F(") and f.endswith(")"): |
| return reachability(f[2:-1], lbl) |
|
|
| raise ValueError( |
| f"Unsupported formula: '{formula}'. " |
| "Supported: G(!p), GF(p), G(p), F(p)" |
| ) |
|
|