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/__init__.py ADDED
@@ -0,0 +1,102 @@
1
+ from .actions import Action, Receive, Send, Tau
2
+ from .agent import Agent
3
+ from .dynamic import (
4
+ ActionEvent,
5
+ ActionModel,
6
+ DynamicEvent,
7
+ PublicAnnouncement,
8
+ action_event,
9
+ announce,
10
+ event,
11
+ product_update,
12
+ public_announcement,
13
+ )
14
+ from .epistemic import (
15
+ CommonKnowledge,
16
+ DistributedKnowledge,
17
+ EpistemicModel,
18
+ EveryoneKnows,
19
+ Knowledge,
20
+ ProductWorld,
21
+ World,
22
+ C,
23
+ D,
24
+ E,
25
+ K,
26
+ )
27
+ from .explorer import StateSpace, explore, explore_many, explore_many_with, explore_with
28
+ from .execution import (
29
+ ActionObservation,
30
+ BlindActionObservation,
31
+ ProcessExecutionEvent,
32
+ TransitionObservation,
33
+ blind_action,
34
+ execute,
35
+ execute_transition,
36
+ observe_action,
37
+ step,
38
+ )
39
+ from .formula import And, Atom, Formula, Not, Or, iff, implies
40
+ from .observation import (
41
+ GlobalObservation,
42
+ LocalObservation,
43
+ ObservationBuilder,
44
+ global_,
45
+ local,
46
+ observe_global,
47
+ observe_local_process,
48
+ observe_local_state,
49
+ public,
50
+ same_observation,
51
+ )
52
+ from .process import Choice, Nil, Parallel, Prefix, Process, choice, parallel, recv, send, stop, tau
53
+ from .propositions import Proposition
54
+ from .runtime import Configuration, GlobalState, SystemTransition
55
+ from .semantics import synchronous_system_transitions
56
+ from .sos import Transition, transitions
57
+ from .temporal import Always, Eventually, Next, Until, F, G, U, X
58
+ from .verify import (
59
+ Trace,
60
+ VerificationResult,
61
+ find_always_counterexample,
62
+ find_eventually_counterexample,
63
+ find_next_counterexample,
64
+ find_path_to,
65
+ find_state_counterexample,
66
+ format_trace,
67
+ format_verification,
68
+ verify,
69
+ verify_always,
70
+ verify_eventually,
71
+ )
72
+
73
+ # Backward-compatible system-level name.
74
+ system_transitions = synchronous_system_transitions
75
+
76
+ __all__ = [
77
+ "Action", "Tau", "Send", "Receive",
78
+ "Agent",
79
+ "Process", "Nil", "Prefix", "Choice", "Parallel",
80
+ "stop", "send", "recv", "tau", "choice", "parallel",
81
+ "GlobalState", "Configuration", "SystemTransition",
82
+ "Proposition",
83
+ "Transition", "transitions",
84
+ "StateSpace", "explore", "explore_many", "explore_with", "explore_many_with",
85
+ "system_transitions", "synchronous_system_transitions",
86
+ "LocalObservation", "GlobalObservation", "ObservationBuilder",
87
+ "local", "global_", "public", "observe_global", "observe_local_state",
88
+ "observe_local_process", "same_observation",
89
+ "Formula", "Atom", "Not", "And", "Or", "implies", "iff",
90
+ "World", "ProductWorld", "EpistemicModel",
91
+ "Knowledge", "EveryoneKnows", "DistributedKnowledge", "CommonKnowledge",
92
+ "K", "E", "D", "C",
93
+ "Next", "Eventually", "Always", "Until", "X", "F", "G", "U",
94
+ "DynamicEvent", "PublicAnnouncement", "ActionEvent", "ActionModel",
95
+ "announce", "public_announcement", "event", "action_event", "product_update",
96
+ "TransitionObservation", "ActionObservation", "BlindActionObservation",
97
+ "ProcessExecutionEvent", "observe_action", "blind_action", "execute_transition",
98
+ "execute", "step",
99
+ "Trace", "VerificationResult", "find_path_to", "find_state_counterexample",
100
+ "find_next_counterexample", "find_always_counterexample", "find_eventually_counterexample",
101
+ "verify_always", "verify_eventually", "verify", "format_trace", "format_verification",
102
+ ]
flake/actions.py ADDED
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+
7
+ @dataclass(frozen=True, slots=True)
8
+ class Tau:
9
+ """
10
+ Internal action.
11
+
12
+ Represents:
13
+ τ
14
+ """
15
+
16
+
17
+ @dataclass(frozen=True, slots=True)
18
+ class Send:
19
+ """
20
+ Output action:
21
+
22
+ channel!value
23
+ """
24
+
25
+ channel: str
26
+ value: Any
27
+
28
+
29
+ @dataclass(frozen=True, slots=True)
30
+ class Receive:
31
+ """
32
+ Input action:
33
+
34
+ channel?variable
35
+ """
36
+
37
+ channel: str
38
+ variable: str
39
+
40
+
41
+ Action = Tau | Send | Receive
flake/agent.py ADDED
@@ -0,0 +1,107 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+ from .process import Process
7
+
8
+
9
+ @dataclass(frozen=True, slots=True)
10
+ class Agent:
11
+ """
12
+ Runtime agent.
13
+
14
+ Agent identity is its logical name.
15
+
16
+ Runtime state consists of:
17
+
18
+ process
19
+ local state
20
+ """
21
+
22
+ name: str
23
+ process: Process
24
+ state: tuple[tuple[str, Any], ...] = ()
25
+
26
+ @classmethod
27
+ def create(
28
+ cls,
29
+ name: str,
30
+ process: Process,
31
+ state: dict[str, Any] | None = None,
32
+ ) -> Agent:
33
+
34
+ if not name:
35
+ raise ValueError(
36
+ "Agent name cannot be empty"
37
+ )
38
+
39
+ if state is None:
40
+ state = {}
41
+
42
+ return cls(
43
+ name=name,
44
+ process=process,
45
+ state=tuple(
46
+ sorted(
47
+ state.items(),
48
+ key=lambda item: item[0],
49
+ )
50
+ ),
51
+ )
52
+
53
+ def get_state(
54
+ self,
55
+ key: str,
56
+ default: Any = None,
57
+ ) -> Any:
58
+ """
59
+ Read one local state variable.
60
+ """
61
+
62
+ return dict(
63
+ self.state
64
+ ).get(
65
+ key,
66
+ default,
67
+ )
68
+
69
+ def with_state(
70
+ self,
71
+ key: str,
72
+ value: Any,
73
+ ) -> Agent:
74
+ """
75
+ Return a new agent with updated local state.
76
+ """
77
+
78
+ state = dict(
79
+ self.state
80
+ )
81
+
82
+ state[key] = value
83
+
84
+ return Agent(
85
+ name=self.name,
86
+ process=self.process,
87
+ state=tuple(
88
+ sorted(
89
+ state.items(),
90
+ key=lambda item: item[0],
91
+ )
92
+ ),
93
+ )
94
+
95
+ def with_process(
96
+ self,
97
+ process: Process,
98
+ ) -> Agent:
99
+ """
100
+ Return a new agent with updated process.
101
+ """
102
+
103
+ return Agent(
104
+ name=self.name,
105
+ process=process,
106
+ state=self.state,
107
+ )
flake/dynamic.py ADDED
@@ -0,0 +1,449 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import (
5
+ Iterable,
6
+ Mapping,
7
+ Protocol,
8
+ TYPE_CHECKING,
9
+ )
10
+
11
+ from .formula import Formula
12
+
13
+ if TYPE_CHECKING:
14
+ from .epistemic import EpistemicModel
15
+
16
+
17
+ class DynamicEvent(Protocol):
18
+ """
19
+ Dynamic epistemic event.
20
+
21
+ Applying an event creates a new epistemic model.
22
+ """
23
+
24
+ def apply(
25
+ self,
26
+ model: "EpistemicModel",
27
+ ) -> "EpistemicModel":
28
+ ...
29
+
30
+
31
+ # ============================================================================
32
+ # Public announcement
33
+ # ============================================================================
34
+
35
+
36
+ @dataclass(frozen=True, slots=True)
37
+ class PublicAnnouncement:
38
+ """
39
+ Public Announcement Logic event.
40
+
41
+ Every world where the formula is false is removed.
42
+
43
+ The original model is never mutated.
44
+ """
45
+
46
+ formula: Formula
47
+
48
+ def apply(
49
+ self,
50
+ model: "EpistemicModel",
51
+ ) -> "EpistemicModel":
52
+ return model._apply_public_announcement(
53
+ self.formula
54
+ )
55
+
56
+ def __repr__(self) -> str:
57
+ return (
58
+ f"PublicAnnouncement({self.formula!r})"
59
+ )
60
+
61
+
62
+ def announce(
63
+ formula: Formula,
64
+ ) -> PublicAnnouncement:
65
+ """
66
+ Construct a public announcement.
67
+
68
+ Example:
69
+
70
+ model2 = model.update(
71
+ announce(p)
72
+ )
73
+ """
74
+
75
+ return PublicAnnouncement(
76
+ formula
77
+ )
78
+
79
+
80
+ def public_announcement(
81
+ formula: Formula,
82
+ ) -> PublicAnnouncement:
83
+ """
84
+ Explicit alias for announce().
85
+ """
86
+
87
+ return announce(formula)
88
+
89
+
90
+ # ============================================================================
91
+ # Action events
92
+ # ============================================================================
93
+
94
+
95
+ @dataclass(frozen=True, slots=True)
96
+ class ActionEvent:
97
+ """
98
+ One event point of an epistemic action model.
99
+
100
+ name:
101
+ Event identifier.
102
+
103
+ precondition:
104
+ Formula that must hold for the event to be executable.
105
+
106
+ None means that the event is executable in every world.
107
+ """
108
+
109
+ name: str
110
+ precondition: Formula | None = None
111
+
112
+ def __post_init__(self) -> None:
113
+ if not self.name.strip():
114
+ raise ValueError(
115
+ "ActionEvent name must not be empty"
116
+ )
117
+
118
+ def enabled(
119
+ self,
120
+ model: "EpistemicModel",
121
+ world,
122
+ ) -> bool:
123
+ """
124
+ Check whether this event is executable at a world.
125
+ """
126
+
127
+ if self.precondition is None:
128
+ return True
129
+
130
+ return model.check(
131
+ self.precondition,
132
+ at=world,
133
+ )
134
+
135
+ def __repr__(self) -> str:
136
+ if self.precondition is None:
137
+ return (
138
+ f"ActionEvent({self.name!r})"
139
+ )
140
+
141
+ return (
142
+ f"ActionEvent("
143
+ f"{self.name!r}, "
144
+ f"precondition={self.precondition!r}"
145
+ f")"
146
+ )
147
+
148
+
149
+ def event(
150
+ name: str,
151
+ precondition: Formula | None = None,
152
+ ) -> ActionEvent:
153
+ """
154
+ Construct an action-model event.
155
+
156
+ Examples:
157
+
158
+ event("learn", K("alice", p))
159
+
160
+ event("noop")
161
+ """
162
+
163
+ return ActionEvent(
164
+ name=name,
165
+ precondition=precondition,
166
+ )
167
+
168
+
169
+ def action_event(
170
+ name: str,
171
+ precondition: Formula | None = None,
172
+ ) -> ActionEvent:
173
+ """
174
+ Explicit alias for event().
175
+ """
176
+
177
+ return event(
178
+ name,
179
+ precondition,
180
+ )
181
+
182
+
183
+ # ============================================================================
184
+ # Action model
185
+ # ============================================================================
186
+
187
+
188
+ @dataclass(slots=True)
189
+ class ActionModel:
190
+ """
191
+ Finite epistemic action model.
192
+
193
+ An ActionModel contains:
194
+
195
+ events
196
+ preconditions
197
+ actual event
198
+ event indistinguishability relations
199
+
200
+ relations:
201
+
202
+ relations[agent][event] = events that agent
203
+ considers possible after observing that event.
204
+
205
+ If no relation is supplied for an agent, all events are
206
+ considered indistinguishable for that agent.
207
+
208
+ Example:
209
+
210
+ action_model = ActionModel(
211
+ events=(
212
+ event("learn", p),
213
+ event("noop", ~p),
214
+ ),
215
+ actual="learn",
216
+ relations={
217
+ "alice": {
218
+ "learn": {"learn"},
219
+ "noop": {"noop"},
220
+ },
221
+ "bob": {
222
+ "learn": {"learn", "noop"},
223
+ "noop": {"learn", "noop"},
224
+ },
225
+ },
226
+ )
227
+ """
228
+
229
+ events: tuple[ActionEvent, ...]
230
+ actual: str
231
+ relations: (
232
+ Mapping[
233
+ str,
234
+ Mapping[
235
+ str,
236
+ Iterable[str],
237
+ ],
238
+ ]
239
+ | None
240
+ ) = None
241
+
242
+ def __post_init__(self) -> None:
243
+ self.events = tuple(
244
+ self.events
245
+ )
246
+
247
+ if not self.events:
248
+ raise ValueError(
249
+ "ActionModel requires at least one event"
250
+ )
251
+
252
+ names = [
253
+ item.name
254
+ for item in self.events
255
+ ]
256
+
257
+ if len(names) != len(set(names)):
258
+ raise ValueError(
259
+ "ActionEvent names must be unique"
260
+ )
261
+
262
+ if self.actual not in names:
263
+ raise ValueError(
264
+ f"Unknown actual event {self.actual!r}"
265
+ )
266
+
267
+ event_names = set(names)
268
+
269
+ normalized: dict[
270
+ str,
271
+ dict[
272
+ str,
273
+ frozenset[str],
274
+ ],
275
+ ] = {}
276
+
277
+ if self.relations is not None:
278
+ for agent, relation in self.relations.items():
279
+ if not isinstance(agent, str):
280
+ raise TypeError(
281
+ "ActionModel agent names must be strings"
282
+ )
283
+
284
+ agent_name = agent.strip()
285
+
286
+ if not agent_name:
287
+ raise ValueError(
288
+ "ActionModel agent name must not be empty"
289
+ )
290
+
291
+ if agent_name in normalized:
292
+ raise ValueError(
293
+ f"Duplicate action relation for "
294
+ f"{agent_name!r}"
295
+ )
296
+
297
+ source_map: dict[
298
+ str,
299
+ frozenset[str],
300
+ ] = {}
301
+
302
+ for source, targets in relation.items():
303
+ if source not in event_names:
304
+ raise ValueError(
305
+ f"Unknown event {source!r} "
306
+ f"in relation for {agent_name!r}"
307
+ )
308
+
309
+ target_set = frozenset(
310
+ targets
311
+ )
312
+
313
+ unknown_targets = (
314
+ target_set - event_names
315
+ )
316
+
317
+ if unknown_targets:
318
+ unknown_text = ", ".join(
319
+ sorted(unknown_targets)
320
+ )
321
+
322
+ raise ValueError(
323
+ f"Unknown target event(s) "
324
+ f"{unknown_text!r} "
325
+ f"in relation for {agent_name!r}"
326
+ )
327
+
328
+ source_map[source] = (
329
+ target_set
330
+ )
331
+
332
+ missing_sources = (
333
+ event_names - set(source_map)
334
+ )
335
+
336
+ if missing_sources:
337
+ missing_text = ", ".join(
338
+ sorted(missing_sources)
339
+ )
340
+
341
+ raise ValueError(
342
+ f"Missing action relation(s) "
343
+ f"for {agent_name!r}: "
344
+ f"{missing_text}"
345
+ )
346
+
347
+ normalized[agent_name] = (
348
+ source_map
349
+ )
350
+
351
+ self.relations = normalized
352
+
353
+ # ---------------------------------------------------------------------
354
+ # Event API
355
+ # ---------------------------------------------------------------------
356
+
357
+ @property
358
+ def event_names(
359
+ self,
360
+ ) -> tuple[str, ...]:
361
+ return tuple(
362
+ item.name
363
+ for item in self.events
364
+ )
365
+
366
+ def event(
367
+ self,
368
+ name: str,
369
+ ) -> ActionEvent:
370
+ for item in self.events:
371
+ if item.name == name:
372
+ return item
373
+
374
+ raise ValueError(
375
+ f"Unknown action event {name!r}"
376
+ )
377
+
378
+ # ---------------------------------------------------------------------
379
+ # Event epistemic relation
380
+ # ---------------------------------------------------------------------
381
+
382
+ def accessible(
383
+ self,
384
+ agent: str,
385
+ event_name: str,
386
+ ) -> tuple[str, ...]:
387
+ """
388
+ Return events that the agent considers possible
389
+ after observing event_name.
390
+
391
+ If no relation is defined for this agent, all events
392
+ are considered indistinguishable.
393
+ """
394
+
395
+ self.event(event_name)
396
+
397
+ relation = self.relations.get(
398
+ agent
399
+ )
400
+
401
+ if relation is None:
402
+ return self.event_names
403
+
404
+ return tuple(
405
+ target
406
+ for target in self.event_names
407
+ if target in relation[event_name]
408
+ )
409
+
410
+ # ---------------------------------------------------------------------
411
+ # DynamicEvent protocol
412
+ # ---------------------------------------------------------------------
413
+
414
+ def apply(
415
+ self,
416
+ model: "EpistemicModel",
417
+ ) -> "EpistemicModel":
418
+ return model._apply_action_model(
419
+ self
420
+ )
421
+
422
+ def __repr__(self) -> str:
423
+ names = ", ".join(
424
+ self.event_names
425
+ )
426
+
427
+ return (
428
+ f"ActionModel("
429
+ f"events=[{names}], "
430
+ f"actual={self.actual!r}"
431
+ f")"
432
+ )
433
+
434
+
435
+ def product_update(
436
+ model: "EpistemicModel",
437
+ action_model: ActionModel,
438
+ ) -> "EpistemicModel":
439
+ """
440
+ Functional helper for product update.
441
+
442
+ Equivalent to:
443
+
444
+ model.product_update(action_model)
445
+ """
446
+
447
+ return model.product_update(
448
+ action_model
449
+ )