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/epistemic.py
ADDED
|
@@ -0,0 +1,2068 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Callable, Iterable, Mapping
|
|
5
|
+
|
|
6
|
+
from .agent import Agent
|
|
7
|
+
from .dynamic import (
|
|
8
|
+
ActionModel,
|
|
9
|
+
DynamicEvent,
|
|
10
|
+
PublicAnnouncement,
|
|
11
|
+
)
|
|
12
|
+
from .explorer import StateSpace
|
|
13
|
+
from .formula import (
|
|
14
|
+
And,
|
|
15
|
+
Atom,
|
|
16
|
+
Formula,
|
|
17
|
+
Not,
|
|
18
|
+
Or,
|
|
19
|
+
)
|
|
20
|
+
from .observation import (
|
|
21
|
+
Observation,
|
|
22
|
+
ObservationBuilder,
|
|
23
|
+
)
|
|
24
|
+
from .propositions import Proposition
|
|
25
|
+
from .runtime import Configuration
|
|
26
|
+
from .temporal import (
|
|
27
|
+
Always,
|
|
28
|
+
Eventually,
|
|
29
|
+
Next,
|
|
30
|
+
Until,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ============================================================================
|
|
35
|
+
# World types
|
|
36
|
+
# ============================================================================
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True, slots=True)
|
|
40
|
+
class ProductWorld:
|
|
41
|
+
"""
|
|
42
|
+
World generated by a dynamic epistemic product update.
|
|
43
|
+
|
|
44
|
+
A ProductWorld is a pair:
|
|
45
|
+
|
|
46
|
+
(base world, executed event)
|
|
47
|
+
|
|
48
|
+
The base Configuration represents the physical world.
|
|
49
|
+
The event represents the epistemic action that occurred.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
world: Configuration
|
|
53
|
+
event: str
|
|
54
|
+
|
|
55
|
+
def __repr__(self) -> str:
|
|
56
|
+
return (
|
|
57
|
+
f"ProductWorld("
|
|
58
|
+
f"world={self.world!r}, "
|
|
59
|
+
f"event={self.event!r}"
|
|
60
|
+
f")"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
World = Configuration | ProductWorld
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _configuration(
|
|
68
|
+
world: World,
|
|
69
|
+
) -> Configuration:
|
|
70
|
+
"""
|
|
71
|
+
Extract the underlying physical configuration.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
if isinstance(
|
|
75
|
+
world,
|
|
76
|
+
ProductWorld,
|
|
77
|
+
):
|
|
78
|
+
return world.world
|
|
79
|
+
|
|
80
|
+
return world
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ============================================================================
|
|
84
|
+
# Epistemic operators
|
|
85
|
+
# ============================================================================
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(frozen=True, slots=True)
|
|
89
|
+
class Knowledge(Formula):
|
|
90
|
+
"""
|
|
91
|
+
K_i φ
|
|
92
|
+
|
|
93
|
+
Agent i knows φ.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
agent: str
|
|
97
|
+
operand: Formula
|
|
98
|
+
|
|
99
|
+
def __post_init__(self) -> None:
|
|
100
|
+
if not self.agent.strip():
|
|
101
|
+
raise ValueError(
|
|
102
|
+
"Knowledge agent must not be empty"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def __repr__(self) -> str:
|
|
106
|
+
return (
|
|
107
|
+
f"K_{self.agent}({self.operand!r})"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def K(
|
|
112
|
+
agent: str | Agent,
|
|
113
|
+
operand: Formula,
|
|
114
|
+
) -> Knowledge:
|
|
115
|
+
"""
|
|
116
|
+
Factory for K_i φ.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
name = (
|
|
120
|
+
agent.name
|
|
121
|
+
if isinstance(
|
|
122
|
+
agent,
|
|
123
|
+
Agent,
|
|
124
|
+
)
|
|
125
|
+
else agent
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
return Knowledge(
|
|
129
|
+
agent=name,
|
|
130
|
+
operand=operand,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ============================================================================
|
|
135
|
+
# Group operators
|
|
136
|
+
# ============================================================================
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _normalize_group(
|
|
140
|
+
agents: (
|
|
141
|
+
str
|
|
142
|
+
| Agent
|
|
143
|
+
| Iterable[str | Agent]
|
|
144
|
+
),
|
|
145
|
+
) -> tuple[str, ...]:
|
|
146
|
+
if isinstance(
|
|
147
|
+
agents,
|
|
148
|
+
Agent,
|
|
149
|
+
):
|
|
150
|
+
values = (
|
|
151
|
+
agents.name,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
elif isinstance(
|
|
155
|
+
agents,
|
|
156
|
+
str,
|
|
157
|
+
):
|
|
158
|
+
values = (
|
|
159
|
+
agents,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
else:
|
|
163
|
+
values = tuple(
|
|
164
|
+
agent.name
|
|
165
|
+
if isinstance(
|
|
166
|
+
agent,
|
|
167
|
+
Agent,
|
|
168
|
+
)
|
|
169
|
+
else agent
|
|
170
|
+
for agent in agents
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
if not values:
|
|
174
|
+
raise ValueError(
|
|
175
|
+
"An epistemic group must not be empty"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
result: list[str] = []
|
|
179
|
+
|
|
180
|
+
for name in values:
|
|
181
|
+
if not isinstance(
|
|
182
|
+
name,
|
|
183
|
+
str,
|
|
184
|
+
):
|
|
185
|
+
raise TypeError(
|
|
186
|
+
"Agent names must be strings or Agent objects"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
name = name.strip()
|
|
190
|
+
|
|
191
|
+
if not name:
|
|
192
|
+
raise ValueError(
|
|
193
|
+
"Agent name must not be empty"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if name not in result:
|
|
197
|
+
result.append(
|
|
198
|
+
name
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
return tuple(result)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@dataclass(frozen=True, slots=True)
|
|
205
|
+
class EveryoneKnows(Formula):
|
|
206
|
+
"""
|
|
207
|
+
E_A φ
|
|
208
|
+
|
|
209
|
+
Every agent in A knows φ.
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
agents: tuple[str, ...]
|
|
213
|
+
operand: Formula
|
|
214
|
+
|
|
215
|
+
def __repr__(self) -> str:
|
|
216
|
+
group = ",".join(
|
|
217
|
+
self.agents
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
return (
|
|
221
|
+
f"E_{{{group}}}({self.operand!r})"
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def E(
|
|
226
|
+
agents: (
|
|
227
|
+
str
|
|
228
|
+
| Agent
|
|
229
|
+
| Iterable[str | Agent]
|
|
230
|
+
),
|
|
231
|
+
operand: Formula,
|
|
232
|
+
) -> EveryoneKnows:
|
|
233
|
+
return EveryoneKnows(
|
|
234
|
+
agents=_normalize_group(
|
|
235
|
+
agents
|
|
236
|
+
),
|
|
237
|
+
operand=operand,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@dataclass(frozen=True, slots=True)
|
|
242
|
+
class DistributedKnowledge(Formula):
|
|
243
|
+
"""
|
|
244
|
+
D_A φ
|
|
245
|
+
|
|
246
|
+
Distributed knowledge of a group.
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
agents: tuple[str, ...]
|
|
250
|
+
operand: Formula
|
|
251
|
+
|
|
252
|
+
def __repr__(self) -> str:
|
|
253
|
+
group = ",".join(
|
|
254
|
+
self.agents
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
f"D_{{{group}}}({self.operand!r})"
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def D(
|
|
263
|
+
agents: (
|
|
264
|
+
str
|
|
265
|
+
| Agent
|
|
266
|
+
| Iterable[str | Agent]
|
|
267
|
+
),
|
|
268
|
+
operand: Formula,
|
|
269
|
+
) -> DistributedKnowledge:
|
|
270
|
+
return DistributedKnowledge(
|
|
271
|
+
agents=_normalize_group(
|
|
272
|
+
agents
|
|
273
|
+
),
|
|
274
|
+
operand=operand,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
@dataclass(frozen=True, slots=True)
|
|
279
|
+
class CommonKnowledge(Formula):
|
|
280
|
+
"""
|
|
281
|
+
C_A φ
|
|
282
|
+
|
|
283
|
+
Common knowledge in a group.
|
|
284
|
+
"""
|
|
285
|
+
|
|
286
|
+
agents: tuple[str, ...]
|
|
287
|
+
operand: Formula
|
|
288
|
+
|
|
289
|
+
def __repr__(self) -> str:
|
|
290
|
+
group = ",".join(
|
|
291
|
+
self.agents
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
return (
|
|
295
|
+
f"C_{{{group}}}({self.operand!r})"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def C(
|
|
300
|
+
agents: (
|
|
301
|
+
str
|
|
302
|
+
| Agent
|
|
303
|
+
| Iterable[str | Agent]
|
|
304
|
+
),
|
|
305
|
+
operand: Formula,
|
|
306
|
+
) -> CommonKnowledge:
|
|
307
|
+
return CommonKnowledge(
|
|
308
|
+
agents=_normalize_group(
|
|
309
|
+
agents
|
|
310
|
+
),
|
|
311
|
+
operand=operand,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# ============================================================================
|
|
316
|
+
# Observation mapping
|
|
317
|
+
# ============================================================================
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
ObservationMap = Mapping[
|
|
321
|
+
str | Agent,
|
|
322
|
+
Observation,
|
|
323
|
+
]
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _normalize_observations(
|
|
327
|
+
observations: (
|
|
328
|
+
ObservationMap
|
|
329
|
+
| ObservationBuilder
|
|
330
|
+
),
|
|
331
|
+
agent_names: Iterable[str],
|
|
332
|
+
) -> dict[
|
|
333
|
+
str,
|
|
334
|
+
Observation,
|
|
335
|
+
]:
|
|
336
|
+
names = tuple(
|
|
337
|
+
agent_names
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
if isinstance(
|
|
341
|
+
observations,
|
|
342
|
+
ObservationBuilder,
|
|
343
|
+
):
|
|
344
|
+
return observations.build(
|
|
345
|
+
names
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
result: dict[
|
|
349
|
+
str,
|
|
350
|
+
Observation,
|
|
351
|
+
] = {}
|
|
352
|
+
|
|
353
|
+
for key, observation in observations.items():
|
|
354
|
+
name = (
|
|
355
|
+
key.name
|
|
356
|
+
if isinstance(
|
|
357
|
+
key,
|
|
358
|
+
Agent,
|
|
359
|
+
)
|
|
360
|
+
else key
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
if not isinstance(
|
|
364
|
+
name,
|
|
365
|
+
str,
|
|
366
|
+
):
|
|
367
|
+
raise TypeError(
|
|
368
|
+
"Observation keys must be strings or Agent objects"
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
name = name.strip()
|
|
372
|
+
|
|
373
|
+
if not name:
|
|
374
|
+
raise ValueError(
|
|
375
|
+
"Observation agent name must not be empty"
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
if name in result:
|
|
379
|
+
raise ValueError(
|
|
380
|
+
f"Duplicate observation "
|
|
381
|
+
f"for agent {name!r}"
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
result[name] = observation
|
|
385
|
+
|
|
386
|
+
expected = set(
|
|
387
|
+
names
|
|
388
|
+
)
|
|
389
|
+
actual = set(
|
|
390
|
+
result
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
missing = expected - actual
|
|
394
|
+
extra = actual - expected
|
|
395
|
+
|
|
396
|
+
if missing:
|
|
397
|
+
missing_text = ", ".join(
|
|
398
|
+
sorted(missing)
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
raise ValueError(
|
|
402
|
+
f"Missing observations for agents: "
|
|
403
|
+
f"{missing_text}"
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
if extra:
|
|
407
|
+
extra_text = ", ".join(
|
|
408
|
+
sorted(extra)
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
raise ValueError(
|
|
412
|
+
f"Observations defined for unknown agents: "
|
|
413
|
+
f"{extra_text}"
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
return result
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
# ============================================================================
|
|
420
|
+
# Epistemic relation overrides
|
|
421
|
+
# ============================================================================
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
RelationMap = Mapping[
|
|
425
|
+
str,
|
|
426
|
+
Mapping[
|
|
427
|
+
World,
|
|
428
|
+
Iterable[World],
|
|
429
|
+
],
|
|
430
|
+
]
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _normalize_relation_overrides(
|
|
434
|
+
relations: RelationMap | None,
|
|
435
|
+
worlds: tuple[World, ...],
|
|
436
|
+
agent_names: tuple[str, ...],
|
|
437
|
+
) -> dict[
|
|
438
|
+
str,
|
|
439
|
+
dict[
|
|
440
|
+
World,
|
|
441
|
+
tuple[World, ...],
|
|
442
|
+
],
|
|
443
|
+
]:
|
|
444
|
+
if relations is None:
|
|
445
|
+
return {}
|
|
446
|
+
|
|
447
|
+
world_set = set(
|
|
448
|
+
worlds
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
agent_set = set(
|
|
452
|
+
agent_names
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
result: dict[
|
|
456
|
+
str,
|
|
457
|
+
dict[
|
|
458
|
+
World,
|
|
459
|
+
tuple[World, ...],
|
|
460
|
+
],
|
|
461
|
+
] = {}
|
|
462
|
+
|
|
463
|
+
for agent, relation in relations.items():
|
|
464
|
+
if agent not in agent_set:
|
|
465
|
+
raise ValueError(
|
|
466
|
+
f"Unknown agent {agent!r} "
|
|
467
|
+
f"in relation override"
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
source_map: dict[
|
|
471
|
+
World,
|
|
472
|
+
tuple[World, ...],
|
|
473
|
+
] = {}
|
|
474
|
+
|
|
475
|
+
for source, targets in relation.items():
|
|
476
|
+
if source not in world_set:
|
|
477
|
+
raise ValueError(
|
|
478
|
+
"Relation override contains "
|
|
479
|
+
"an unknown source world"
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
target_tuple = tuple(
|
|
483
|
+
targets
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
for target in target_tuple:
|
|
487
|
+
if target not in world_set:
|
|
488
|
+
raise ValueError(
|
|
489
|
+
"Relation override contains "
|
|
490
|
+
"an unknown target world"
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
source_map[source] = target_tuple
|
|
494
|
+
|
|
495
|
+
result[agent] = source_map
|
|
496
|
+
|
|
497
|
+
return result
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
# ============================================================================
|
|
501
|
+
# Epistemic model
|
|
502
|
+
# ============================================================================
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
class EpistemicModel:
|
|
506
|
+
"""
|
|
507
|
+
Finite epistemic model.
|
|
508
|
+
|
|
509
|
+
Standard models derive R_i from observations:
|
|
510
|
+
|
|
511
|
+
w R_i v iff h_i(w) = h_i(v)
|
|
512
|
+
|
|
513
|
+
Dynamic epistemic product updates can install explicit
|
|
514
|
+
relation overrides:
|
|
515
|
+
|
|
516
|
+
(w,e) R_i' (v,f)
|
|
517
|
+
|
|
518
|
+
iff
|
|
519
|
+
|
|
520
|
+
w R_i v
|
|
521
|
+
|
|
522
|
+
and
|
|
523
|
+
|
|
524
|
+
e R_i^A f
|
|
525
|
+
|
|
526
|
+
Temporal operators require a transition structure when
|
|
527
|
+
one is available.
|
|
528
|
+
|
|
529
|
+
If no StateSpace is attached, the model is treated as a
|
|
530
|
+
static model with implicit stuttering:
|
|
531
|
+
|
|
532
|
+
w -> w
|
|
533
|
+
|
|
534
|
+
This makes temporal queries such as G(phi) meaningful for
|
|
535
|
+
a purely epistemic snapshot.
|
|
536
|
+
"""
|
|
537
|
+
|
|
538
|
+
def __init__(
|
|
539
|
+
self,
|
|
540
|
+
worlds: Iterable[World],
|
|
541
|
+
observations: (
|
|
542
|
+
ObservationMap
|
|
543
|
+
| ObservationBuilder
|
|
544
|
+
),
|
|
545
|
+
actual: World,
|
|
546
|
+
propositions: (
|
|
547
|
+
Iterable[Proposition]
|
|
548
|
+
| Mapping[str, Proposition]
|
|
549
|
+
| None
|
|
550
|
+
) = None,
|
|
551
|
+
state_space: StateSpace | None = None,
|
|
552
|
+
relations: RelationMap | None = None,
|
|
553
|
+
) -> None:
|
|
554
|
+
world_tuple = tuple(
|
|
555
|
+
worlds
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
if not world_tuple:
|
|
559
|
+
raise ValueError(
|
|
560
|
+
"EpistemicModel requires at least one world"
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
if actual not in world_tuple:
|
|
564
|
+
raise ValueError(
|
|
565
|
+
"Actual world must belong to worlds"
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
if (
|
|
569
|
+
state_space is not None
|
|
570
|
+
and any(
|
|
571
|
+
not isinstance(
|
|
572
|
+
world,
|
|
573
|
+
Configuration,
|
|
574
|
+
)
|
|
575
|
+
for world in world_tuple
|
|
576
|
+
)
|
|
577
|
+
):
|
|
578
|
+
raise ValueError(
|
|
579
|
+
"StateSpace can only be attached "
|
|
580
|
+
"to Configuration worlds"
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
reference_configuration = (
|
|
584
|
+
_configuration(
|
|
585
|
+
world_tuple[0]
|
|
586
|
+
)
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
reference_agents = tuple(
|
|
590
|
+
reference_configuration.agents
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
reference_names = tuple(
|
|
594
|
+
agent.name
|
|
595
|
+
for agent in reference_agents
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
reference_name_set = set(
|
|
599
|
+
reference_names
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
for world in world_tuple:
|
|
603
|
+
current_names = {
|
|
604
|
+
agent.name
|
|
605
|
+
for agent in _configuration(
|
|
606
|
+
world
|
|
607
|
+
).agents
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if current_names != reference_name_set:
|
|
611
|
+
raise ValueError(
|
|
612
|
+
"All epistemic worlds must contain "
|
|
613
|
+
"the same agents"
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
if state_space is not None:
|
|
617
|
+
state_space_states = set(
|
|
618
|
+
state_space.states
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
if not set(
|
|
622
|
+
world_tuple
|
|
623
|
+
).issubset(
|
|
624
|
+
state_space_states
|
|
625
|
+
):
|
|
626
|
+
raise ValueError(
|
|
627
|
+
"All epistemic worlds must belong "
|
|
628
|
+
"to the attached StateSpace"
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
self._worlds = world_tuple
|
|
632
|
+
self._actual = actual
|
|
633
|
+
|
|
634
|
+
self._agents = reference_agents
|
|
635
|
+
self._agent_names = reference_names
|
|
636
|
+
|
|
637
|
+
self._observations = (
|
|
638
|
+
_normalize_observations(
|
|
639
|
+
observations,
|
|
640
|
+
self._agent_names,
|
|
641
|
+
)
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
self._propositions = (
|
|
645
|
+
self._normalize_propositions(
|
|
646
|
+
propositions
|
|
647
|
+
)
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
self._state_space = state_space
|
|
651
|
+
|
|
652
|
+
self._relation_overrides = (
|
|
653
|
+
_normalize_relation_overrides(
|
|
654
|
+
relations,
|
|
655
|
+
world_tuple,
|
|
656
|
+
self._agent_names,
|
|
657
|
+
)
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
self._successors_map = (
|
|
661
|
+
self._build_successors_map(
|
|
662
|
+
state_space
|
|
663
|
+
)
|
|
664
|
+
if state_space is not None
|
|
665
|
+
else {}
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
# ---------------------------------------------------------------------
|
|
669
|
+
# Construction
|
|
670
|
+
# ---------------------------------------------------------------------
|
|
671
|
+
|
|
672
|
+
@classmethod
|
|
673
|
+
def from_state_space(
|
|
674
|
+
cls,
|
|
675
|
+
state_space: StateSpace,
|
|
676
|
+
observations: (
|
|
677
|
+
ObservationMap
|
|
678
|
+
| ObservationBuilder
|
|
679
|
+
),
|
|
680
|
+
actual: Configuration | None = None,
|
|
681
|
+
propositions: (
|
|
682
|
+
Iterable[Proposition]
|
|
683
|
+
| Mapping[str, Proposition]
|
|
684
|
+
| None
|
|
685
|
+
) = None,
|
|
686
|
+
) -> "EpistemicModel":
|
|
687
|
+
if not state_space.initials:
|
|
688
|
+
raise ValueError(
|
|
689
|
+
"StateSpace must contain at least one initial"
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
if actual is None:
|
|
693
|
+
if len(
|
|
694
|
+
state_space.initials
|
|
695
|
+
) != 1:
|
|
696
|
+
raise ValueError(
|
|
697
|
+
"actual must be specified when the state "
|
|
698
|
+
"space has multiple initial worlds"
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
actual = state_space.initials[0]
|
|
702
|
+
|
|
703
|
+
return cls(
|
|
704
|
+
worlds=state_space.states,
|
|
705
|
+
observations=observations,
|
|
706
|
+
actual=actual,
|
|
707
|
+
propositions=propositions,
|
|
708
|
+
state_space=state_space,
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
# ---------------------------------------------------------------------
|
|
712
|
+
# Public properties
|
|
713
|
+
# ---------------------------------------------------------------------
|
|
714
|
+
|
|
715
|
+
@property
|
|
716
|
+
def worlds(
|
|
717
|
+
self,
|
|
718
|
+
) -> tuple[World, ...]:
|
|
719
|
+
return self._worlds
|
|
720
|
+
|
|
721
|
+
@property
|
|
722
|
+
def actual(
|
|
723
|
+
self,
|
|
724
|
+
) -> World:
|
|
725
|
+
return self._actual
|
|
726
|
+
|
|
727
|
+
@property
|
|
728
|
+
def agents(
|
|
729
|
+
self,
|
|
730
|
+
) -> tuple[Agent, ...]:
|
|
731
|
+
return self._agents
|
|
732
|
+
|
|
733
|
+
@property
|
|
734
|
+
def propositions(
|
|
735
|
+
self,
|
|
736
|
+
) -> Mapping[str, Proposition]:
|
|
737
|
+
return self._propositions
|
|
738
|
+
|
|
739
|
+
@property
|
|
740
|
+
def state_space(
|
|
741
|
+
self,
|
|
742
|
+
) -> StateSpace | None:
|
|
743
|
+
return self._state_space
|
|
744
|
+
|
|
745
|
+
# ---------------------------------------------------------------------
|
|
746
|
+
# Agent helpers
|
|
747
|
+
# ---------------------------------------------------------------------
|
|
748
|
+
|
|
749
|
+
def _require_agent(
|
|
750
|
+
self,
|
|
751
|
+
agent: str | Agent,
|
|
752
|
+
) -> str:
|
|
753
|
+
name = (
|
|
754
|
+
agent.name
|
|
755
|
+
if isinstance(
|
|
756
|
+
agent,
|
|
757
|
+
Agent,
|
|
758
|
+
)
|
|
759
|
+
else agent
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
if name not in self._agent_names:
|
|
763
|
+
raise ValueError(
|
|
764
|
+
f"Unknown agent {name!r}"
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
return name
|
|
768
|
+
|
|
769
|
+
# ---------------------------------------------------------------------
|
|
770
|
+
# Observation
|
|
771
|
+
# ---------------------------------------------------------------------
|
|
772
|
+
|
|
773
|
+
def observe(
|
|
774
|
+
self,
|
|
775
|
+
agent: str | Agent,
|
|
776
|
+
world: World,
|
|
777
|
+
):
|
|
778
|
+
name = self._require_agent(
|
|
779
|
+
agent
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
if world not in self._worlds:
|
|
783
|
+
raise ValueError(
|
|
784
|
+
"World does not belong "
|
|
785
|
+
"to the epistemic model"
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
runtime_agent = (
|
|
789
|
+
_configuration(
|
|
790
|
+
world
|
|
791
|
+
).agent(
|
|
792
|
+
name
|
|
793
|
+
)
|
|
794
|
+
)
|
|
795
|
+
|
|
796
|
+
value = self._observations[name](
|
|
797
|
+
runtime_agent,
|
|
798
|
+
_configuration(
|
|
799
|
+
world
|
|
800
|
+
),
|
|
801
|
+
)
|
|
802
|
+
|
|
803
|
+
hash(value)
|
|
804
|
+
|
|
805
|
+
return value
|
|
806
|
+
|
|
807
|
+
# ---------------------------------------------------------------------
|
|
808
|
+
# Accessibility
|
|
809
|
+
# ---------------------------------------------------------------------
|
|
810
|
+
|
|
811
|
+
def accessible(
|
|
812
|
+
self,
|
|
813
|
+
agent: str | Agent,
|
|
814
|
+
world: World,
|
|
815
|
+
) -> tuple[World, ...]:
|
|
816
|
+
"""
|
|
817
|
+
Return worlds accessible for an agent.
|
|
818
|
+
|
|
819
|
+
Ordinary models use observation equality.
|
|
820
|
+
|
|
821
|
+
Product-update models use explicit product relations.
|
|
822
|
+
"""
|
|
823
|
+
|
|
824
|
+
name = self._require_agent(
|
|
825
|
+
agent
|
|
826
|
+
)
|
|
827
|
+
|
|
828
|
+
if world not in self._worlds:
|
|
829
|
+
raise ValueError(
|
|
830
|
+
"World does not belong "
|
|
831
|
+
"to the epistemic model"
|
|
832
|
+
)
|
|
833
|
+
|
|
834
|
+
override = (
|
|
835
|
+
self._relation_overrides.get(
|
|
836
|
+
name
|
|
837
|
+
)
|
|
838
|
+
)
|
|
839
|
+
|
|
840
|
+
if override is not None:
|
|
841
|
+
return override.get(
|
|
842
|
+
world,
|
|
843
|
+
(),
|
|
844
|
+
)
|
|
845
|
+
|
|
846
|
+
target_observation = (
|
|
847
|
+
self.observe(
|
|
848
|
+
name,
|
|
849
|
+
world,
|
|
850
|
+
)
|
|
851
|
+
)
|
|
852
|
+
|
|
853
|
+
return tuple(
|
|
854
|
+
candidate
|
|
855
|
+
for candidate in self._worlds
|
|
856
|
+
if self.observe(
|
|
857
|
+
name,
|
|
858
|
+
candidate,
|
|
859
|
+
)
|
|
860
|
+
== target_observation
|
|
861
|
+
)
|
|
862
|
+
|
|
863
|
+
# ---------------------------------------------------------------------
|
|
864
|
+
# Distributed knowledge
|
|
865
|
+
# ---------------------------------------------------------------------
|
|
866
|
+
|
|
867
|
+
def distributed_accessible(
|
|
868
|
+
self,
|
|
869
|
+
agents: (
|
|
870
|
+
str
|
|
871
|
+
| Agent
|
|
872
|
+
| Iterable[str | Agent]
|
|
873
|
+
),
|
|
874
|
+
world: World,
|
|
875
|
+
) -> tuple[World, ...]:
|
|
876
|
+
"""
|
|
877
|
+
Return the intersection of individual epistemic
|
|
878
|
+
accessibility relations:
|
|
879
|
+
|
|
880
|
+
R_DA = ⋂ R_i
|
|
881
|
+
|
|
882
|
+
This uses accessible() so product-update relations
|
|
883
|
+
remain fully respected.
|
|
884
|
+
"""
|
|
885
|
+
|
|
886
|
+
group = _normalize_group(
|
|
887
|
+
agents
|
|
888
|
+
)
|
|
889
|
+
|
|
890
|
+
for agent in group:
|
|
891
|
+
self._require_agent(
|
|
892
|
+
agent
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
if world not in self._worlds:
|
|
896
|
+
raise ValueError(
|
|
897
|
+
"World does not belong "
|
|
898
|
+
"to the epistemic model"
|
|
899
|
+
)
|
|
900
|
+
|
|
901
|
+
accessible_sets = [
|
|
902
|
+
set(
|
|
903
|
+
self.accessible(
|
|
904
|
+
agent,
|
|
905
|
+
world,
|
|
906
|
+
)
|
|
907
|
+
)
|
|
908
|
+
for agent in group
|
|
909
|
+
]
|
|
910
|
+
|
|
911
|
+
if not accessible_sets:
|
|
912
|
+
return ()
|
|
913
|
+
|
|
914
|
+
intersection = accessible_sets[0]
|
|
915
|
+
|
|
916
|
+
for accessible_set in accessible_sets[1:]:
|
|
917
|
+
intersection &= accessible_set
|
|
918
|
+
|
|
919
|
+
return tuple(
|
|
920
|
+
candidate
|
|
921
|
+
for candidate in self._worlds
|
|
922
|
+
if candidate in intersection
|
|
923
|
+
)
|
|
924
|
+
|
|
925
|
+
# ---------------------------------------------------------------------
|
|
926
|
+
# Common knowledge
|
|
927
|
+
# ---------------------------------------------------------------------
|
|
928
|
+
|
|
929
|
+
def common_knowledge_closure(
|
|
930
|
+
self,
|
|
931
|
+
agents: (
|
|
932
|
+
str
|
|
933
|
+
| Agent
|
|
934
|
+
| Iterable[str | Agent]
|
|
935
|
+
),
|
|
936
|
+
world: World,
|
|
937
|
+
) -> tuple[World, ...]:
|
|
938
|
+
"""
|
|
939
|
+
Reflexive-transitive closure:
|
|
940
|
+
|
|
941
|
+
(⋃ R_i)*
|
|
942
|
+
"""
|
|
943
|
+
|
|
944
|
+
group = _normalize_group(
|
|
945
|
+
agents
|
|
946
|
+
)
|
|
947
|
+
|
|
948
|
+
for agent in group:
|
|
949
|
+
self._require_agent(
|
|
950
|
+
agent
|
|
951
|
+
)
|
|
952
|
+
|
|
953
|
+
if world not in self._worlds:
|
|
954
|
+
raise ValueError(
|
|
955
|
+
"World does not belong "
|
|
956
|
+
"to the epistemic model"
|
|
957
|
+
)
|
|
958
|
+
|
|
959
|
+
visited: set[World] = {
|
|
960
|
+
world
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
queue: list[World] = [
|
|
964
|
+
world
|
|
965
|
+
]
|
|
966
|
+
|
|
967
|
+
while queue:
|
|
968
|
+
current = queue.pop(
|
|
969
|
+
0
|
|
970
|
+
)
|
|
971
|
+
|
|
972
|
+
for agent in group:
|
|
973
|
+
for candidate in self.accessible(
|
|
974
|
+
agent,
|
|
975
|
+
current,
|
|
976
|
+
):
|
|
977
|
+
if candidate not in visited:
|
|
978
|
+
visited.add(
|
|
979
|
+
candidate
|
|
980
|
+
)
|
|
981
|
+
queue.append(
|
|
982
|
+
candidate
|
|
983
|
+
)
|
|
984
|
+
|
|
985
|
+
return tuple(
|
|
986
|
+
candidate
|
|
987
|
+
for candidate in self._worlds
|
|
988
|
+
if candidate in visited
|
|
989
|
+
)
|
|
990
|
+
|
|
991
|
+
# ---------------------------------------------------------------------
|
|
992
|
+
# Dynamic update API
|
|
993
|
+
# ---------------------------------------------------------------------
|
|
994
|
+
|
|
995
|
+
def update(
|
|
996
|
+
self,
|
|
997
|
+
event: DynamicEvent,
|
|
998
|
+
) -> "EpistemicModel":
|
|
999
|
+
"""
|
|
1000
|
+
Apply a dynamic epistemic event.
|
|
1001
|
+
|
|
1002
|
+
The current model is never mutated.
|
|
1003
|
+
"""
|
|
1004
|
+
|
|
1005
|
+
return event.apply(
|
|
1006
|
+
self
|
|
1007
|
+
)
|
|
1008
|
+
|
|
1009
|
+
def announce(
|
|
1010
|
+
self,
|
|
1011
|
+
formula: Formula,
|
|
1012
|
+
) -> "EpistemicModel":
|
|
1013
|
+
"""
|
|
1014
|
+
Apply a public announcement.
|
|
1015
|
+
"""
|
|
1016
|
+
|
|
1017
|
+
return self.update(
|
|
1018
|
+
PublicAnnouncement(
|
|
1019
|
+
formula
|
|
1020
|
+
)
|
|
1021
|
+
)
|
|
1022
|
+
|
|
1023
|
+
def product_update(
|
|
1024
|
+
self,
|
|
1025
|
+
action_model: ActionModel,
|
|
1026
|
+
) -> "EpistemicModel":
|
|
1027
|
+
"""
|
|
1028
|
+
Apply an epistemic ActionModel.
|
|
1029
|
+
"""
|
|
1030
|
+
|
|
1031
|
+
return self._apply_action_model(
|
|
1032
|
+
action_model
|
|
1033
|
+
)
|
|
1034
|
+
|
|
1035
|
+
# ---------------------------------------------------------------------
|
|
1036
|
+
# Product update
|
|
1037
|
+
# ---------------------------------------------------------------------
|
|
1038
|
+
|
|
1039
|
+
def _apply_action_model(
|
|
1040
|
+
self,
|
|
1041
|
+
action_model: ActionModel,
|
|
1042
|
+
) -> "EpistemicModel":
|
|
1043
|
+
"""
|
|
1044
|
+
Construct the product model:
|
|
1045
|
+
|
|
1046
|
+
M ⊗ A
|
|
1047
|
+
|
|
1048
|
+
with worlds:
|
|
1049
|
+
|
|
1050
|
+
(w, e)
|
|
1051
|
+
|
|
1052
|
+
for every executable world/event pair.
|
|
1053
|
+
|
|
1054
|
+
Product accessibility is:
|
|
1055
|
+
|
|
1056
|
+
(w,e) R_i' (v,f)
|
|
1057
|
+
|
|
1058
|
+
iff
|
|
1059
|
+
|
|
1060
|
+
w R_i v
|
|
1061
|
+
|
|
1062
|
+
and
|
|
1063
|
+
|
|
1064
|
+
e R_i^A f
|
|
1065
|
+
"""
|
|
1066
|
+
|
|
1067
|
+
enabled_pairs: list[
|
|
1068
|
+
tuple[World, str]
|
|
1069
|
+
] = []
|
|
1070
|
+
|
|
1071
|
+
enabled_pair_set: set[
|
|
1072
|
+
tuple[World, str]
|
|
1073
|
+
] = set()
|
|
1074
|
+
|
|
1075
|
+
for world in self._worlds:
|
|
1076
|
+
for action_event in action_model.events:
|
|
1077
|
+
if action_event.enabled(
|
|
1078
|
+
self,
|
|
1079
|
+
world,
|
|
1080
|
+
):
|
|
1081
|
+
pair = (
|
|
1082
|
+
world,
|
|
1083
|
+
action_event.name,
|
|
1084
|
+
)
|
|
1085
|
+
|
|
1086
|
+
enabled_pairs.append(
|
|
1087
|
+
pair
|
|
1088
|
+
)
|
|
1089
|
+
|
|
1090
|
+
enabled_pair_set.add(
|
|
1091
|
+
pair
|
|
1092
|
+
)
|
|
1093
|
+
|
|
1094
|
+
if not enabled_pairs:
|
|
1095
|
+
raise ValueError(
|
|
1096
|
+
"ActionModel has no executable "
|
|
1097
|
+
"world/event pairs"
|
|
1098
|
+
)
|
|
1099
|
+
|
|
1100
|
+
actual_pair = (
|
|
1101
|
+
self._actual,
|
|
1102
|
+
action_model.actual,
|
|
1103
|
+
)
|
|
1104
|
+
|
|
1105
|
+
if actual_pair not in enabled_pair_set:
|
|
1106
|
+
raise ValueError(
|
|
1107
|
+
"Actual action event is not executable "
|
|
1108
|
+
"in the actual world"
|
|
1109
|
+
)
|
|
1110
|
+
|
|
1111
|
+
product_worlds = tuple(
|
|
1112
|
+
ProductWorld(
|
|
1113
|
+
world=world,
|
|
1114
|
+
event=event_name,
|
|
1115
|
+
)
|
|
1116
|
+
for world, event_name
|
|
1117
|
+
in enabled_pairs
|
|
1118
|
+
)
|
|
1119
|
+
|
|
1120
|
+
pair_to_product: dict[
|
|
1121
|
+
tuple[World, str],
|
|
1122
|
+
ProductWorld,
|
|
1123
|
+
] = {
|
|
1124
|
+
(
|
|
1125
|
+
product.world,
|
|
1126
|
+
product.event,
|
|
1127
|
+
): product
|
|
1128
|
+
for product in product_worlds
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
actual_product = pair_to_product[
|
|
1132
|
+
actual_pair
|
|
1133
|
+
]
|
|
1134
|
+
|
|
1135
|
+
relation_overrides: dict[
|
|
1136
|
+
str,
|
|
1137
|
+
dict[
|
|
1138
|
+
ProductWorld,
|
|
1139
|
+
tuple[
|
|
1140
|
+
ProductWorld,
|
|
1141
|
+
...,
|
|
1142
|
+
],
|
|
1143
|
+
],
|
|
1144
|
+
] = {}
|
|
1145
|
+
|
|
1146
|
+
for agent in self._agent_names:
|
|
1147
|
+
source_map: dict[
|
|
1148
|
+
ProductWorld,
|
|
1149
|
+
tuple[
|
|
1150
|
+
ProductWorld,
|
|
1151
|
+
...,
|
|
1152
|
+
],
|
|
1153
|
+
] = {}
|
|
1154
|
+
|
|
1155
|
+
for product_world in product_worlds:
|
|
1156
|
+
base_world = (
|
|
1157
|
+
product_world.world
|
|
1158
|
+
)
|
|
1159
|
+
|
|
1160
|
+
event_name = (
|
|
1161
|
+
product_world.event
|
|
1162
|
+
)
|
|
1163
|
+
|
|
1164
|
+
base_accessible = (
|
|
1165
|
+
self.accessible(
|
|
1166
|
+
agent,
|
|
1167
|
+
base_world,
|
|
1168
|
+
)
|
|
1169
|
+
)
|
|
1170
|
+
|
|
1171
|
+
event_accessible = (
|
|
1172
|
+
action_model.accessible(
|
|
1173
|
+
agent,
|
|
1174
|
+
event_name,
|
|
1175
|
+
)
|
|
1176
|
+
)
|
|
1177
|
+
|
|
1178
|
+
targets: list[
|
|
1179
|
+
ProductWorld
|
|
1180
|
+
] = []
|
|
1181
|
+
|
|
1182
|
+
for candidate_base in base_accessible:
|
|
1183
|
+
for candidate_event in event_accessible:
|
|
1184
|
+
candidate_pair = (
|
|
1185
|
+
candidate_base,
|
|
1186
|
+
candidate_event,
|
|
1187
|
+
)
|
|
1188
|
+
|
|
1189
|
+
if (
|
|
1190
|
+
candidate_pair
|
|
1191
|
+
not in enabled_pair_set
|
|
1192
|
+
):
|
|
1193
|
+
continue
|
|
1194
|
+
|
|
1195
|
+
candidate_product = (
|
|
1196
|
+
pair_to_product[
|
|
1197
|
+
candidate_pair
|
|
1198
|
+
]
|
|
1199
|
+
)
|
|
1200
|
+
|
|
1201
|
+
if (
|
|
1202
|
+
candidate_product
|
|
1203
|
+
not in targets
|
|
1204
|
+
):
|
|
1205
|
+
targets.append(
|
|
1206
|
+
candidate_product
|
|
1207
|
+
)
|
|
1208
|
+
|
|
1209
|
+
source_map[
|
|
1210
|
+
product_world
|
|
1211
|
+
] = tuple(
|
|
1212
|
+
targets
|
|
1213
|
+
)
|
|
1214
|
+
|
|
1215
|
+
relation_overrides[
|
|
1216
|
+
agent
|
|
1217
|
+
] = source_map
|
|
1218
|
+
|
|
1219
|
+
return EpistemicModel(
|
|
1220
|
+
worlds=product_worlds,
|
|
1221
|
+
observations=self._observations,
|
|
1222
|
+
actual=actual_product,
|
|
1223
|
+
propositions=self._propositions,
|
|
1224
|
+
state_space=None,
|
|
1225
|
+
relations=relation_overrides,
|
|
1226
|
+
)
|
|
1227
|
+
|
|
1228
|
+
# ---------------------------------------------------------------------
|
|
1229
|
+
# Public announcement
|
|
1230
|
+
# ---------------------------------------------------------------------
|
|
1231
|
+
|
|
1232
|
+
def _apply_public_announcement(
|
|
1233
|
+
self,
|
|
1234
|
+
formula: Formula,
|
|
1235
|
+
) -> "EpistemicModel":
|
|
1236
|
+
"""
|
|
1237
|
+
Restrict the current model to worlds where formula holds.
|
|
1238
|
+
|
|
1239
|
+
M | φ
|
|
1240
|
+
"""
|
|
1241
|
+
|
|
1242
|
+
retained_worlds = tuple(
|
|
1243
|
+
world
|
|
1244
|
+
for world in self._worlds
|
|
1245
|
+
if self.check(
|
|
1246
|
+
formula,
|
|
1247
|
+
at=world,
|
|
1248
|
+
)
|
|
1249
|
+
)
|
|
1250
|
+
|
|
1251
|
+
if self._actual not in retained_worlds:
|
|
1252
|
+
raise ValueError(
|
|
1253
|
+
"Public announcement is false "
|
|
1254
|
+
"in the actual world"
|
|
1255
|
+
)
|
|
1256
|
+
|
|
1257
|
+
if not retained_worlds:
|
|
1258
|
+
raise ValueError(
|
|
1259
|
+
"Public announcement removed all worlds"
|
|
1260
|
+
)
|
|
1261
|
+
|
|
1262
|
+
updated_state_space = (
|
|
1263
|
+
self._restrict_state_space(
|
|
1264
|
+
retained_worlds
|
|
1265
|
+
)
|
|
1266
|
+
if self._state_space is not None
|
|
1267
|
+
else None
|
|
1268
|
+
)
|
|
1269
|
+
|
|
1270
|
+
updated_relations = (
|
|
1271
|
+
self._restrict_relations(
|
|
1272
|
+
retained_worlds
|
|
1273
|
+
)
|
|
1274
|
+
if self._relation_overrides
|
|
1275
|
+
else None
|
|
1276
|
+
)
|
|
1277
|
+
|
|
1278
|
+
return EpistemicModel(
|
|
1279
|
+
worlds=retained_worlds,
|
|
1280
|
+
observations=self._observations,
|
|
1281
|
+
actual=self._actual,
|
|
1282
|
+
propositions=self._propositions,
|
|
1283
|
+
state_space=updated_state_space,
|
|
1284
|
+
relations=updated_relations,
|
|
1285
|
+
)
|
|
1286
|
+
|
|
1287
|
+
def _restrict_state_space(
|
|
1288
|
+
self,
|
|
1289
|
+
retained_worlds: tuple[World, ...],
|
|
1290
|
+
) -> StateSpace:
|
|
1291
|
+
"""
|
|
1292
|
+
Restrict attached StateSpace to retained worlds.
|
|
1293
|
+
"""
|
|
1294
|
+
|
|
1295
|
+
if self._state_space is None:
|
|
1296
|
+
raise ValueError(
|
|
1297
|
+
"Cannot restrict a missing StateSpace"
|
|
1298
|
+
)
|
|
1299
|
+
|
|
1300
|
+
retained = set(
|
|
1301
|
+
retained_worlds
|
|
1302
|
+
)
|
|
1303
|
+
|
|
1304
|
+
initials = tuple(
|
|
1305
|
+
initial
|
|
1306
|
+
for initial in self._state_space.initials
|
|
1307
|
+
if initial in retained
|
|
1308
|
+
)
|
|
1309
|
+
|
|
1310
|
+
if not initials:
|
|
1311
|
+
raise ValueError(
|
|
1312
|
+
"Public announcement removed all "
|
|
1313
|
+
"initial worlds from the StateSpace"
|
|
1314
|
+
)
|
|
1315
|
+
|
|
1316
|
+
transitions = tuple(
|
|
1317
|
+
transition
|
|
1318
|
+
for transition
|
|
1319
|
+
in self._state_space.transitions
|
|
1320
|
+
if (
|
|
1321
|
+
transition.source in retained
|
|
1322
|
+
and transition.target in retained
|
|
1323
|
+
)
|
|
1324
|
+
)
|
|
1325
|
+
|
|
1326
|
+
return StateSpace(
|
|
1327
|
+
initials=initials,
|
|
1328
|
+
states=retained_worlds,
|
|
1329
|
+
transitions=transitions,
|
|
1330
|
+
)
|
|
1331
|
+
|
|
1332
|
+
def _restrict_relations(
|
|
1333
|
+
self,
|
|
1334
|
+
retained_worlds: tuple[World, ...],
|
|
1335
|
+
) -> dict[
|
|
1336
|
+
str,
|
|
1337
|
+
dict[
|
|
1338
|
+
World,
|
|
1339
|
+
tuple[World, ...],
|
|
1340
|
+
],
|
|
1341
|
+
]:
|
|
1342
|
+
retained = set(
|
|
1343
|
+
retained_worlds
|
|
1344
|
+
)
|
|
1345
|
+
|
|
1346
|
+
result: dict[
|
|
1347
|
+
str,
|
|
1348
|
+
dict[
|
|
1349
|
+
World,
|
|
1350
|
+
tuple[World, ...],
|
|
1351
|
+
],
|
|
1352
|
+
] = {}
|
|
1353
|
+
|
|
1354
|
+
for agent, relation in (
|
|
1355
|
+
self._relation_overrides.items()
|
|
1356
|
+
):
|
|
1357
|
+
filtered: dict[
|
|
1358
|
+
World,
|
|
1359
|
+
tuple[World, ...],
|
|
1360
|
+
] = {}
|
|
1361
|
+
|
|
1362
|
+
for source, targets in (
|
|
1363
|
+
relation.items()
|
|
1364
|
+
):
|
|
1365
|
+
if source not in retained:
|
|
1366
|
+
continue
|
|
1367
|
+
|
|
1368
|
+
filtered[
|
|
1369
|
+
source
|
|
1370
|
+
] = tuple(
|
|
1371
|
+
target
|
|
1372
|
+
for target in targets
|
|
1373
|
+
if target in retained
|
|
1374
|
+
)
|
|
1375
|
+
|
|
1376
|
+
result[
|
|
1377
|
+
agent
|
|
1378
|
+
] = filtered
|
|
1379
|
+
|
|
1380
|
+
return result
|
|
1381
|
+
|
|
1382
|
+
# ---------------------------------------------------------------------
|
|
1383
|
+
# State-space helpers
|
|
1384
|
+
# ---------------------------------------------------------------------
|
|
1385
|
+
|
|
1386
|
+
@staticmethod
|
|
1387
|
+
def _build_successors_map(
|
|
1388
|
+
state_space: StateSpace,
|
|
1389
|
+
) -> dict[
|
|
1390
|
+
Configuration,
|
|
1391
|
+
tuple[Configuration, ...],
|
|
1392
|
+
]:
|
|
1393
|
+
states = tuple(
|
|
1394
|
+
state_space.states
|
|
1395
|
+
)
|
|
1396
|
+
|
|
1397
|
+
state_set = set(
|
|
1398
|
+
states
|
|
1399
|
+
)
|
|
1400
|
+
|
|
1401
|
+
result_lists: dict[
|
|
1402
|
+
Configuration,
|
|
1403
|
+
list[Configuration],
|
|
1404
|
+
] = {
|
|
1405
|
+
state: []
|
|
1406
|
+
for state in states
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
for transition in (
|
|
1410
|
+
state_space.transitions
|
|
1411
|
+
):
|
|
1412
|
+
source = transition.source
|
|
1413
|
+
target = transition.target
|
|
1414
|
+
|
|
1415
|
+
if source not in state_set:
|
|
1416
|
+
raise ValueError(
|
|
1417
|
+
"StateSpace transition source is not "
|
|
1418
|
+
"present in StateSpace.states"
|
|
1419
|
+
)
|
|
1420
|
+
|
|
1421
|
+
if target not in state_set:
|
|
1422
|
+
raise ValueError(
|
|
1423
|
+
"StateSpace transition target is not "
|
|
1424
|
+
"present in StateSpace.states"
|
|
1425
|
+
)
|
|
1426
|
+
|
|
1427
|
+
if target not in result_lists[
|
|
1428
|
+
source
|
|
1429
|
+
]:
|
|
1430
|
+
result_lists[
|
|
1431
|
+
source
|
|
1432
|
+
].append(
|
|
1433
|
+
target
|
|
1434
|
+
)
|
|
1435
|
+
|
|
1436
|
+
return {
|
|
1437
|
+
source: tuple(
|
|
1438
|
+
targets
|
|
1439
|
+
)
|
|
1440
|
+
for source, targets
|
|
1441
|
+
in result_lists.items()
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
def successors(
|
|
1445
|
+
self,
|
|
1446
|
+
world: World,
|
|
1447
|
+
) -> tuple[World, ...]:
|
|
1448
|
+
"""
|
|
1449
|
+
Return temporal successors.
|
|
1450
|
+
|
|
1451
|
+
For a model with StateSpace, return actual transition
|
|
1452
|
+
successors.
|
|
1453
|
+
|
|
1454
|
+
For a static epistemic snapshot without StateSpace,
|
|
1455
|
+
use implicit stuttering:
|
|
1456
|
+
|
|
1457
|
+
w -> w
|
|
1458
|
+
"""
|
|
1459
|
+
|
|
1460
|
+
if world not in self._worlds:
|
|
1461
|
+
raise ValueError(
|
|
1462
|
+
"World does not belong "
|
|
1463
|
+
"to the epistemic model"
|
|
1464
|
+
)
|
|
1465
|
+
|
|
1466
|
+
# Static epistemic model:
|
|
1467
|
+
#
|
|
1468
|
+
# w -> w -> w -> ...
|
|
1469
|
+
#
|
|
1470
|
+
# This allows temporal formulas such as G(phi)
|
|
1471
|
+
# to express state-invariant properties over a
|
|
1472
|
+
# snapshot model.
|
|
1473
|
+
if self._state_space is None:
|
|
1474
|
+
return (
|
|
1475
|
+
world,
|
|
1476
|
+
)
|
|
1477
|
+
|
|
1478
|
+
if not isinstance(
|
|
1479
|
+
world,
|
|
1480
|
+
Configuration,
|
|
1481
|
+
):
|
|
1482
|
+
raise ValueError(
|
|
1483
|
+
"ProductWorld does not currently "
|
|
1484
|
+
"have StateSpace temporal successors"
|
|
1485
|
+
)
|
|
1486
|
+
|
|
1487
|
+
return self._successors_map[
|
|
1488
|
+
world
|
|
1489
|
+
]
|
|
1490
|
+
|
|
1491
|
+
def _temporal_successors(
|
|
1492
|
+
self,
|
|
1493
|
+
world: World,
|
|
1494
|
+
) -> tuple[World, ...]:
|
|
1495
|
+
successors = self.successors(
|
|
1496
|
+
world
|
|
1497
|
+
)
|
|
1498
|
+
|
|
1499
|
+
if successors:
|
|
1500
|
+
return successors
|
|
1501
|
+
|
|
1502
|
+
# Terminal state:
|
|
1503
|
+
#
|
|
1504
|
+
# w -> w
|
|
1505
|
+
#
|
|
1506
|
+
return (
|
|
1507
|
+
world,
|
|
1508
|
+
)
|
|
1509
|
+
|
|
1510
|
+
# ---------------------------------------------------------------------
|
|
1511
|
+
# Propositions
|
|
1512
|
+
# ---------------------------------------------------------------------
|
|
1513
|
+
|
|
1514
|
+
@staticmethod
|
|
1515
|
+
def _normalize_propositions(
|
|
1516
|
+
propositions: (
|
|
1517
|
+
Iterable[Proposition]
|
|
1518
|
+
| Mapping[
|
|
1519
|
+
str,
|
|
1520
|
+
Proposition
|
|
1521
|
+
| Callable[[Configuration], bool],
|
|
1522
|
+
]
|
|
1523
|
+
| None
|
|
1524
|
+
),
|
|
1525
|
+
) -> dict[
|
|
1526
|
+
str,
|
|
1527
|
+
Proposition,
|
|
1528
|
+
]:
|
|
1529
|
+
if propositions is None:
|
|
1530
|
+
return {}
|
|
1531
|
+
|
|
1532
|
+
if isinstance(
|
|
1533
|
+
propositions,
|
|
1534
|
+
Mapping,
|
|
1535
|
+
):
|
|
1536
|
+
result: dict[str, Proposition] = {}
|
|
1537
|
+
|
|
1538
|
+
for name, proposition in propositions.items():
|
|
1539
|
+
if not isinstance(name, str) or not name:
|
|
1540
|
+
raise ValueError(
|
|
1541
|
+
"Proposition names must be non-empty strings"
|
|
1542
|
+
)
|
|
1543
|
+
|
|
1544
|
+
if isinstance(
|
|
1545
|
+
proposition,
|
|
1546
|
+
Proposition,
|
|
1547
|
+
):
|
|
1548
|
+
result[name] = proposition
|
|
1549
|
+
continue
|
|
1550
|
+
|
|
1551
|
+
if callable(proposition):
|
|
1552
|
+
result[name] = Proposition(
|
|
1553
|
+
name,
|
|
1554
|
+
proposition,
|
|
1555
|
+
)
|
|
1556
|
+
continue
|
|
1557
|
+
|
|
1558
|
+
raise TypeError(
|
|
1559
|
+
"Proposition mappings must contain Proposition"
|
|
1560
|
+
" objects or callables"
|
|
1561
|
+
)
|
|
1562
|
+
|
|
1563
|
+
return result
|
|
1564
|
+
|
|
1565
|
+
result = {}
|
|
1566
|
+
|
|
1567
|
+
for proposition in propositions:
|
|
1568
|
+
if not isinstance(
|
|
1569
|
+
proposition,
|
|
1570
|
+
Proposition,
|
|
1571
|
+
):
|
|
1572
|
+
raise TypeError(
|
|
1573
|
+
"Iterable propositions must contain Proposition objects"
|
|
1574
|
+
)
|
|
1575
|
+
|
|
1576
|
+
result[
|
|
1577
|
+
proposition.name
|
|
1578
|
+
] = proposition
|
|
1579
|
+
|
|
1580
|
+
return result
|
|
1581
|
+
|
|
1582
|
+
def proposition_holds(
|
|
1583
|
+
self,
|
|
1584
|
+
name: str,
|
|
1585
|
+
world: World,
|
|
1586
|
+
) -> bool:
|
|
1587
|
+
proposition = self._propositions.get(
|
|
1588
|
+
name
|
|
1589
|
+
)
|
|
1590
|
+
|
|
1591
|
+
configuration = _configuration(
|
|
1592
|
+
world
|
|
1593
|
+
)
|
|
1594
|
+
|
|
1595
|
+
if proposition is not None:
|
|
1596
|
+
return proposition.holds(
|
|
1597
|
+
configuration
|
|
1598
|
+
)
|
|
1599
|
+
|
|
1600
|
+
return (
|
|
1601
|
+
configuration.state.get(
|
|
1602
|
+
name
|
|
1603
|
+
)
|
|
1604
|
+
is True
|
|
1605
|
+
)
|
|
1606
|
+
|
|
1607
|
+
# ---------------------------------------------------------------------
|
|
1608
|
+
# Temporal fixed points
|
|
1609
|
+
# ---------------------------------------------------------------------
|
|
1610
|
+
|
|
1611
|
+
def _eventually_set(
|
|
1612
|
+
self,
|
|
1613
|
+
operand: Formula,
|
|
1614
|
+
cache: dict[
|
|
1615
|
+
tuple[Formula, World],
|
|
1616
|
+
bool,
|
|
1617
|
+
],
|
|
1618
|
+
) -> set[World]:
|
|
1619
|
+
satisfying = {
|
|
1620
|
+
world
|
|
1621
|
+
for world in self._worlds
|
|
1622
|
+
if self._check(
|
|
1623
|
+
operand,
|
|
1624
|
+
world,
|
|
1625
|
+
cache,
|
|
1626
|
+
)
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
changed = True
|
|
1630
|
+
|
|
1631
|
+
while changed:
|
|
1632
|
+
changed = False
|
|
1633
|
+
|
|
1634
|
+
for world in self._worlds:
|
|
1635
|
+
if world in satisfying:
|
|
1636
|
+
continue
|
|
1637
|
+
|
|
1638
|
+
successors = (
|
|
1639
|
+
self._temporal_successors(
|
|
1640
|
+
world
|
|
1641
|
+
)
|
|
1642
|
+
)
|
|
1643
|
+
|
|
1644
|
+
if all(
|
|
1645
|
+
successor in satisfying
|
|
1646
|
+
for successor in successors
|
|
1647
|
+
):
|
|
1648
|
+
satisfying.add(
|
|
1649
|
+
world
|
|
1650
|
+
)
|
|
1651
|
+
|
|
1652
|
+
changed = True
|
|
1653
|
+
|
|
1654
|
+
return satisfying
|
|
1655
|
+
|
|
1656
|
+
def _always_set(
|
|
1657
|
+
self,
|
|
1658
|
+
operand: Formula,
|
|
1659
|
+
cache: dict[
|
|
1660
|
+
tuple[Formula, World],
|
|
1661
|
+
bool,
|
|
1662
|
+
],
|
|
1663
|
+
) -> set[World]:
|
|
1664
|
+
satisfying = set(
|
|
1665
|
+
self._worlds
|
|
1666
|
+
)
|
|
1667
|
+
|
|
1668
|
+
changed = True
|
|
1669
|
+
|
|
1670
|
+
while changed:
|
|
1671
|
+
changed = False
|
|
1672
|
+
|
|
1673
|
+
for world in tuple(
|
|
1674
|
+
satisfying
|
|
1675
|
+
):
|
|
1676
|
+
if not self._check(
|
|
1677
|
+
operand,
|
|
1678
|
+
world,
|
|
1679
|
+
cache,
|
|
1680
|
+
):
|
|
1681
|
+
satisfying.remove(
|
|
1682
|
+
world
|
|
1683
|
+
)
|
|
1684
|
+
|
|
1685
|
+
changed = True
|
|
1686
|
+
|
|
1687
|
+
continue
|
|
1688
|
+
|
|
1689
|
+
successors = (
|
|
1690
|
+
self._temporal_successors(
|
|
1691
|
+
world
|
|
1692
|
+
)
|
|
1693
|
+
)
|
|
1694
|
+
|
|
1695
|
+
if any(
|
|
1696
|
+
successor
|
|
1697
|
+
not in satisfying
|
|
1698
|
+
for successor
|
|
1699
|
+
in successors
|
|
1700
|
+
):
|
|
1701
|
+
satisfying.remove(
|
|
1702
|
+
world
|
|
1703
|
+
)
|
|
1704
|
+
|
|
1705
|
+
changed = True
|
|
1706
|
+
|
|
1707
|
+
return satisfying
|
|
1708
|
+
|
|
1709
|
+
def _until_set(
|
|
1710
|
+
self,
|
|
1711
|
+
left: Formula,
|
|
1712
|
+
right: Formula,
|
|
1713
|
+
cache: dict[
|
|
1714
|
+
tuple[Formula, World],
|
|
1715
|
+
bool,
|
|
1716
|
+
],
|
|
1717
|
+
) -> set[World]:
|
|
1718
|
+
satisfying = {
|
|
1719
|
+
world
|
|
1720
|
+
for world in self._worlds
|
|
1721
|
+
if self._check(
|
|
1722
|
+
right,
|
|
1723
|
+
world,
|
|
1724
|
+
cache,
|
|
1725
|
+
)
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
changed = True
|
|
1729
|
+
|
|
1730
|
+
while changed:
|
|
1731
|
+
changed = False
|
|
1732
|
+
|
|
1733
|
+
for world in self._worlds:
|
|
1734
|
+
if world in satisfying:
|
|
1735
|
+
continue
|
|
1736
|
+
|
|
1737
|
+
if not self._check(
|
|
1738
|
+
left,
|
|
1739
|
+
world,
|
|
1740
|
+
cache,
|
|
1741
|
+
):
|
|
1742
|
+
continue
|
|
1743
|
+
|
|
1744
|
+
successors = (
|
|
1745
|
+
self._temporal_successors(
|
|
1746
|
+
world
|
|
1747
|
+
)
|
|
1748
|
+
)
|
|
1749
|
+
|
|
1750
|
+
if all(
|
|
1751
|
+
successor in satisfying
|
|
1752
|
+
for successor in successors
|
|
1753
|
+
):
|
|
1754
|
+
satisfying.add(
|
|
1755
|
+
world
|
|
1756
|
+
)
|
|
1757
|
+
|
|
1758
|
+
changed = True
|
|
1759
|
+
|
|
1760
|
+
return satisfying
|
|
1761
|
+
|
|
1762
|
+
# ---------------------------------------------------------------------
|
|
1763
|
+
# Model checking
|
|
1764
|
+
# ---------------------------------------------------------------------
|
|
1765
|
+
|
|
1766
|
+
def check(
|
|
1767
|
+
self,
|
|
1768
|
+
formula: Formula,
|
|
1769
|
+
at: World | None = None,
|
|
1770
|
+
) -> bool:
|
|
1771
|
+
world = (
|
|
1772
|
+
self._actual
|
|
1773
|
+
if at is None
|
|
1774
|
+
else at
|
|
1775
|
+
)
|
|
1776
|
+
|
|
1777
|
+
if world not in self._worlds:
|
|
1778
|
+
raise ValueError(
|
|
1779
|
+
"World does not belong "
|
|
1780
|
+
"to the epistemic model"
|
|
1781
|
+
)
|
|
1782
|
+
|
|
1783
|
+
cache: dict[
|
|
1784
|
+
tuple[Formula, World],
|
|
1785
|
+
bool,
|
|
1786
|
+
] = {}
|
|
1787
|
+
|
|
1788
|
+
return self._check(
|
|
1789
|
+
formula,
|
|
1790
|
+
world,
|
|
1791
|
+
cache,
|
|
1792
|
+
)
|
|
1793
|
+
|
|
1794
|
+
def _check(
|
|
1795
|
+
self,
|
|
1796
|
+
formula: Formula,
|
|
1797
|
+
world: World,
|
|
1798
|
+
cache: dict[
|
|
1799
|
+
tuple[Formula, World],
|
|
1800
|
+
bool,
|
|
1801
|
+
],
|
|
1802
|
+
) -> bool:
|
|
1803
|
+
key = (
|
|
1804
|
+
formula,
|
|
1805
|
+
world,
|
|
1806
|
+
)
|
|
1807
|
+
|
|
1808
|
+
if key in cache:
|
|
1809
|
+
return cache[key]
|
|
1810
|
+
|
|
1811
|
+
# -----------------------------------------------------------------
|
|
1812
|
+
# Atom
|
|
1813
|
+
# -----------------------------------------------------------------
|
|
1814
|
+
|
|
1815
|
+
if isinstance(
|
|
1816
|
+
formula,
|
|
1817
|
+
Atom,
|
|
1818
|
+
):
|
|
1819
|
+
result = self.proposition_holds(
|
|
1820
|
+
formula.name,
|
|
1821
|
+
world,
|
|
1822
|
+
)
|
|
1823
|
+
|
|
1824
|
+
# -----------------------------------------------------------------
|
|
1825
|
+
# Boolean
|
|
1826
|
+
# -----------------------------------------------------------------
|
|
1827
|
+
|
|
1828
|
+
elif isinstance(
|
|
1829
|
+
formula,
|
|
1830
|
+
Not,
|
|
1831
|
+
):
|
|
1832
|
+
result = not self._check(
|
|
1833
|
+
formula.operand,
|
|
1834
|
+
world,
|
|
1835
|
+
cache,
|
|
1836
|
+
)
|
|
1837
|
+
|
|
1838
|
+
elif isinstance(
|
|
1839
|
+
formula,
|
|
1840
|
+
And,
|
|
1841
|
+
):
|
|
1842
|
+
result = (
|
|
1843
|
+
self._check(
|
|
1844
|
+
formula.left,
|
|
1845
|
+
world,
|
|
1846
|
+
cache,
|
|
1847
|
+
)
|
|
1848
|
+
and self._check(
|
|
1849
|
+
formula.right,
|
|
1850
|
+
world,
|
|
1851
|
+
cache,
|
|
1852
|
+
)
|
|
1853
|
+
)
|
|
1854
|
+
|
|
1855
|
+
elif isinstance(
|
|
1856
|
+
formula,
|
|
1857
|
+
Or,
|
|
1858
|
+
):
|
|
1859
|
+
result = (
|
|
1860
|
+
self._check(
|
|
1861
|
+
formula.left,
|
|
1862
|
+
world,
|
|
1863
|
+
cache,
|
|
1864
|
+
)
|
|
1865
|
+
or self._check(
|
|
1866
|
+
formula.right,
|
|
1867
|
+
world,
|
|
1868
|
+
cache,
|
|
1869
|
+
)
|
|
1870
|
+
)
|
|
1871
|
+
|
|
1872
|
+
# -----------------------------------------------------------------
|
|
1873
|
+
# Temporal
|
|
1874
|
+
# -----------------------------------------------------------------
|
|
1875
|
+
|
|
1876
|
+
elif isinstance(
|
|
1877
|
+
formula,
|
|
1878
|
+
Next,
|
|
1879
|
+
):
|
|
1880
|
+
successors = (
|
|
1881
|
+
self._temporal_successors(
|
|
1882
|
+
world
|
|
1883
|
+
)
|
|
1884
|
+
)
|
|
1885
|
+
|
|
1886
|
+
result = all(
|
|
1887
|
+
self._check(
|
|
1888
|
+
formula.operand,
|
|
1889
|
+
successor,
|
|
1890
|
+
cache,
|
|
1891
|
+
)
|
|
1892
|
+
for successor in successors
|
|
1893
|
+
)
|
|
1894
|
+
|
|
1895
|
+
elif isinstance(
|
|
1896
|
+
formula,
|
|
1897
|
+
Eventually,
|
|
1898
|
+
):
|
|
1899
|
+
satisfying = (
|
|
1900
|
+
self._eventually_set(
|
|
1901
|
+
formula.operand,
|
|
1902
|
+
cache,
|
|
1903
|
+
)
|
|
1904
|
+
)
|
|
1905
|
+
|
|
1906
|
+
for candidate in self._worlds:
|
|
1907
|
+
cache[
|
|
1908
|
+
(
|
|
1909
|
+
formula,
|
|
1910
|
+
candidate,
|
|
1911
|
+
)
|
|
1912
|
+
] = (
|
|
1913
|
+
candidate
|
|
1914
|
+
in satisfying
|
|
1915
|
+
)
|
|
1916
|
+
|
|
1917
|
+
result = (
|
|
1918
|
+
world
|
|
1919
|
+
in satisfying
|
|
1920
|
+
)
|
|
1921
|
+
|
|
1922
|
+
elif isinstance(
|
|
1923
|
+
formula,
|
|
1924
|
+
Always,
|
|
1925
|
+
):
|
|
1926
|
+
satisfying = (
|
|
1927
|
+
self._always_set(
|
|
1928
|
+
formula.operand,
|
|
1929
|
+
cache,
|
|
1930
|
+
)
|
|
1931
|
+
)
|
|
1932
|
+
|
|
1933
|
+
for candidate in self._worlds:
|
|
1934
|
+
cache[
|
|
1935
|
+
(
|
|
1936
|
+
formula,
|
|
1937
|
+
candidate,
|
|
1938
|
+
)
|
|
1939
|
+
] = (
|
|
1940
|
+
candidate
|
|
1941
|
+
in satisfying
|
|
1942
|
+
)
|
|
1943
|
+
|
|
1944
|
+
result = (
|
|
1945
|
+
world
|
|
1946
|
+
in satisfying
|
|
1947
|
+
)
|
|
1948
|
+
|
|
1949
|
+
elif isinstance(
|
|
1950
|
+
formula,
|
|
1951
|
+
Until,
|
|
1952
|
+
):
|
|
1953
|
+
satisfying = (
|
|
1954
|
+
self._until_set(
|
|
1955
|
+
formula.left,
|
|
1956
|
+
formula.right,
|
|
1957
|
+
cache,
|
|
1958
|
+
)
|
|
1959
|
+
)
|
|
1960
|
+
|
|
1961
|
+
for candidate in self._worlds:
|
|
1962
|
+
cache[
|
|
1963
|
+
(
|
|
1964
|
+
formula,
|
|
1965
|
+
candidate,
|
|
1966
|
+
)
|
|
1967
|
+
] = (
|
|
1968
|
+
candidate
|
|
1969
|
+
in satisfying
|
|
1970
|
+
)
|
|
1971
|
+
|
|
1972
|
+
result = (
|
|
1973
|
+
world
|
|
1974
|
+
in satisfying
|
|
1975
|
+
)
|
|
1976
|
+
|
|
1977
|
+
# -----------------------------------------------------------------
|
|
1978
|
+
# Knowledge
|
|
1979
|
+
# -----------------------------------------------------------------
|
|
1980
|
+
|
|
1981
|
+
elif isinstance(
|
|
1982
|
+
formula,
|
|
1983
|
+
Knowledge,
|
|
1984
|
+
):
|
|
1985
|
+
result = all(
|
|
1986
|
+
self._check(
|
|
1987
|
+
formula.operand,
|
|
1988
|
+
candidate,
|
|
1989
|
+
cache,
|
|
1990
|
+
)
|
|
1991
|
+
for candidate in self.accessible(
|
|
1992
|
+
formula.agent,
|
|
1993
|
+
world,
|
|
1994
|
+
)
|
|
1995
|
+
)
|
|
1996
|
+
|
|
1997
|
+
# -----------------------------------------------------------------
|
|
1998
|
+
# Everyone knows
|
|
1999
|
+
# -----------------------------------------------------------------
|
|
2000
|
+
|
|
2001
|
+
elif isinstance(
|
|
2002
|
+
formula,
|
|
2003
|
+
EveryoneKnows,
|
|
2004
|
+
):
|
|
2005
|
+
result = all(
|
|
2006
|
+
self._check(
|
|
2007
|
+
Knowledge(
|
|
2008
|
+
agent=agent,
|
|
2009
|
+
operand=formula.operand,
|
|
2010
|
+
),
|
|
2011
|
+
world,
|
|
2012
|
+
cache,
|
|
2013
|
+
)
|
|
2014
|
+
for agent
|
|
2015
|
+
in formula.agents
|
|
2016
|
+
)
|
|
2017
|
+
|
|
2018
|
+
# -----------------------------------------------------------------
|
|
2019
|
+
# Distributed knowledge
|
|
2020
|
+
# -----------------------------------------------------------------
|
|
2021
|
+
|
|
2022
|
+
elif isinstance(
|
|
2023
|
+
formula,
|
|
2024
|
+
DistributedKnowledge,
|
|
2025
|
+
):
|
|
2026
|
+
result = all(
|
|
2027
|
+
self._check(
|
|
2028
|
+
formula.operand,
|
|
2029
|
+
candidate,
|
|
2030
|
+
cache,
|
|
2031
|
+
)
|
|
2032
|
+
for candidate
|
|
2033
|
+
in self.distributed_accessible(
|
|
2034
|
+
formula.agents,
|
|
2035
|
+
world,
|
|
2036
|
+
)
|
|
2037
|
+
)
|
|
2038
|
+
|
|
2039
|
+
# -----------------------------------------------------------------
|
|
2040
|
+
# Common knowledge
|
|
2041
|
+
# -----------------------------------------------------------------
|
|
2042
|
+
|
|
2043
|
+
elif isinstance(
|
|
2044
|
+
formula,
|
|
2045
|
+
CommonKnowledge,
|
|
2046
|
+
):
|
|
2047
|
+
result = all(
|
|
2048
|
+
self._check(
|
|
2049
|
+
formula.operand,
|
|
2050
|
+
candidate,
|
|
2051
|
+
cache,
|
|
2052
|
+
)
|
|
2053
|
+
for candidate
|
|
2054
|
+
in self.common_knowledge_closure(
|
|
2055
|
+
formula.agents,
|
|
2056
|
+
world,
|
|
2057
|
+
)
|
|
2058
|
+
)
|
|
2059
|
+
|
|
2060
|
+
else:
|
|
2061
|
+
raise TypeError(
|
|
2062
|
+
f"Unsupported formula type: "
|
|
2063
|
+
f"{type(formula).__name__}"
|
|
2064
|
+
)
|
|
2065
|
+
|
|
2066
|
+
cache[key] = result
|
|
2067
|
+
|
|
2068
|
+
return result
|