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/observation.py
ADDED
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Hashable, Iterable, Protocol, TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from .agent import Agent
|
|
8
|
+
from .runtime import Configuration
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Observation(Protocol):
|
|
12
|
+
"""
|
|
13
|
+
Describes what an agent can observe in a configuration.
|
|
14
|
+
|
|
15
|
+
The returned value must be hashable because observations
|
|
16
|
+
induce epistemic equivalence classes over worlds.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __call__(
|
|
20
|
+
self,
|
|
21
|
+
agent: "Agent",
|
|
22
|
+
configuration: "Configuration",
|
|
23
|
+
) -> Hashable:
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _normalize_keys(
|
|
28
|
+
keys: str | Iterable[str],
|
|
29
|
+
) -> tuple[str, ...]:
|
|
30
|
+
if isinstance(keys, str):
|
|
31
|
+
return (keys,)
|
|
32
|
+
|
|
33
|
+
return tuple(keys)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _ensure_hashable(value: Any) -> Hashable:
|
|
37
|
+
hash(value)
|
|
38
|
+
return value
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ============================================================================
|
|
42
|
+
# Declarative observation components
|
|
43
|
+
# ============================================================================
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True, slots=True)
|
|
47
|
+
class LocalObservation:
|
|
48
|
+
"""
|
|
49
|
+
Observation of one particular agent's local information.
|
|
50
|
+
|
|
51
|
+
Examples:
|
|
52
|
+
|
|
53
|
+
local("bob", state=("x",))
|
|
54
|
+
|
|
55
|
+
local("bob", state=("x",), process=True)
|
|
56
|
+
|
|
57
|
+
The observation is represented as a tuple of tagged fragments:
|
|
58
|
+
|
|
59
|
+
(
|
|
60
|
+
("state", ...),
|
|
61
|
+
("process", ...),
|
|
62
|
+
)
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
agent_name: str
|
|
66
|
+
state_keys: tuple[str, ...] = ()
|
|
67
|
+
include_process: bool = False
|
|
68
|
+
|
|
69
|
+
def __post_init__(self) -> None:
|
|
70
|
+
if not self.agent_name.strip():
|
|
71
|
+
raise ValueError(
|
|
72
|
+
"agent_name must not be empty"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
for key in self.state_keys:
|
|
76
|
+
if not isinstance(key, str) or not key.strip():
|
|
77
|
+
raise ValueError(
|
|
78
|
+
"state observation keys must be non-empty strings"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def __call__(
|
|
82
|
+
self,
|
|
83
|
+
agent: "Agent",
|
|
84
|
+
configuration: "Configuration",
|
|
85
|
+
) -> Hashable:
|
|
86
|
+
if agent.name != self.agent_name:
|
|
87
|
+
raise ValueError(
|
|
88
|
+
f"Local observation for {self.agent_name!r} "
|
|
89
|
+
f"was applied to {agent.name!r}"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
fragments: list[tuple[str, Hashable]] = []
|
|
93
|
+
|
|
94
|
+
if self.state_keys:
|
|
95
|
+
local_state = tuple(
|
|
96
|
+
(key, agent.get_state(key))
|
|
97
|
+
for key in self.state_keys
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
_ensure_hashable(local_state)
|
|
101
|
+
|
|
102
|
+
fragments.append(
|
|
103
|
+
("state", local_state)
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
if self.include_process:
|
|
107
|
+
process = _ensure_hashable(
|
|
108
|
+
agent.process
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
fragments.append(
|
|
112
|
+
("process", process)
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return _ensure_hashable(
|
|
116
|
+
tuple(fragments)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def local(
|
|
121
|
+
agent_name: str,
|
|
122
|
+
*,
|
|
123
|
+
state: str | Iterable[str] = (),
|
|
124
|
+
process: bool = False,
|
|
125
|
+
) -> LocalObservation:
|
|
126
|
+
"""
|
|
127
|
+
Declare an observation of one agent's local information.
|
|
128
|
+
|
|
129
|
+
Example:
|
|
130
|
+
|
|
131
|
+
local(
|
|
132
|
+
"bob",
|
|
133
|
+
state=("x",),
|
|
134
|
+
process=True,
|
|
135
|
+
)
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
return LocalObservation(
|
|
139
|
+
agent_name=agent_name,
|
|
140
|
+
state_keys=_normalize_keys(state),
|
|
141
|
+
include_process=process,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@dataclass(frozen=True, slots=True)
|
|
146
|
+
class GlobalObservation:
|
|
147
|
+
"""
|
|
148
|
+
Observation of globally visible information.
|
|
149
|
+
|
|
150
|
+
Example:
|
|
151
|
+
|
|
152
|
+
public("round")
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
keys: tuple[str, ...]
|
|
156
|
+
|
|
157
|
+
def __post_init__(self) -> None:
|
|
158
|
+
for key in self.keys:
|
|
159
|
+
if not isinstance(key, str) or not key.strip():
|
|
160
|
+
raise ValueError(
|
|
161
|
+
"global observation keys must be non-empty strings"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
def __call__(
|
|
165
|
+
self,
|
|
166
|
+
agent: "Agent",
|
|
167
|
+
configuration: "Configuration",
|
|
168
|
+
) -> Hashable:
|
|
169
|
+
value = tuple(
|
|
170
|
+
(key, configuration.state.get(key))
|
|
171
|
+
for key in self.keys
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
_ensure_hashable(value)
|
|
175
|
+
|
|
176
|
+
return _ensure_hashable(
|
|
177
|
+
(
|
|
178
|
+
("global", value),
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def global_(
|
|
184
|
+
*keys: str,
|
|
185
|
+
) -> GlobalObservation:
|
|
186
|
+
"""
|
|
187
|
+
Declare globally observable variables.
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
return GlobalObservation(
|
|
191
|
+
tuple(keys)
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def public(
|
|
196
|
+
*keys: str,
|
|
197
|
+
) -> GlobalObservation:
|
|
198
|
+
"""
|
|
199
|
+
Semantic alias for global_().
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
return global_(*keys)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# ============================================================================
|
|
206
|
+
# Combined observation
|
|
207
|
+
# ============================================================================
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@dataclass(frozen=True, slots=True)
|
|
211
|
+
class _CombinedObservation:
|
|
212
|
+
"""
|
|
213
|
+
Composition of declarative observation components.
|
|
214
|
+
|
|
215
|
+
Declarative components return tuples of tagged fragments.
|
|
216
|
+
We flatten those fragments here so that:
|
|
217
|
+
|
|
218
|
+
local(...) + public(...)
|
|
219
|
+
|
|
220
|
+
becomes:
|
|
221
|
+
|
|
222
|
+
(
|
|
223
|
+
("global", ...),
|
|
224
|
+
("state", ...),
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
rather than introducing accidental nested tuples.
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
components: tuple[Observation, ...]
|
|
231
|
+
|
|
232
|
+
def __call__(
|
|
233
|
+
self,
|
|
234
|
+
agent: "Agent",
|
|
235
|
+
configuration: "Configuration",
|
|
236
|
+
) -> Hashable:
|
|
237
|
+
fragments: list[Hashable] = []
|
|
238
|
+
|
|
239
|
+
for component in self.components:
|
|
240
|
+
value = component(
|
|
241
|
+
agent,
|
|
242
|
+
configuration,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
if isinstance(
|
|
246
|
+
component,
|
|
247
|
+
(
|
|
248
|
+
LocalObservation,
|
|
249
|
+
GlobalObservation,
|
|
250
|
+
),
|
|
251
|
+
):
|
|
252
|
+
# Both declarative components return
|
|
253
|
+
# tuples of tagged fragments.
|
|
254
|
+
fragments.extend(value)
|
|
255
|
+
else:
|
|
256
|
+
# Preserve arbitrary custom observations.
|
|
257
|
+
fragments.append(
|
|
258
|
+
(
|
|
259
|
+
"component",
|
|
260
|
+
_ensure_hashable(value),
|
|
261
|
+
)
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
return _ensure_hashable(
|
|
265
|
+
tuple(fragments)
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
@dataclass(frozen=True, slots=True)
|
|
270
|
+
class ObservationBuilder:
|
|
271
|
+
"""
|
|
272
|
+
Declarative builder for per-agent observations.
|
|
273
|
+
|
|
274
|
+
Example:
|
|
275
|
+
|
|
276
|
+
observations = (
|
|
277
|
+
ObservationBuilder()
|
|
278
|
+
.for_agent(
|
|
279
|
+
"alice",
|
|
280
|
+
state=("secret",),
|
|
281
|
+
)
|
|
282
|
+
.for_agent(
|
|
283
|
+
"bob",
|
|
284
|
+
state=("x",),
|
|
285
|
+
process=True,
|
|
286
|
+
)
|
|
287
|
+
.public("round")
|
|
288
|
+
.build(("alice", "bob"))
|
|
289
|
+
)
|
|
290
|
+
"""
|
|
291
|
+
|
|
292
|
+
components: tuple[Observation, ...] = ()
|
|
293
|
+
|
|
294
|
+
@classmethod
|
|
295
|
+
def create(
|
|
296
|
+
cls,
|
|
297
|
+
*components: Observation,
|
|
298
|
+
) -> "ObservationBuilder":
|
|
299
|
+
return cls(
|
|
300
|
+
tuple(components)
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
def add(
|
|
304
|
+
self,
|
|
305
|
+
component: Observation,
|
|
306
|
+
) -> "ObservationBuilder":
|
|
307
|
+
return ObservationBuilder(
|
|
308
|
+
self.components + (component,)
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
def for_agent(
|
|
312
|
+
self,
|
|
313
|
+
agent_name: str,
|
|
314
|
+
*,
|
|
315
|
+
state: str | Iterable[str] = (),
|
|
316
|
+
process: bool = False,
|
|
317
|
+
) -> "ObservationBuilder":
|
|
318
|
+
return self.add(
|
|
319
|
+
local(
|
|
320
|
+
agent_name,
|
|
321
|
+
state=state,
|
|
322
|
+
process=process,
|
|
323
|
+
)
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
def global_(
|
|
327
|
+
self,
|
|
328
|
+
*keys: str,
|
|
329
|
+
) -> "ObservationBuilder":
|
|
330
|
+
return self.add(
|
|
331
|
+
global_(*keys)
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
def public(
|
|
335
|
+
self,
|
|
336
|
+
*keys: str,
|
|
337
|
+
) -> "ObservationBuilder":
|
|
338
|
+
return self.add(
|
|
339
|
+
public(*keys)
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
def build(
|
|
343
|
+
self,
|
|
344
|
+
agent_names: Iterable[str],
|
|
345
|
+
) -> dict[str, Observation]:
|
|
346
|
+
names = tuple(agent_names)
|
|
347
|
+
|
|
348
|
+
if not names:
|
|
349
|
+
raise ValueError(
|
|
350
|
+
"ObservationBuilder requires at least one agent"
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
if len(names) != len(set(names)):
|
|
354
|
+
raise ValueError(
|
|
355
|
+
"agent_names must be unique"
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
global_components: list[Observation] = []
|
|
359
|
+
|
|
360
|
+
local_components: dict[
|
|
361
|
+
str,
|
|
362
|
+
list[Observation],
|
|
363
|
+
] = {
|
|
364
|
+
name: []
|
|
365
|
+
for name in names
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
for component in self.components:
|
|
369
|
+
if isinstance(
|
|
370
|
+
component,
|
|
371
|
+
LocalObservation,
|
|
372
|
+
):
|
|
373
|
+
if component.agent_name not in local_components:
|
|
374
|
+
raise ValueError(
|
|
375
|
+
f"Unknown agent "
|
|
376
|
+
f"{component.agent_name!r} "
|
|
377
|
+
f"in observation builder"
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
local_components[
|
|
381
|
+
component.agent_name
|
|
382
|
+
].append(component)
|
|
383
|
+
|
|
384
|
+
else:
|
|
385
|
+
global_components.append(
|
|
386
|
+
component
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
result: dict[str, Observation] = {}
|
|
390
|
+
|
|
391
|
+
for name in names:
|
|
392
|
+
components = tuple(
|
|
393
|
+
global_components
|
|
394
|
+
+ local_components[name]
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
if not components:
|
|
398
|
+
raise ValueError(
|
|
399
|
+
f"No observation defined for agent {name!r}"
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
result[name] = _CombinedObservation(
|
|
403
|
+
components
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
return result
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
# ============================================================================
|
|
410
|
+
# Compatibility helpers
|
|
411
|
+
# ============================================================================
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def same_observation(
|
|
415
|
+
agent: "Agent",
|
|
416
|
+
first: "Configuration",
|
|
417
|
+
second: "Configuration",
|
|
418
|
+
observation: Observation,
|
|
419
|
+
) -> bool:
|
|
420
|
+
return (
|
|
421
|
+
observation(agent, first)
|
|
422
|
+
== observation(agent, second)
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def observe_global(
|
|
427
|
+
*keys: str,
|
|
428
|
+
) -> Observation:
|
|
429
|
+
"""
|
|
430
|
+
Backward-compatible global observation.
|
|
431
|
+
|
|
432
|
+
No keys means an empty public observation.
|
|
433
|
+
"""
|
|
434
|
+
|
|
435
|
+
def observation(
|
|
436
|
+
agent: "Agent",
|
|
437
|
+
configuration: "Configuration",
|
|
438
|
+
) -> Hashable:
|
|
439
|
+
value = tuple(
|
|
440
|
+
(key, configuration.state.get(key))
|
|
441
|
+
for key in keys
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
return _ensure_hashable(value)
|
|
445
|
+
|
|
446
|
+
return observation
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def observe_local_state(
|
|
450
|
+
*keys: str,
|
|
451
|
+
) -> Observation:
|
|
452
|
+
"""
|
|
453
|
+
Backward-compatible local-state observation.
|
|
454
|
+
|
|
455
|
+
With explicit keys:
|
|
456
|
+
|
|
457
|
+
observe_local_state("x", "y")
|
|
458
|
+
|
|
459
|
+
observes exactly those variables.
|
|
460
|
+
|
|
461
|
+
Without keys:
|
|
462
|
+
|
|
463
|
+
observe_local_state()
|
|
464
|
+
|
|
465
|
+
observes the complete local agent state.
|
|
466
|
+
"""
|
|
467
|
+
|
|
468
|
+
def observation(
|
|
469
|
+
agent: "Agent",
|
|
470
|
+
configuration: "Configuration",
|
|
471
|
+
) -> Hashable:
|
|
472
|
+
if keys:
|
|
473
|
+
value = tuple(
|
|
474
|
+
(key, agent.get_state(key))
|
|
475
|
+
for key in keys
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
else:
|
|
479
|
+
# Agent.state is already a canonical immutable tuple.
|
|
480
|
+
value = tuple(agent.state)
|
|
481
|
+
|
|
482
|
+
return _ensure_hashable(value)
|
|
483
|
+
|
|
484
|
+
return observation
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def observe_local_process() -> Observation:
|
|
488
|
+
"""
|
|
489
|
+
Backward-compatible process observation.
|
|
490
|
+
"""
|
|
491
|
+
|
|
492
|
+
def observation(
|
|
493
|
+
agent: "Agent",
|
|
494
|
+
configuration: "Configuration",
|
|
495
|
+
) -> Hashable:
|
|
496
|
+
return _ensure_hashable(
|
|
497
|
+
agent.process
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
return observation
|