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/execution.py
ADDED
|
@@ -0,0 +1,892 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import (
|
|
5
|
+
Callable,
|
|
6
|
+
Hashable,
|
|
7
|
+
Iterable,
|
|
8
|
+
Mapping,
|
|
9
|
+
Protocol,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from .actions import (
|
|
13
|
+
Action,
|
|
14
|
+
Receive,
|
|
15
|
+
Send,
|
|
16
|
+
Tau,
|
|
17
|
+
)
|
|
18
|
+
from .agent import Agent
|
|
19
|
+
from .epistemic import (
|
|
20
|
+
EpistemicModel,
|
|
21
|
+
ProductWorld,
|
|
22
|
+
)
|
|
23
|
+
from .runtime import (
|
|
24
|
+
Configuration,
|
|
25
|
+
SystemTransition,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TransitionObservation(Protocol):
|
|
30
|
+
"""
|
|
31
|
+
Describes what an agent observes about a runtime transition.
|
|
32
|
+
|
|
33
|
+
The returned value must be hashable.
|
|
34
|
+
|
|
35
|
+
The observation is intentionally separate from the agent's
|
|
36
|
+
ordinary world observation. It describes the event itself.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __call__(
|
|
40
|
+
self,
|
|
41
|
+
agent: Agent,
|
|
42
|
+
transition: SystemTransition,
|
|
43
|
+
) -> Hashable:
|
|
44
|
+
...
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ============================================================================
|
|
48
|
+
# Action observations
|
|
49
|
+
# ============================================================================
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True, slots=True)
|
|
53
|
+
class ActionObservation:
|
|
54
|
+
"""
|
|
55
|
+
Declarative observation of a process-calculus action.
|
|
56
|
+
|
|
57
|
+
By default an agent observes:
|
|
58
|
+
|
|
59
|
+
action kind
|
|
60
|
+
channel
|
|
61
|
+
|
|
62
|
+
but not the payload.
|
|
63
|
+
|
|
64
|
+
Examples:
|
|
65
|
+
|
|
66
|
+
send("c", "secret")
|
|
67
|
+
-> ("kind", "send"), ("channel", "c")
|
|
68
|
+
|
|
69
|
+
recv("c", "x")
|
|
70
|
+
-> ("kind", "receive"), ("channel", "c")
|
|
71
|
+
|
|
72
|
+
tau()
|
|
73
|
+
-> ("kind", "tau")
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
include_kind: bool = True
|
|
77
|
+
include_channel: bool = True
|
|
78
|
+
include_value: bool = False
|
|
79
|
+
|
|
80
|
+
def __call__(
|
|
81
|
+
self,
|
|
82
|
+
agent: Agent,
|
|
83
|
+
transition: SystemTransition,
|
|
84
|
+
) -> Hashable:
|
|
85
|
+
action = transition.action
|
|
86
|
+
|
|
87
|
+
if isinstance(action, Send):
|
|
88
|
+
kind = "send"
|
|
89
|
+
|
|
90
|
+
elif isinstance(action, Receive):
|
|
91
|
+
kind = "receive"
|
|
92
|
+
|
|
93
|
+
elif isinstance(action, Tau):
|
|
94
|
+
kind = "tau"
|
|
95
|
+
|
|
96
|
+
else:
|
|
97
|
+
kind = type(action).__name__.lower()
|
|
98
|
+
|
|
99
|
+
parts: list[tuple[str, Hashable]] = []
|
|
100
|
+
|
|
101
|
+
if self.include_kind:
|
|
102
|
+
parts.append(
|
|
103
|
+
(
|
|
104
|
+
"kind",
|
|
105
|
+
kind,
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
if self.include_channel and isinstance(
|
|
110
|
+
action,
|
|
111
|
+
(
|
|
112
|
+
Send,
|
|
113
|
+
Receive,
|
|
114
|
+
),
|
|
115
|
+
):
|
|
116
|
+
parts.append(
|
|
117
|
+
(
|
|
118
|
+
"channel",
|
|
119
|
+
action.channel,
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
if self.include_value:
|
|
124
|
+
if isinstance(
|
|
125
|
+
action,
|
|
126
|
+
Send,
|
|
127
|
+
):
|
|
128
|
+
parts.append(
|
|
129
|
+
(
|
|
130
|
+
"value",
|
|
131
|
+
action.value,
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
elif isinstance(
|
|
136
|
+
action,
|
|
137
|
+
Receive,
|
|
138
|
+
):
|
|
139
|
+
parts.append(
|
|
140
|
+
(
|
|
141
|
+
"variable",
|
|
142
|
+
action.variable,
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
result = tuple(parts)
|
|
147
|
+
|
|
148
|
+
hash(result)
|
|
149
|
+
|
|
150
|
+
return result
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def observe_action(
|
|
154
|
+
*,
|
|
155
|
+
kind: bool = True,
|
|
156
|
+
channel: bool = True,
|
|
157
|
+
value: bool = False,
|
|
158
|
+
) -> ActionObservation:
|
|
159
|
+
"""
|
|
160
|
+
Create an action observation policy.
|
|
161
|
+
|
|
162
|
+
Example:
|
|
163
|
+
|
|
164
|
+
observe_action(
|
|
165
|
+
kind=True,
|
|
166
|
+
channel=True,
|
|
167
|
+
value=False,
|
|
168
|
+
)
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
return ActionObservation(
|
|
172
|
+
include_kind=kind,
|
|
173
|
+
include_channel=channel,
|
|
174
|
+
include_value=value,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@dataclass(frozen=True, slots=True)
|
|
179
|
+
class BlindActionObservation:
|
|
180
|
+
"""
|
|
181
|
+
Observation policy where the agent cannot distinguish
|
|
182
|
+
runtime actions based on the event itself.
|
|
183
|
+
|
|
184
|
+
Information may still be obtained from the resulting world.
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
def __call__(
|
|
188
|
+
self,
|
|
189
|
+
agent: Agent,
|
|
190
|
+
transition: SystemTransition,
|
|
191
|
+
) -> Hashable:
|
|
192
|
+
return (
|
|
193
|
+
"blind",
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def blind_action() -> BlindActionObservation:
|
|
198
|
+
"""
|
|
199
|
+
Construct a completely blind action observation.
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
return BlindActionObservation()
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# ============================================================================
|
|
206
|
+
# Observation mapping
|
|
207
|
+
# ============================================================================
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
EventObservationMap = Mapping[
|
|
211
|
+
str | Agent,
|
|
212
|
+
TransitionObservation,
|
|
213
|
+
]
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _normalize_event_observations(
|
|
217
|
+
observations: (
|
|
218
|
+
EventObservationMap
|
|
219
|
+
| TransitionObservation
|
|
220
|
+
| None
|
|
221
|
+
),
|
|
222
|
+
agent_names: Iterable[str],
|
|
223
|
+
) -> dict[
|
|
224
|
+
str,
|
|
225
|
+
TransitionObservation,
|
|
226
|
+
]:
|
|
227
|
+
names = tuple(agent_names)
|
|
228
|
+
|
|
229
|
+
if observations is None:
|
|
230
|
+
default = observe_action()
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
name: default
|
|
234
|
+
for name in names
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if callable(observations) and not isinstance(
|
|
238
|
+
observations,
|
|
239
|
+
Mapping,
|
|
240
|
+
):
|
|
241
|
+
return {
|
|
242
|
+
name: observations
|
|
243
|
+
for name in names
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if not isinstance(
|
|
247
|
+
observations,
|
|
248
|
+
Mapping,
|
|
249
|
+
):
|
|
250
|
+
raise TypeError(
|
|
251
|
+
"Event observations must be a mapping, "
|
|
252
|
+
"callable, or None"
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
result: dict[
|
|
256
|
+
str,
|
|
257
|
+
TransitionObservation,
|
|
258
|
+
] = {}
|
|
259
|
+
|
|
260
|
+
for key, observation in observations.items():
|
|
261
|
+
name = (
|
|
262
|
+
key.name
|
|
263
|
+
if isinstance(
|
|
264
|
+
key,
|
|
265
|
+
Agent,
|
|
266
|
+
)
|
|
267
|
+
else key
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
if not isinstance(
|
|
271
|
+
name,
|
|
272
|
+
str,
|
|
273
|
+
):
|
|
274
|
+
raise TypeError(
|
|
275
|
+
"Event observation keys must be "
|
|
276
|
+
"strings or Agent objects"
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
name = name.strip()
|
|
280
|
+
|
|
281
|
+
if not name:
|
|
282
|
+
raise ValueError(
|
|
283
|
+
"Event observation agent name "
|
|
284
|
+
"must not be empty"
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
if not callable(observation):
|
|
288
|
+
raise TypeError(
|
|
289
|
+
f"Event observation for {name!r} "
|
|
290
|
+
f"must be callable"
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
result[name] = observation
|
|
294
|
+
|
|
295
|
+
expected = set(names)
|
|
296
|
+
actual = set(result)
|
|
297
|
+
|
|
298
|
+
missing = expected - actual
|
|
299
|
+
extra = actual - expected
|
|
300
|
+
|
|
301
|
+
if missing:
|
|
302
|
+
missing_text = ", ".join(
|
|
303
|
+
sorted(missing)
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
raise ValueError(
|
|
307
|
+
f"Missing event observations for agents: "
|
|
308
|
+
f"{missing_text}"
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
if extra:
|
|
312
|
+
extra_text = ", ".join(
|
|
313
|
+
sorted(extra)
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
raise ValueError(
|
|
317
|
+
f"Event observations defined for unknown "
|
|
318
|
+
f"agents: {extra_text}"
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
return result
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# ============================================================================
|
|
325
|
+
# Helpers
|
|
326
|
+
# ============================================================================
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _observe_configuration(
|
|
330
|
+
model: EpistemicModel,
|
|
331
|
+
agent_name: str,
|
|
332
|
+
configuration: Configuration,
|
|
333
|
+
) -> Hashable:
|
|
334
|
+
"""
|
|
335
|
+
Evaluate an ordinary world observation on an arbitrary
|
|
336
|
+
Configuration.
|
|
337
|
+
|
|
338
|
+
This is used for successor states that do not yet belong
|
|
339
|
+
to the old epistemic model.
|
|
340
|
+
"""
|
|
341
|
+
|
|
342
|
+
runtime_agent = configuration.agent(
|
|
343
|
+
agent_name
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
value = model._observations[
|
|
347
|
+
agent_name
|
|
348
|
+
](
|
|
349
|
+
runtime_agent,
|
|
350
|
+
configuration,
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
hash(value)
|
|
354
|
+
|
|
355
|
+
return value
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _action_kind(
|
|
359
|
+
action: Action,
|
|
360
|
+
) -> str:
|
|
361
|
+
if isinstance(
|
|
362
|
+
action,
|
|
363
|
+
Send,
|
|
364
|
+
):
|
|
365
|
+
return "send"
|
|
366
|
+
|
|
367
|
+
if isinstance(
|
|
368
|
+
action,
|
|
369
|
+
Receive,
|
|
370
|
+
):
|
|
371
|
+
return "receive"
|
|
372
|
+
|
|
373
|
+
if isinstance(
|
|
374
|
+
action,
|
|
375
|
+
Tau,
|
|
376
|
+
):
|
|
377
|
+
return "tau"
|
|
378
|
+
|
|
379
|
+
return type(action).__name__.lower()
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _event_name(
|
|
383
|
+
transition_index: int,
|
|
384
|
+
transition: SystemTransition,
|
|
385
|
+
) -> str:
|
|
386
|
+
"""
|
|
387
|
+
Produce a stable human-readable event identifier.
|
|
388
|
+
|
|
389
|
+
The transition index makes otherwise identical transitions
|
|
390
|
+
distinguishable.
|
|
391
|
+
"""
|
|
392
|
+
|
|
393
|
+
return (
|
|
394
|
+
f"transition:"
|
|
395
|
+
f"{transition_index}:"
|
|
396
|
+
f"{_action_kind(transition.action)}"
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
# ============================================================================
|
|
401
|
+
# Process execution event
|
|
402
|
+
# ============================================================================
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
@dataclass(frozen=True, slots=True)
|
|
406
|
+
class ProcessExecutionEvent:
|
|
407
|
+
"""
|
|
408
|
+
Dynamic epistemic event generated from an actual
|
|
409
|
+
process-calculus transition.
|
|
410
|
+
|
|
411
|
+
This is the bridge between:
|
|
412
|
+
|
|
413
|
+
Process calculus
|
|
414
|
+
↓
|
|
415
|
+
runtime transition
|
|
416
|
+
↓
|
|
417
|
+
epistemic update
|
|
418
|
+
|
|
419
|
+
The resulting epistemic worlds are ProductWorld objects:
|
|
420
|
+
|
|
421
|
+
ProductWorld(
|
|
422
|
+
world=target_configuration,
|
|
423
|
+
event=transition_id,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
The event relation combines:
|
|
427
|
+
|
|
428
|
+
1. source-world epistemic indistinguishability
|
|
429
|
+
2. event indistinguishability
|
|
430
|
+
3. target-world observations
|
|
431
|
+
"""
|
|
432
|
+
|
|
433
|
+
transition: SystemTransition
|
|
434
|
+
|
|
435
|
+
observations: (
|
|
436
|
+
EventObservationMap
|
|
437
|
+
| TransitionObservation
|
|
438
|
+
| None
|
|
439
|
+
) = None
|
|
440
|
+
|
|
441
|
+
def apply(
|
|
442
|
+
self,
|
|
443
|
+
model: EpistemicModel,
|
|
444
|
+
) -> EpistemicModel:
|
|
445
|
+
return _apply_process_execution(
|
|
446
|
+
model=model,
|
|
447
|
+
transition=self.transition,
|
|
448
|
+
observations=self.observations,
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
def __repr__(self) -> str:
|
|
452
|
+
return (
|
|
453
|
+
f"ProcessExecutionEvent("
|
|
454
|
+
f"action={self.transition.action!r}, "
|
|
455
|
+
f"source={self.transition.source!r}, "
|
|
456
|
+
f"target={self.transition.target!r}"
|
|
457
|
+
f")"
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
# ============================================================================
|
|
462
|
+
# Execution
|
|
463
|
+
# ============================================================================
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def execute_transition(
|
|
467
|
+
transition: SystemTransition,
|
|
468
|
+
observations: (
|
|
469
|
+
EventObservationMap
|
|
470
|
+
| TransitionObservation
|
|
471
|
+
| None
|
|
472
|
+
) = None,
|
|
473
|
+
) -> ProcessExecutionEvent:
|
|
474
|
+
"""
|
|
475
|
+
Convert a runtime SystemTransition into a dynamic event.
|
|
476
|
+
|
|
477
|
+
Example:
|
|
478
|
+
|
|
479
|
+
event = execute_transition(
|
|
480
|
+
communication,
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
updated = model.update(event)
|
|
484
|
+
"""
|
|
485
|
+
|
|
486
|
+
return ProcessExecutionEvent(
|
|
487
|
+
transition=transition,
|
|
488
|
+
observations=observations,
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def execute(
|
|
493
|
+
model: EpistemicModel,
|
|
494
|
+
transition: SystemTransition,
|
|
495
|
+
observations: (
|
|
496
|
+
EventObservationMap
|
|
497
|
+
| TransitionObservation
|
|
498
|
+
| None
|
|
499
|
+
) = None,
|
|
500
|
+
) -> EpistemicModel:
|
|
501
|
+
"""
|
|
502
|
+
Execute a process-calculus transition and update
|
|
503
|
+
the epistemic model.
|
|
504
|
+
|
|
505
|
+
Equivalent to:
|
|
506
|
+
|
|
507
|
+
model.update(
|
|
508
|
+
execute_transition(
|
|
509
|
+
transition,
|
|
510
|
+
observations,
|
|
511
|
+
)
|
|
512
|
+
)
|
|
513
|
+
"""
|
|
514
|
+
|
|
515
|
+
return model.update(
|
|
516
|
+
execute_transition(
|
|
517
|
+
transition,
|
|
518
|
+
observations,
|
|
519
|
+
)
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def step(
|
|
524
|
+
model: EpistemicModel,
|
|
525
|
+
transition: SystemTransition,
|
|
526
|
+
observations: (
|
|
527
|
+
EventObservationMap
|
|
528
|
+
| TransitionObservation
|
|
529
|
+
| None
|
|
530
|
+
) = None,
|
|
531
|
+
) -> EpistemicModel:
|
|
532
|
+
"""
|
|
533
|
+
Short alias for execute().
|
|
534
|
+
"""
|
|
535
|
+
|
|
536
|
+
return execute(
|
|
537
|
+
model,
|
|
538
|
+
transition,
|
|
539
|
+
observations,
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
# ============================================================================
|
|
544
|
+
# Internal implementation
|
|
545
|
+
# ============================================================================
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def _apply_process_execution(
|
|
549
|
+
model: EpistemicModel,
|
|
550
|
+
transition: SystemTransition,
|
|
551
|
+
observations: (
|
|
552
|
+
EventObservationMap
|
|
553
|
+
| TransitionObservation
|
|
554
|
+
| None
|
|
555
|
+
),
|
|
556
|
+
) -> EpistemicModel:
|
|
557
|
+
if model.state_space is None:
|
|
558
|
+
raise ValueError(
|
|
559
|
+
"Process execution requires an attached StateSpace"
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
if not isinstance(
|
|
563
|
+
model.actual,
|
|
564
|
+
Configuration,
|
|
565
|
+
):
|
|
566
|
+
raise ValueError(
|
|
567
|
+
"Process execution currently requires "
|
|
568
|
+
"a Configuration as the actual world"
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
transitions = tuple(
|
|
572
|
+
model.state_space.transitions
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
try:
|
|
576
|
+
actual_index = transitions.index(
|
|
577
|
+
transition
|
|
578
|
+
)
|
|
579
|
+
except ValueError as exc:
|
|
580
|
+
raise ValueError(
|
|
581
|
+
"The transition does not belong "
|
|
582
|
+
"to the model StateSpace"
|
|
583
|
+
) from exc
|
|
584
|
+
|
|
585
|
+
if transition.source != model.actual:
|
|
586
|
+
raise ValueError(
|
|
587
|
+
"The executed transition must start "
|
|
588
|
+
"at the actual world"
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
event_observations = (
|
|
592
|
+
_normalize_event_observations(
|
|
593
|
+
observations,
|
|
594
|
+
model._agent_names,
|
|
595
|
+
)
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
actual_source = transition.source
|
|
599
|
+
actual_target = transition.target
|
|
600
|
+
|
|
601
|
+
# ---------------------------------------------------------------------
|
|
602
|
+
# Generate candidate histories.
|
|
603
|
+
#
|
|
604
|
+
# A transition can remain possible for at least one agent when:
|
|
605
|
+
#
|
|
606
|
+
# source is epistemically possible
|
|
607
|
+
# AND
|
|
608
|
+
# event is observationally equivalent
|
|
609
|
+
# AND
|
|
610
|
+
# resulting observation is equivalent
|
|
611
|
+
#
|
|
612
|
+
# ---------------------------------------------------------------------
|
|
613
|
+
|
|
614
|
+
candidates: list[
|
|
615
|
+
tuple[
|
|
616
|
+
int,
|
|
617
|
+
SystemTransition,
|
|
618
|
+
]
|
|
619
|
+
] = []
|
|
620
|
+
|
|
621
|
+
for index, candidate in enumerate(
|
|
622
|
+
transitions
|
|
623
|
+
):
|
|
624
|
+
# Only transitions whose source is itself
|
|
625
|
+
# an epistemically possible current world matter.
|
|
626
|
+
if candidate.source not in model.worlds:
|
|
627
|
+
continue
|
|
628
|
+
|
|
629
|
+
possible_for_some_agent = False
|
|
630
|
+
|
|
631
|
+
for agent_name in model._agent_names:
|
|
632
|
+
source_accessible = model.accessible(
|
|
633
|
+
agent_name,
|
|
634
|
+
actual_source,
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
if candidate.source not in source_accessible:
|
|
638
|
+
continue
|
|
639
|
+
|
|
640
|
+
actual_event_observation = (
|
|
641
|
+
event_observations[
|
|
642
|
+
agent_name
|
|
643
|
+
](
|
|
644
|
+
actual_source.agent(
|
|
645
|
+
agent_name
|
|
646
|
+
),
|
|
647
|
+
transition,
|
|
648
|
+
)
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
candidate_event_observation = (
|
|
652
|
+
event_observations[
|
|
653
|
+
agent_name
|
|
654
|
+
](
|
|
655
|
+
candidate.source.agent(
|
|
656
|
+
agent_name
|
|
657
|
+
),
|
|
658
|
+
candidate,
|
|
659
|
+
)
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
hash(actual_event_observation)
|
|
663
|
+
hash(candidate_event_observation)
|
|
664
|
+
|
|
665
|
+
if (
|
|
666
|
+
actual_event_observation
|
|
667
|
+
!= candidate_event_observation
|
|
668
|
+
):
|
|
669
|
+
continue
|
|
670
|
+
|
|
671
|
+
actual_target_observation = (
|
|
672
|
+
_observe_configuration(
|
|
673
|
+
model,
|
|
674
|
+
agent_name,
|
|
675
|
+
actual_target,
|
|
676
|
+
)
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
candidate_target_observation = (
|
|
680
|
+
_observe_configuration(
|
|
681
|
+
model,
|
|
682
|
+
agent_name,
|
|
683
|
+
candidate.target,
|
|
684
|
+
)
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
if (
|
|
688
|
+
actual_target_observation
|
|
689
|
+
!= candidate_target_observation
|
|
690
|
+
):
|
|
691
|
+
continue
|
|
692
|
+
|
|
693
|
+
possible_for_some_agent = True
|
|
694
|
+
break
|
|
695
|
+
|
|
696
|
+
if possible_for_some_agent:
|
|
697
|
+
candidates.append(
|
|
698
|
+
(
|
|
699
|
+
index,
|
|
700
|
+
candidate,
|
|
701
|
+
)
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
actual_candidate = next(
|
|
705
|
+
(
|
|
706
|
+
item
|
|
707
|
+
for item in candidates
|
|
708
|
+
if item[0] == actual_index
|
|
709
|
+
),
|
|
710
|
+
None,
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
if actual_candidate is None:
|
|
714
|
+
raise ValueError(
|
|
715
|
+
"The actual transition is not "
|
|
716
|
+
"epistemically executable"
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
# ---------------------------------------------------------------------
|
|
720
|
+
# Materialize ProductWorld histories.
|
|
721
|
+
# ---------------------------------------------------------------------
|
|
722
|
+
|
|
723
|
+
product_worlds: list[
|
|
724
|
+
ProductWorld
|
|
725
|
+
] = []
|
|
726
|
+
|
|
727
|
+
by_index: dict[
|
|
728
|
+
int,
|
|
729
|
+
ProductWorld,
|
|
730
|
+
] = {}
|
|
731
|
+
|
|
732
|
+
transition_by_index: dict[
|
|
733
|
+
int,
|
|
734
|
+
SystemTransition,
|
|
735
|
+
] = {}
|
|
736
|
+
|
|
737
|
+
for index, candidate in candidates:
|
|
738
|
+
product = ProductWorld(
|
|
739
|
+
world=candidate.target,
|
|
740
|
+
event=_event_name(
|
|
741
|
+
index,
|
|
742
|
+
candidate,
|
|
743
|
+
),
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
product_worlds.append(
|
|
747
|
+
product
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
by_index[index] = product
|
|
751
|
+
transition_by_index[index] = candidate
|
|
752
|
+
|
|
753
|
+
product_worlds_tuple = tuple(
|
|
754
|
+
product_worlds
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
actual_product = by_index[
|
|
758
|
+
actual_index
|
|
759
|
+
]
|
|
760
|
+
|
|
761
|
+
# ---------------------------------------------------------------------
|
|
762
|
+
# Build post-event epistemic relations.
|
|
763
|
+
# ---------------------------------------------------------------------
|
|
764
|
+
|
|
765
|
+
relation_overrides: dict[
|
|
766
|
+
str,
|
|
767
|
+
dict[
|
|
768
|
+
ProductWorld,
|
|
769
|
+
tuple[
|
|
770
|
+
ProductWorld,
|
|
771
|
+
...,
|
|
772
|
+
],
|
|
773
|
+
],
|
|
774
|
+
] = {}
|
|
775
|
+
|
|
776
|
+
for agent_name in model._agent_names:
|
|
777
|
+
source_map: dict[
|
|
778
|
+
ProductWorld,
|
|
779
|
+
tuple[
|
|
780
|
+
ProductWorld,
|
|
781
|
+
...,
|
|
782
|
+
],
|
|
783
|
+
] = {}
|
|
784
|
+
|
|
785
|
+
for source_index, source_candidate in (
|
|
786
|
+
transition_by_index.items()
|
|
787
|
+
):
|
|
788
|
+
source_product = by_index[
|
|
789
|
+
source_index
|
|
790
|
+
]
|
|
791
|
+
|
|
792
|
+
source_accessible = set(
|
|
793
|
+
model.accessible(
|
|
794
|
+
agent_name,
|
|
795
|
+
source_candidate.source,
|
|
796
|
+
)
|
|
797
|
+
)
|
|
798
|
+
|
|
799
|
+
source_event_observation = (
|
|
800
|
+
event_observations[
|
|
801
|
+
agent_name
|
|
802
|
+
](
|
|
803
|
+
source_candidate.source.agent(
|
|
804
|
+
agent_name
|
|
805
|
+
),
|
|
806
|
+
source_candidate,
|
|
807
|
+
)
|
|
808
|
+
)
|
|
809
|
+
|
|
810
|
+
hash(
|
|
811
|
+
source_event_observation
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
source_target_observation = (
|
|
815
|
+
_observe_configuration(
|
|
816
|
+
model,
|
|
817
|
+
agent_name,
|
|
818
|
+
source_candidate.target,
|
|
819
|
+
)
|
|
820
|
+
)
|
|
821
|
+
|
|
822
|
+
targets: list[
|
|
823
|
+
ProductWorld
|
|
824
|
+
] = []
|
|
825
|
+
|
|
826
|
+
for candidate_index, candidate in (
|
|
827
|
+
transition_by_index.items()
|
|
828
|
+
):
|
|
829
|
+
if (
|
|
830
|
+
candidate.source
|
|
831
|
+
not in source_accessible
|
|
832
|
+
):
|
|
833
|
+
continue
|
|
834
|
+
|
|
835
|
+
candidate_event_observation = (
|
|
836
|
+
event_observations[
|
|
837
|
+
agent_name
|
|
838
|
+
](
|
|
839
|
+
candidate.source.agent(
|
|
840
|
+
agent_name
|
|
841
|
+
),
|
|
842
|
+
candidate,
|
|
843
|
+
)
|
|
844
|
+
)
|
|
845
|
+
|
|
846
|
+
if (
|
|
847
|
+
candidate_event_observation
|
|
848
|
+
!= source_event_observation
|
|
849
|
+
):
|
|
850
|
+
continue
|
|
851
|
+
|
|
852
|
+
candidate_target_observation = (
|
|
853
|
+
_observe_configuration(
|
|
854
|
+
model,
|
|
855
|
+
agent_name,
|
|
856
|
+
candidate.target,
|
|
857
|
+
)
|
|
858
|
+
)
|
|
859
|
+
|
|
860
|
+
if (
|
|
861
|
+
candidate_target_observation
|
|
862
|
+
!= source_target_observation
|
|
863
|
+
):
|
|
864
|
+
continue
|
|
865
|
+
|
|
866
|
+
target_product = by_index[
|
|
867
|
+
candidate_index
|
|
868
|
+
]
|
|
869
|
+
|
|
870
|
+
if target_product not in targets:
|
|
871
|
+
targets.append(
|
|
872
|
+
target_product
|
|
873
|
+
)
|
|
874
|
+
|
|
875
|
+
source_map[
|
|
876
|
+
source_product
|
|
877
|
+
] = tuple(
|
|
878
|
+
targets
|
|
879
|
+
)
|
|
880
|
+
|
|
881
|
+
relation_overrides[
|
|
882
|
+
agent_name
|
|
883
|
+
] = source_map
|
|
884
|
+
|
|
885
|
+
return EpistemicModel(
|
|
886
|
+
worlds=product_worlds_tuple,
|
|
887
|
+
observations=model._observations,
|
|
888
|
+
actual=actual_product,
|
|
889
|
+
propositions=model._propositions,
|
|
890
|
+
state_space=None,
|
|
891
|
+
relations=relation_overrides,
|
|
892
|
+
)
|