flakepl 0.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.
flake/explorer.py ADDED
@@ -0,0 +1,196 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import deque
4
+ from dataclasses import dataclass
5
+ from typing import Callable, Iterable
6
+
7
+ from .runtime import (
8
+ Configuration,
9
+ SystemTransition,
10
+ system_transitions,
11
+ )
12
+
13
+
14
+ TransitionProvider = Callable[
15
+ [Configuration],
16
+ Iterable[SystemTransition],
17
+ ]
18
+
19
+
20
+ @dataclass(frozen=True, slots=True)
21
+ class StateSpace:
22
+ """
23
+ Finite reachable state space.
24
+
25
+ initials:
26
+ Initial configurations.
27
+
28
+ states:
29
+ All discovered configurations.
30
+
31
+ transitions:
32
+ Reachable transitions between configurations.
33
+ """
34
+
35
+ initials: tuple[Configuration, ...]
36
+ states: tuple[Configuration, ...]
37
+ transitions: tuple[SystemTransition, ...]
38
+
39
+ @property
40
+ def initial(self) -> Configuration:
41
+ """
42
+ Convenience accessor for a single-initial state space.
43
+ """
44
+
45
+ if len(self.initials) != 1:
46
+ raise ValueError(
47
+ "StateSpace.initial requires exactly one "
48
+ "initial configuration"
49
+ )
50
+
51
+ return self.initials[0]
52
+
53
+
54
+ def _explore_many(
55
+ initials: Iterable[Configuration],
56
+ transition_provider: TransitionProvider,
57
+ max_states: int | None = None,
58
+ ) -> StateSpace:
59
+ initial_tuple = tuple(
60
+ initials
61
+ )
62
+
63
+ if not initial_tuple:
64
+ raise ValueError(
65
+ "At least one initial configuration is required"
66
+ )
67
+
68
+ if max_states is not None:
69
+ if max_states <= 0:
70
+ raise ValueError(
71
+ "max_states must be positive"
72
+ )
73
+
74
+ visited: set[Configuration] = set(
75
+ initial_tuple
76
+ )
77
+
78
+ queue: deque[Configuration] = deque(
79
+ initial_tuple
80
+ )
81
+
82
+ transitions_result: list[
83
+ SystemTransition
84
+ ] = []
85
+
86
+ while queue:
87
+ current = queue.popleft()
88
+
89
+ for transition in transition_provider(
90
+ current
91
+ ):
92
+ transitions_result.append(
93
+ transition
94
+ )
95
+
96
+ target = transition.target
97
+
98
+ if target in visited:
99
+ continue
100
+
101
+ if (
102
+ max_states is not None
103
+ and len(visited) >= max_states
104
+ ):
105
+ continue
106
+
107
+ visited.add(
108
+ target
109
+ )
110
+
111
+ queue.append(
112
+ target
113
+ )
114
+
115
+ states = tuple(
116
+ visited
117
+ )
118
+
119
+ return StateSpace(
120
+ initials=initial_tuple,
121
+ states=states,
122
+ transitions=tuple(
123
+ transitions_result
124
+ ),
125
+ )
126
+
127
+
128
+ def explore(
129
+ initial: Configuration,
130
+ max_states: int | None = None,
131
+ ) -> StateSpace:
132
+ """
133
+ Explore using the default runtime semantics.
134
+ """
135
+
136
+ return _explore_many(
137
+ (initial,),
138
+ system_transitions,
139
+ max_states=max_states,
140
+ )
141
+
142
+
143
+ def explore_many(
144
+ initials: Iterable[Configuration],
145
+ max_states: int | None = None,
146
+ ) -> StateSpace:
147
+ """
148
+ Explore multiple initial configurations using the
149
+ default runtime semantics.
150
+ """
151
+
152
+ return _explore_many(
153
+ initials,
154
+ system_transitions,
155
+ max_states=max_states,
156
+ )
157
+
158
+
159
+ def explore_with(
160
+ initial: Configuration,
161
+ transition_provider: TransitionProvider,
162
+ max_states: int | None = None,
163
+ ) -> StateSpace:
164
+ """
165
+ Explore using explicitly supplied operational semantics.
166
+
167
+ Example:
168
+
169
+ space = explore_with(
170
+ initial,
171
+ synchronous_system_transitions,
172
+ )
173
+ """
174
+
175
+ return _explore_many(
176
+ (initial,),
177
+ transition_provider,
178
+ max_states=max_states,
179
+ )
180
+
181
+
182
+ def explore_many_with(
183
+ initials: Iterable[Configuration],
184
+ transition_provider: TransitionProvider,
185
+ max_states: int | None = None,
186
+ ) -> StateSpace:
187
+ """
188
+ Explore multiple initial configurations using explicitly
189
+ supplied operational semantics.
190
+ """
191
+
192
+ return _explore_many(
193
+ initials,
194
+ transition_provider,
195
+ max_states=max_states,
196
+ )
flake/formula.py ADDED
@@ -0,0 +1,173 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ class Formula:
7
+ """
8
+ Base class for all FlakePL formulas.
9
+
10
+ Formula objects form an immutable abstract syntax tree.
11
+
12
+ Boolean composition:
13
+
14
+ phi & psi
15
+ phi | psi
16
+ ~phi
17
+
18
+ Derived operators:
19
+
20
+ phi.implies(psi)
21
+ phi.iff(psi)
22
+ """
23
+
24
+ def __and__(
25
+ self,
26
+ other: "Formula",
27
+ ) -> "Formula":
28
+ return And(
29
+ self,
30
+ other,
31
+ )
32
+
33
+ def __or__(
34
+ self,
35
+ other: "Formula",
36
+ ) -> "Formula":
37
+ return Or(
38
+ self,
39
+ other,
40
+ )
41
+
42
+ def __invert__(self) -> "Formula":
43
+ return Not(
44
+ self
45
+ )
46
+
47
+ def implies(
48
+ self,
49
+ other: "Formula",
50
+ ) -> "Formula":
51
+ """
52
+ Logical implication:
53
+
54
+ φ → ψ
55
+
56
+ represented as:
57
+
58
+ ¬φ ∨ ψ
59
+ """
60
+
61
+ return Or(
62
+ Not(self),
63
+ other,
64
+ )
65
+
66
+ def iff(
67
+ self,
68
+ other: "Formula",
69
+ ) -> "Formula":
70
+ """
71
+ Logical equivalence:
72
+
73
+ φ ↔ ψ
74
+ """
75
+
76
+ return And(
77
+ self.implies(other),
78
+ other.implies(self),
79
+ )
80
+
81
+
82
+ def implies(
83
+ left: Formula,
84
+ right: Formula,
85
+ ) -> Formula:
86
+ """
87
+ Functional form of implication.
88
+ """
89
+
90
+ return left.implies(
91
+ right
92
+ )
93
+
94
+
95
+ def iff(
96
+ left: Formula,
97
+ right: Formula,
98
+ ) -> Formula:
99
+ """
100
+ Functional form of logical equivalence.
101
+ """
102
+
103
+ return left.iff(
104
+ right
105
+ )
106
+
107
+
108
+ @dataclass(frozen=True, slots=True)
109
+ class Atom(Formula):
110
+ """
111
+ Atomic proposition.
112
+
113
+ Example:
114
+
115
+ Atom("ready")
116
+ """
117
+
118
+ name: str
119
+
120
+ def __post_init__(self) -> None:
121
+ if not self.name.strip():
122
+ raise ValueError(
123
+ "Atom name must not be empty"
124
+ )
125
+
126
+ def __repr__(self) -> str:
127
+ return self.name
128
+
129
+
130
+ @dataclass(frozen=True, slots=True)
131
+ class Not(Formula):
132
+ """
133
+ Boolean negation.
134
+ """
135
+
136
+ operand: Formula
137
+
138
+ def __repr__(self) -> str:
139
+ return (
140
+ f"¬({self.operand!r})"
141
+ )
142
+
143
+
144
+ @dataclass(frozen=True, slots=True)
145
+ class And(Formula):
146
+ """
147
+ Boolean conjunction.
148
+ """
149
+
150
+ left: Formula
151
+ right: Formula
152
+
153
+ def __repr__(self) -> str:
154
+ return (
155
+ f"({self.left!r} ∧ "
156
+ f"{self.right!r})"
157
+ )
158
+
159
+
160
+ @dataclass(frozen=True, slots=True)
161
+ class Or(Formula):
162
+ """
163
+ Boolean disjunction.
164
+ """
165
+
166
+ left: Formula
167
+ right: Formula
168
+
169
+ def __repr__(self) -> str:
170
+ return (
171
+ f"({self.left!r} ∨ "
172
+ f"{self.right!r})"
173
+ )