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 +102 -0
- flake/actions.py +41 -0
- flake/agent.py +107 -0
- flake/dynamic.py +449 -0
- flake/epistemic.py +2068 -0
- flake/execution.py +892 -0
- flake/explorer.py +196 -0
- flake/formula.py +173 -0
- flake/observation.py +500 -0
- flake/process.py +342 -0
- flake/propositions.py +46 -0
- flake/py.typed +0 -0
- flake/runtime.py +370 -0
- flake/semantics.py +222 -0
- flake/sos.py +97 -0
- flake/temporal.py +109 -0
- flake/verify.py +1081 -0
- flakepl/__init__.py +8 -0
- flakepl/_api.py +29 -0
- flakepl/agent.py +9 -0
- flakepl/explorer.py +19 -0
- flakepl/process.py +31 -0
- flakepl/py.typed +0 -0
- flakepl/runtime.py +20 -0
- flakepl/semantics.py +7 -0
- flakepl/sos.py +5 -0
- flakepl/verify.py +29 -0
- flakepl-0.3.1.dist-info/METADATA +126 -0
- flakepl-0.3.1.dist-info/RECORD +31 -0
- flakepl-0.3.1.dist-info/WHEEL +5 -0
- flakepl-0.3.1.dist-info/top_level.txt +2 -0
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
|
+
)
|