gds-framework 0.2.0__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.
- gds/__init__.py +175 -0
- gds/blocks/__init__.py +29 -0
- gds/blocks/base.py +91 -0
- gds/blocks/composition.py +141 -0
- gds/blocks/errors.py +17 -0
- gds/blocks/roles.py +129 -0
- gds/canonical.py +136 -0
- gds/compiler/__init__.py +5 -0
- gds/compiler/compile.py +274 -0
- gds/helpers.py +151 -0
- gds/ir/__init__.py +19 -0
- gds/ir/models.py +105 -0
- gds/ir/serialization.py +34 -0
- gds/parameters.py +88 -0
- gds/py.typed +0 -0
- gds/query.py +121 -0
- gds/serialize.py +107 -0
- gds/spaces.py +61 -0
- gds/spec.py +230 -0
- gds/state.py +63 -0
- gds/tagged.py +49 -0
- gds/types/__init__.py +29 -0
- gds/types/interface.py +42 -0
- gds/types/tokens.py +48 -0
- gds/types/typedef.py +73 -0
- gds/verification/__init__.py +21 -0
- gds/verification/engine.py +40 -0
- gds/verification/findings.py +51 -0
- gds/verification/generic_checks.py +259 -0
- gds/verification/spec_checks.py +276 -0
- gds/visualization.py +10 -0
- gds_framework-0.2.0.dist-info/METADATA +377 -0
- gds_framework-0.2.0.dist-info/RECORD +35 -0
- gds_framework-0.2.0.dist-info/WHEEL +4 -0
- gds_framework-0.2.0.dist-info/licenses/LICENSE +190 -0
gds/__init__.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Generalized Dynamical Systems — typed compositional specifications for complex systems.
|
|
2
|
+
|
|
3
|
+
GDS synthesizes ideas from GDS theory (Roxin, Zargham & Shorish), MSML (BlockScience),
|
|
4
|
+
BDP-lib (Block Diagram Protocol), and categorical cybernetics (Ghani, Hedges et al.)
|
|
5
|
+
into a single, dependency-light Python framework.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
|
|
10
|
+
# ── Composition algebra ─────────────────────────────────────
|
|
11
|
+
from gds.blocks.base import AtomicBlock, Block
|
|
12
|
+
from gds.blocks.composition import (
|
|
13
|
+
FeedbackLoop,
|
|
14
|
+
ParallelComposition,
|
|
15
|
+
StackComposition,
|
|
16
|
+
TemporalLoop,
|
|
17
|
+
Wiring,
|
|
18
|
+
)
|
|
19
|
+
from gds.blocks.errors import GDSCompositionError, GDSError, GDSTypeError
|
|
20
|
+
|
|
21
|
+
# ── GDS block roles ─────────────────────────────────────────
|
|
22
|
+
from gds.blocks.roles import (
|
|
23
|
+
BoundaryAction,
|
|
24
|
+
ControlAction,
|
|
25
|
+
HasConstraints,
|
|
26
|
+
HasOptions,
|
|
27
|
+
HasParams,
|
|
28
|
+
Mechanism,
|
|
29
|
+
Policy,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# ── Canonical projection ───────────────────────────────────
|
|
33
|
+
from gds.canonical import CanonicalGDS, project_canonical
|
|
34
|
+
from gds.compiler.compile import compile_system
|
|
35
|
+
|
|
36
|
+
# ── Convenience helpers ────────────────────────────────────
|
|
37
|
+
from gds.helpers import (
|
|
38
|
+
all_checks,
|
|
39
|
+
entity,
|
|
40
|
+
gds_check,
|
|
41
|
+
get_custom_checks,
|
|
42
|
+
interface,
|
|
43
|
+
space,
|
|
44
|
+
state_var,
|
|
45
|
+
typedef,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# ── IR and serialization ────────────────────────────────────
|
|
49
|
+
from gds.ir.models import (
|
|
50
|
+
BlockIR,
|
|
51
|
+
CompositionType,
|
|
52
|
+
FlowDirection,
|
|
53
|
+
HierarchyNodeIR,
|
|
54
|
+
SystemIR,
|
|
55
|
+
WiringIR,
|
|
56
|
+
)
|
|
57
|
+
from gds.ir.serialization import IRDocument, IRMetadata, load_ir, save_ir
|
|
58
|
+
|
|
59
|
+
# ── Parameters ─────────────────────────────────────────────
|
|
60
|
+
from gds.parameters import ParameterDef, ParameterSchema
|
|
61
|
+
|
|
62
|
+
# ── Query engine ────────────────────────────────────────────
|
|
63
|
+
from gds.query import SpecQuery
|
|
64
|
+
from gds.serialize import spec_to_dict, spec_to_json
|
|
65
|
+
|
|
66
|
+
# ── Typed spaces and state ──────────────────────────────────
|
|
67
|
+
from gds.spaces import EMPTY, TERMINAL, Space
|
|
68
|
+
|
|
69
|
+
# ── Specification registry ──────────────────────────────────
|
|
70
|
+
from gds.spec import GDSSpec, SpecWiring, Wire
|
|
71
|
+
from gds.state import Entity, StateVariable
|
|
72
|
+
from gds.tagged import Tagged
|
|
73
|
+
from gds.types.interface import Interface, Port, port
|
|
74
|
+
from gds.types.tokens import tokenize, tokens_overlap, tokens_subset
|
|
75
|
+
|
|
76
|
+
# ── Type system ─────────────────────────────────────────────
|
|
77
|
+
from gds.types.typedef import (
|
|
78
|
+
AgentID,
|
|
79
|
+
NonNegativeFloat,
|
|
80
|
+
PositiveInt,
|
|
81
|
+
Probability,
|
|
82
|
+
Timestamp,
|
|
83
|
+
TokenAmount,
|
|
84
|
+
TypeDef,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# ── Verification ────────────────────────────────────────────
|
|
88
|
+
from gds.verification.engine import verify
|
|
89
|
+
from gds.verification.findings import Finding, Severity, VerificationReport
|
|
90
|
+
from gds.verification.spec_checks import (
|
|
91
|
+
check_canonical_wellformedness,
|
|
92
|
+
check_completeness,
|
|
93
|
+
check_determinism,
|
|
94
|
+
check_parameter_references,
|
|
95
|
+
check_reachability,
|
|
96
|
+
check_type_safety,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
__all__ = [
|
|
100
|
+
"EMPTY",
|
|
101
|
+
"TERMINAL",
|
|
102
|
+
"AgentID",
|
|
103
|
+
"AtomicBlock",
|
|
104
|
+
"Block",
|
|
105
|
+
"BlockIR",
|
|
106
|
+
"BoundaryAction",
|
|
107
|
+
"CanonicalGDS",
|
|
108
|
+
"CompositionType",
|
|
109
|
+
"ControlAction",
|
|
110
|
+
"Entity",
|
|
111
|
+
"FeedbackLoop",
|
|
112
|
+
"Finding",
|
|
113
|
+
"FlowDirection",
|
|
114
|
+
"GDSCompositionError",
|
|
115
|
+
"GDSError",
|
|
116
|
+
"GDSSpec",
|
|
117
|
+
"GDSTypeError",
|
|
118
|
+
"HasConstraints",
|
|
119
|
+
"HasOptions",
|
|
120
|
+
"HasParams",
|
|
121
|
+
"HierarchyNodeIR",
|
|
122
|
+
"IRDocument",
|
|
123
|
+
"IRMetadata",
|
|
124
|
+
"Interface",
|
|
125
|
+
"Mechanism",
|
|
126
|
+
"NonNegativeFloat",
|
|
127
|
+
"ParallelComposition",
|
|
128
|
+
"ParameterDef",
|
|
129
|
+
"ParameterSchema",
|
|
130
|
+
"Policy",
|
|
131
|
+
"Port",
|
|
132
|
+
"PositiveInt",
|
|
133
|
+
"Probability",
|
|
134
|
+
"Severity",
|
|
135
|
+
"Space",
|
|
136
|
+
"SpecQuery",
|
|
137
|
+
"SpecWiring",
|
|
138
|
+
"StackComposition",
|
|
139
|
+
"StateVariable",
|
|
140
|
+
"SystemIR",
|
|
141
|
+
"Tagged",
|
|
142
|
+
"TemporalLoop",
|
|
143
|
+
"Timestamp",
|
|
144
|
+
"TokenAmount",
|
|
145
|
+
"TypeDef",
|
|
146
|
+
"VerificationReport",
|
|
147
|
+
"Wire",
|
|
148
|
+
"Wiring",
|
|
149
|
+
"WiringIR",
|
|
150
|
+
"all_checks",
|
|
151
|
+
"check_canonical_wellformedness",
|
|
152
|
+
"check_completeness",
|
|
153
|
+
"check_determinism",
|
|
154
|
+
"check_parameter_references",
|
|
155
|
+
"check_reachability",
|
|
156
|
+
"check_type_safety",
|
|
157
|
+
"compile_system",
|
|
158
|
+
"entity",
|
|
159
|
+
"gds_check",
|
|
160
|
+
"get_custom_checks",
|
|
161
|
+
"interface",
|
|
162
|
+
"load_ir",
|
|
163
|
+
"port",
|
|
164
|
+
"project_canonical",
|
|
165
|
+
"save_ir",
|
|
166
|
+
"space",
|
|
167
|
+
"spec_to_dict",
|
|
168
|
+
"spec_to_json",
|
|
169
|
+
"state_var",
|
|
170
|
+
"tokenize",
|
|
171
|
+
"tokens_overlap",
|
|
172
|
+
"tokens_subset",
|
|
173
|
+
"typedef",
|
|
174
|
+
"verify",
|
|
175
|
+
]
|
gds/blocks/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Block primitives, composition operators, and GDS roles."""
|
|
2
|
+
|
|
3
|
+
from gds.blocks.base import AtomicBlock, Block
|
|
4
|
+
from gds.blocks.composition import (
|
|
5
|
+
FeedbackLoop,
|
|
6
|
+
ParallelComposition,
|
|
7
|
+
StackComposition,
|
|
8
|
+
TemporalLoop,
|
|
9
|
+
Wiring,
|
|
10
|
+
)
|
|
11
|
+
from gds.blocks.errors import GDSCompositionError, GDSError, GDSTypeError
|
|
12
|
+
from gds.blocks.roles import BoundaryAction, ControlAction, Mechanism, Policy
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"AtomicBlock",
|
|
16
|
+
"Block",
|
|
17
|
+
"BoundaryAction",
|
|
18
|
+
"ControlAction",
|
|
19
|
+
"FeedbackLoop",
|
|
20
|
+
"GDSCompositionError",
|
|
21
|
+
"GDSError",
|
|
22
|
+
"GDSTypeError",
|
|
23
|
+
"Mechanism",
|
|
24
|
+
"ParallelComposition",
|
|
25
|
+
"Policy",
|
|
26
|
+
"StackComposition",
|
|
27
|
+
"TemporalLoop",
|
|
28
|
+
"Wiring",
|
|
29
|
+
]
|
gds/blocks/base.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Abstract base class for Blocks in the GDS framework.
|
|
2
|
+
|
|
3
|
+
A Block is the fundamental composable unit. It has a name and an Interface
|
|
4
|
+
(directional port pairs). Composition operators build composite blocks
|
|
5
|
+
from simpler ones, forming a tree that the compiler flattens into a list
|
|
6
|
+
of atomic blocks + wiring.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from abc import ABC, abstractmethod
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
from gds.tagged import Tagged
|
|
15
|
+
from gds.types.interface import Interface
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from gds.blocks.composition import (
|
|
19
|
+
FeedbackLoop,
|
|
20
|
+
ParallelComposition,
|
|
21
|
+
StackComposition,
|
|
22
|
+
TemporalLoop,
|
|
23
|
+
Wiring,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Block(Tagged, ABC):
|
|
28
|
+
"""Abstract base for all Blocks — both atomic and composite.
|
|
29
|
+
|
|
30
|
+
Every block has a ``name`` and an ``interface`` describing its boundary
|
|
31
|
+
ports. Composition operators (``>>``, ``|``, ``.feedback()``, ``.loop()``)
|
|
32
|
+
build composite blocks from simpler ones.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
name: str
|
|
36
|
+
interface: Interface = Interface()
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def flatten(self) -> list[AtomicBlock]:
|
|
40
|
+
"""Return all atomic blocks in evaluation order."""
|
|
41
|
+
|
|
42
|
+
def __rshift__(self, other: Block) -> StackComposition:
|
|
43
|
+
"""``a >> b`` — stack (sequential) composition."""
|
|
44
|
+
from gds.blocks.composition import StackComposition
|
|
45
|
+
|
|
46
|
+
return StackComposition(
|
|
47
|
+
name=f"{self.name} >> {other.name}",
|
|
48
|
+
first=self,
|
|
49
|
+
second=other,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def __or__(self, other: Block) -> ParallelComposition:
|
|
53
|
+
"""``a | b`` — parallel composition."""
|
|
54
|
+
from gds.blocks.composition import ParallelComposition
|
|
55
|
+
|
|
56
|
+
return ParallelComposition(
|
|
57
|
+
name=f"{self.name} | {other.name}",
|
|
58
|
+
left=self,
|
|
59
|
+
right=other,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def feedback(self, wiring: list[Wiring]) -> FeedbackLoop:
|
|
63
|
+
"""Wrap with backward feedback within a single timestep."""
|
|
64
|
+
from gds.blocks.composition import FeedbackLoop
|
|
65
|
+
|
|
66
|
+
return FeedbackLoop(
|
|
67
|
+
name=f"{self.name} [feedback]",
|
|
68
|
+
inner=self,
|
|
69
|
+
feedback_wiring=wiring,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def loop(self, wiring: list[Wiring], exit_condition: str = "") -> TemporalLoop:
|
|
73
|
+
"""Wrap with forward temporal iteration across timesteps."""
|
|
74
|
+
from gds.blocks.composition import TemporalLoop
|
|
75
|
+
|
|
76
|
+
return TemporalLoop(
|
|
77
|
+
name=f"{self.name} [loop]",
|
|
78
|
+
inner=self,
|
|
79
|
+
temporal_wiring=wiring,
|
|
80
|
+
exit_condition=exit_condition,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class AtomicBlock(Block):
|
|
85
|
+
"""Base class for non-decomposable (leaf) blocks.
|
|
86
|
+
|
|
87
|
+
Domain packages subclass this to define their own atomic block types.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def flatten(self) -> list[AtomicBlock]:
|
|
91
|
+
return [self]
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Composition operators: stack, parallel, feedback, and temporal loop.
|
|
2
|
+
|
|
3
|
+
These operators combine blocks into larger composite systems:
|
|
4
|
+
|
|
5
|
+
- **Stack** (``>>``) — chains blocks so one's output feeds another's input.
|
|
6
|
+
- **Parallel** (``|``) — runs blocks side-by-side with no shared wires.
|
|
7
|
+
- **Feedback** — connects backward_out to backward_in within a single timestep.
|
|
8
|
+
- **Temporal Loop** — connects forward_out to forward_in across timesteps.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Self
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel, Field, model_validator
|
|
16
|
+
|
|
17
|
+
from gds.blocks.base import AtomicBlock, Block
|
|
18
|
+
from gds.blocks.errors import GDSTypeError
|
|
19
|
+
from gds.ir.models import FlowDirection
|
|
20
|
+
from gds.types.interface import Interface, Port
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Wiring(BaseModel, frozen=True):
|
|
24
|
+
"""An explicit connection between two blocks.
|
|
25
|
+
|
|
26
|
+
Covariant wirings (the default) carry data forward; contravariant
|
|
27
|
+
wirings carry feedback backward.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
source_block: str
|
|
31
|
+
source_port: str
|
|
32
|
+
target_block: str
|
|
33
|
+
target_port: str
|
|
34
|
+
direction: FlowDirection = FlowDirection.COVARIANT
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class StackComposition(Block):
|
|
38
|
+
"""``a >> b`` — stack (sequential) composition.
|
|
39
|
+
|
|
40
|
+
Output of the first block feeds input of the second. If no explicit
|
|
41
|
+
``wiring`` is provided, the validator checks that forward_out tokens
|
|
42
|
+
overlap with forward_in tokens.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
first: Block
|
|
46
|
+
second: Block
|
|
47
|
+
wiring: list[Wiring] = Field(default_factory=list)
|
|
48
|
+
|
|
49
|
+
@model_validator(mode="after")
|
|
50
|
+
def _compute_interface_and_validate(self) -> Self:
|
|
51
|
+
if not self.wiring:
|
|
52
|
+
first_out_tokens = _collect_tokens(self.first.interface.forward_out)
|
|
53
|
+
second_in_tokens = _collect_tokens(self.second.interface.forward_in)
|
|
54
|
+
|
|
55
|
+
if first_out_tokens and second_in_tokens and not (first_out_tokens & second_in_tokens):
|
|
56
|
+
raise GDSTypeError(
|
|
57
|
+
f"Stack composition {self.name!r}: "
|
|
58
|
+
f"first.forward_out tokens {first_out_tokens} have no overlap with "
|
|
59
|
+
f"second.forward_in tokens {second_in_tokens}"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
self.interface = Interface(
|
|
63
|
+
forward_in=self.first.interface.forward_in + self.second.interface.forward_in,
|
|
64
|
+
forward_out=self.first.interface.forward_out + self.second.interface.forward_out,
|
|
65
|
+
backward_in=self.first.interface.backward_in + self.second.interface.backward_in,
|
|
66
|
+
backward_out=self.first.interface.backward_out + self.second.interface.backward_out,
|
|
67
|
+
)
|
|
68
|
+
return self
|
|
69
|
+
|
|
70
|
+
def flatten(self) -> list[AtomicBlock]:
|
|
71
|
+
return self.first.flatten() + self.second.flatten()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ParallelComposition(Block):
|
|
75
|
+
"""``a | b`` — parallel composition: blocks run independently."""
|
|
76
|
+
|
|
77
|
+
left: Block
|
|
78
|
+
right: Block
|
|
79
|
+
|
|
80
|
+
@model_validator(mode="after")
|
|
81
|
+
def _compute_interface(self) -> Self:
|
|
82
|
+
self.interface = Interface(
|
|
83
|
+
forward_in=self.left.interface.forward_in + self.right.interface.forward_in,
|
|
84
|
+
forward_out=self.left.interface.forward_out + self.right.interface.forward_out,
|
|
85
|
+
backward_in=self.left.interface.backward_in + self.right.interface.backward_in,
|
|
86
|
+
backward_out=self.left.interface.backward_out + self.right.interface.backward_out,
|
|
87
|
+
)
|
|
88
|
+
return self
|
|
89
|
+
|
|
90
|
+
def flatten(self) -> list[AtomicBlock]:
|
|
91
|
+
return self.left.flatten() + self.right.flatten()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class FeedbackLoop(Block):
|
|
95
|
+
"""Backward feedback within a single timestep (backward_out -> backward_in)."""
|
|
96
|
+
|
|
97
|
+
inner: Block
|
|
98
|
+
feedback_wiring: list[Wiring]
|
|
99
|
+
|
|
100
|
+
@model_validator(mode="after")
|
|
101
|
+
def _validate_and_compute_interface(self) -> Self:
|
|
102
|
+
self.interface = self.inner.interface
|
|
103
|
+
return self
|
|
104
|
+
|
|
105
|
+
def flatten(self) -> list[AtomicBlock]:
|
|
106
|
+
return self.inner.flatten()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class TemporalLoop(Block):
|
|
110
|
+
"""Forward temporal iteration across timesteps (forward_out -> forward_in).
|
|
111
|
+
|
|
112
|
+
All temporal wiring must be covariant direction.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
inner: Block
|
|
116
|
+
temporal_wiring: list[Wiring]
|
|
117
|
+
exit_condition: str = ""
|
|
118
|
+
|
|
119
|
+
@model_validator(mode="after")
|
|
120
|
+
def _validate_and_compute_interface(self) -> Self:
|
|
121
|
+
for w in self.temporal_wiring:
|
|
122
|
+
if w.direction != FlowDirection.COVARIANT:
|
|
123
|
+
raise GDSTypeError(
|
|
124
|
+
f"TemporalLoop {self.name!r}: temporal wiring "
|
|
125
|
+
f"{w.source_block}.{w.source_port} → {w.target_block}.{w.target_port} "
|
|
126
|
+
f"must be COVARIANT (got {w.direction.value})"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
self.interface = self.inner.interface
|
|
130
|
+
return self
|
|
131
|
+
|
|
132
|
+
def flatten(self) -> list[AtomicBlock]:
|
|
133
|
+
return self.inner.flatten()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _collect_tokens(ports: tuple[Port, ...]) -> frozenset[str]:
|
|
137
|
+
"""Collect all type tokens from a tuple of ports."""
|
|
138
|
+
tokens: set[str] = set()
|
|
139
|
+
for p in ports:
|
|
140
|
+
tokens.update(p.type_tokens)
|
|
141
|
+
return frozenset(tokens)
|
gds/blocks/errors.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""GDS-specific error types.
|
|
2
|
+
|
|
3
|
+
Raised at construction time during Pydantic validation when a block
|
|
4
|
+
or composition violates structural constraints.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class GDSError(Exception):
|
|
9
|
+
"""Base class for all GDS errors."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GDSTypeError(GDSError):
|
|
13
|
+
"""Port type mismatch or invalid port structure during construction."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GDSCompositionError(GDSError):
|
|
17
|
+
"""Invalid composition structure."""
|
gds/blocks/roles.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""GDS block roles from the MSML decomposition.
|
|
2
|
+
|
|
3
|
+
These roles decompose the transition function h into typed components:
|
|
4
|
+
|
|
5
|
+
- **BoundaryAction** — exogenous input (GDS admissible inputs U)
|
|
6
|
+
- **ControlAction** — endogenous control (reads state, emits signals)
|
|
7
|
+
- **Policy** — decision logic (maps signals to mechanism inputs)
|
|
8
|
+
- **Mechanism** — state update (the only component that writes state)
|
|
9
|
+
|
|
10
|
+
Each role subclasses AtomicBlock, inheriting composition operators and
|
|
11
|
+
flatten(). Role-specific validators enforce structural constraints.
|
|
12
|
+
|
|
13
|
+
Protocols
|
|
14
|
+
---------
|
|
15
|
+
``HasParams``, ``HasConstraints``, and ``HasOptions`` describe the structural
|
|
16
|
+
attributes shared by multiple role types without requiring a common base class.
|
|
17
|
+
Use ``isinstance(block, HasParams)`` for safe narrowing instead of
|
|
18
|
+
``cast(Any, block)`` or ``hasattr`` guards.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from typing import Protocol, Self, runtime_checkable
|
|
24
|
+
|
|
25
|
+
from pydantic import Field, model_validator
|
|
26
|
+
|
|
27
|
+
from gds.blocks.base import AtomicBlock
|
|
28
|
+
from gds.blocks.errors import GDSCompositionError
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@runtime_checkable
|
|
32
|
+
class HasParams(Protocol):
|
|
33
|
+
"""Structural protocol for blocks that declare parameter dependencies."""
|
|
34
|
+
|
|
35
|
+
params_used: list[str]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@runtime_checkable
|
|
39
|
+
class HasConstraints(Protocol):
|
|
40
|
+
"""Structural protocol for blocks that carry constraint annotations."""
|
|
41
|
+
|
|
42
|
+
constraints: list[str]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@runtime_checkable
|
|
46
|
+
class HasOptions(Protocol):
|
|
47
|
+
"""Structural protocol for blocks that enumerate named strategy options."""
|
|
48
|
+
|
|
49
|
+
options: list[str]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class BoundaryAction(AtomicBlock):
|
|
53
|
+
"""Exogenous input — enters the system from outside.
|
|
54
|
+
|
|
55
|
+
In GDS terms: part of the admissible input set U.
|
|
56
|
+
Boundary actions model external agents, oracles, user inputs,
|
|
57
|
+
environmental signals — anything the system doesn't control.
|
|
58
|
+
|
|
59
|
+
Enforces ``forward_in = ()`` since boundary actions receive no
|
|
60
|
+
internal forward signals.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
kind: str = "boundary"
|
|
64
|
+
options: list[str] = Field(default_factory=list)
|
|
65
|
+
params_used: list[str] = Field(default_factory=list)
|
|
66
|
+
constraints: list[str] = Field(default_factory=list)
|
|
67
|
+
|
|
68
|
+
@model_validator(mode="after")
|
|
69
|
+
def _enforce_no_forward_in(self) -> Self:
|
|
70
|
+
if self.interface.forward_in:
|
|
71
|
+
raise GDSCompositionError(
|
|
72
|
+
f"BoundaryAction {self.name!r}: forward_in must be empty "
|
|
73
|
+
f"(boundary actions receive no internal forward signals)"
|
|
74
|
+
)
|
|
75
|
+
return self
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class ControlAction(AtomicBlock):
|
|
79
|
+
"""Endogenous control — reads state, emits control signals.
|
|
80
|
+
|
|
81
|
+
These are internal feedback loops: the system observing itself
|
|
82
|
+
and generating signals that influence downstream policy/mechanism blocks.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
kind: str = "control"
|
|
86
|
+
options: list[str] = Field(default_factory=list)
|
|
87
|
+
params_used: list[str] = Field(default_factory=list)
|
|
88
|
+
constraints: list[str] = Field(default_factory=list)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class Policy(AtomicBlock):
|
|
92
|
+
"""Decision logic — maps signals to mechanism inputs.
|
|
93
|
+
|
|
94
|
+
Policies select from feasible actions. Named options support
|
|
95
|
+
scenario analysis and A/B testing.
|
|
96
|
+
|
|
97
|
+
In GDS terms: policies implement the decision mapping
|
|
98
|
+
within the admissibility constraint.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
kind: str = "policy"
|
|
102
|
+
options: list[str] = Field(default_factory=list)
|
|
103
|
+
params_used: list[str] = Field(default_factory=list)
|
|
104
|
+
constraints: list[str] = Field(default_factory=list)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class Mechanism(AtomicBlock):
|
|
108
|
+
"""State update — the only block type that writes to state.
|
|
109
|
+
|
|
110
|
+
Mechanisms are the atomic state transitions that compose into h.
|
|
111
|
+
They have no backward ports (state writes don't propagate signals).
|
|
112
|
+
|
|
113
|
+
``updates`` lists (entity_name, variable_name) pairs specifying
|
|
114
|
+
which state variables this mechanism modifies.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
kind: str = "mechanism"
|
|
118
|
+
updates: list[tuple[str, str]] = Field(default_factory=list)
|
|
119
|
+
params_used: list[str] = Field(default_factory=list)
|
|
120
|
+
constraints: list[str] = Field(default_factory=list)
|
|
121
|
+
|
|
122
|
+
@model_validator(mode="after")
|
|
123
|
+
def _enforce_no_backward(self) -> Self:
|
|
124
|
+
if self.interface.backward_in or self.interface.backward_out:
|
|
125
|
+
raise GDSCompositionError(
|
|
126
|
+
f"Mechanism {self.name!r}: backward ports must be empty "
|
|
127
|
+
f"(mechanisms write state, they don't pass backward signals)"
|
|
128
|
+
)
|
|
129
|
+
return self
|