gds-framework 0.2.2__tar.gz → 0.2.3__tar.gz
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_framework-0.2.2 → gds_framework-0.2.3}/PKG-INFO +1 -1
- {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/__init__.py +18 -2
- gds_framework-0.2.3/gds/compiler/__init__.py +19 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/compiler/compile.py +184 -62
- {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/ir/models.py +30 -2
- {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/verification/generic_checks.py +90 -19
- {gds_framework-0.2.2 → gds_framework-0.2.3}/pyproject.toml +4 -1
- gds_framework-0.2.3/tests/test_compiler.py +456 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/tests/test_verification.py +105 -6
- gds_framework-0.2.2/gds/compiler/__init__.py +0 -5
- gds_framework-0.2.2/tests/test_compiler.py +0 -223
- {gds_framework-0.2.2 → gds_framework-0.2.3}/.gitignore +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/CITATION.cff +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/CLAUDE.md +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/LICENSE +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/README.md +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/blocks/__init__.py +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/blocks/base.py +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/blocks/composition.py +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/blocks/errors.py +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/blocks/roles.py +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/canonical.py +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/helpers.py +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/ir/__init__.py +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/ir/serialization.py +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/parameters.py +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/py.typed +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/query.py +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/serialize.py +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/spaces.py +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/spec.py +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/state.py +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/tagged.py +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/types/__init__.py +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/types/interface.py +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/types/tokens.py +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/types/typedef.py +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/verification/__init__.py +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/verification/engine.py +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/verification/findings.py +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/verification/spec_checks.py +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/visualization.py +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/tests/__init__.py +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/tests/conftest.py +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/tests/test_blocks.py +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/tests/test_helpers.py +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/tests/test_ir.py +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/tests/test_query.py +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/tests/test_serialize.py +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/tests/test_spaces.py +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/tests/test_spec.py +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/tests/test_spec_checks.py +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/tests/test_state.py +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/tests/test_types.py +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/tests/test_v02_features.py +0 -0
- {gds_framework-0.2.2 → gds_framework-0.2.3}/tests/test_visualization.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gds-framework
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Generalized Dynamical Systems — typed compositional specifications for complex systems
|
|
5
5
|
Project-URL: Homepage, https://github.com/BlockScience/gds-core
|
|
6
6
|
Project-URL: Repository, https://github.com/BlockScience/gds-core
|
|
@@ -6,7 +6,7 @@ cybernetics (Ghani, Hedges et al.) into a single, dependency-light
|
|
|
6
6
|
Python framework.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
__version__ = "0.2.
|
|
9
|
+
__version__ = "0.2.3"
|
|
10
10
|
|
|
11
11
|
# ── Composition algebra ─────────────────────────────────────
|
|
12
12
|
from gds.blocks.base import AtomicBlock, Block
|
|
@@ -32,7 +32,14 @@ from gds.blocks.roles import (
|
|
|
32
32
|
|
|
33
33
|
# ── Canonical projection ───────────────────────────────────
|
|
34
34
|
from gds.canonical import CanonicalGDS, project_canonical
|
|
35
|
-
from gds.compiler.compile import
|
|
35
|
+
from gds.compiler.compile import (
|
|
36
|
+
StructuralWiring,
|
|
37
|
+
WiringOrigin,
|
|
38
|
+
compile_system,
|
|
39
|
+
extract_hierarchy,
|
|
40
|
+
extract_wirings,
|
|
41
|
+
flatten_blocks,
|
|
42
|
+
)
|
|
36
43
|
|
|
37
44
|
# ── Convenience helpers ────────────────────────────────────
|
|
38
45
|
from gds.helpers import (
|
|
@@ -52,8 +59,10 @@ from gds.ir.models import (
|
|
|
52
59
|
CompositionType,
|
|
53
60
|
FlowDirection,
|
|
54
61
|
HierarchyNodeIR,
|
|
62
|
+
InputIR,
|
|
55
63
|
SystemIR,
|
|
56
64
|
WiringIR,
|
|
65
|
+
sanitize_id,
|
|
57
66
|
)
|
|
58
67
|
from gds.ir.serialization import IRDocument, IRMetadata, load_ir, save_ir
|
|
59
68
|
|
|
@@ -122,6 +131,7 @@ __all__ = [
|
|
|
122
131
|
"HierarchyNodeIR",
|
|
123
132
|
"IRDocument",
|
|
124
133
|
"IRMetadata",
|
|
134
|
+
"InputIR",
|
|
125
135
|
"Interface",
|
|
126
136
|
"Mechanism",
|
|
127
137
|
"NonNegativeFloat",
|
|
@@ -138,6 +148,7 @@ __all__ = [
|
|
|
138
148
|
"SpecWiring",
|
|
139
149
|
"StackComposition",
|
|
140
150
|
"StateVariable",
|
|
151
|
+
"StructuralWiring",
|
|
141
152
|
"SystemIR",
|
|
142
153
|
"Tagged",
|
|
143
154
|
"TemporalLoop",
|
|
@@ -148,6 +159,7 @@ __all__ = [
|
|
|
148
159
|
"Wire",
|
|
149
160
|
"Wiring",
|
|
150
161
|
"WiringIR",
|
|
162
|
+
"WiringOrigin",
|
|
151
163
|
"all_checks",
|
|
152
164
|
"check_canonical_wellformedness",
|
|
153
165
|
"check_completeness",
|
|
@@ -157,12 +169,16 @@ __all__ = [
|
|
|
157
169
|
"check_type_safety",
|
|
158
170
|
"compile_system",
|
|
159
171
|
"entity",
|
|
172
|
+
"extract_hierarchy",
|
|
173
|
+
"extract_wirings",
|
|
174
|
+
"flatten_blocks",
|
|
160
175
|
"gds_check",
|
|
161
176
|
"get_custom_checks",
|
|
162
177
|
"interface",
|
|
163
178
|
"load_ir",
|
|
164
179
|
"port",
|
|
165
180
|
"project_canonical",
|
|
181
|
+
"sanitize_id",
|
|
166
182
|
"save_ir",
|
|
167
183
|
"space",
|
|
168
184
|
"spec_to_dict",
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Generic compilation pipeline: Block tree → flat SystemIR."""
|
|
2
|
+
|
|
3
|
+
from gds.compiler.compile import (
|
|
4
|
+
StructuralWiring,
|
|
5
|
+
WiringOrigin,
|
|
6
|
+
compile_system,
|
|
7
|
+
extract_hierarchy,
|
|
8
|
+
extract_wirings,
|
|
9
|
+
flatten_blocks,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"StructuralWiring",
|
|
14
|
+
"WiringOrigin",
|
|
15
|
+
"compile_system",
|
|
16
|
+
"extract_hierarchy",
|
|
17
|
+
"extract_wirings",
|
|
18
|
+
"flatten_blocks",
|
|
19
|
+
]
|
|
@@ -6,13 +6,19 @@ The compiler performs three transformations:
|
|
|
6
6
|
2. **Wire** — extracts explicit wirings and auto-wires stack compositions.
|
|
7
7
|
3. **Hierarchy** — captures the composition tree structure for visualization.
|
|
8
8
|
|
|
9
|
+
Each stage is exposed as a standalone generic function (``flatten_blocks``,
|
|
10
|
+
``extract_wirings``, ``extract_hierarchy``) so domain packages can reuse the
|
|
11
|
+
DFS traversal with custom callbacks instead of forking the compiler.
|
|
12
|
+
|
|
9
13
|
Domain packages provide a ``block_compiler`` callback to convert their
|
|
10
|
-
specific atomic block types into BlockIR
|
|
14
|
+
specific atomic block types into BlockIR, and optionally a
|
|
15
|
+
``wiring_emitter`` callback to transform structural wirings into domain IR.
|
|
11
16
|
"""
|
|
12
17
|
|
|
13
18
|
from __future__ import annotations
|
|
14
19
|
|
|
15
|
-
import
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from enum import StrEnum
|
|
16
22
|
from typing import TYPE_CHECKING
|
|
17
23
|
|
|
18
24
|
from gds.blocks.base import AtomicBlock, Block
|
|
@@ -28,8 +34,10 @@ from gds.ir.models import (
|
|
|
28
34
|
CompositionType,
|
|
29
35
|
FlowDirection,
|
|
30
36
|
HierarchyNodeIR,
|
|
37
|
+
InputIR,
|
|
31
38
|
SystemIR,
|
|
32
39
|
WiringIR,
|
|
40
|
+
sanitize_id,
|
|
33
41
|
)
|
|
34
42
|
|
|
35
43
|
if TYPE_CHECKING:
|
|
@@ -38,12 +46,126 @@ if TYPE_CHECKING:
|
|
|
38
46
|
from gds.types.interface import Port
|
|
39
47
|
|
|
40
48
|
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
# Structural intermediates
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class WiringOrigin(StrEnum):
|
|
55
|
+
"""How a structural wiring was discovered during DFS traversal."""
|
|
56
|
+
|
|
57
|
+
AUTO = "auto"
|
|
58
|
+
EXPLICIT = "explicit"
|
|
59
|
+
FEEDBACK = "feedback"
|
|
60
|
+
TEMPORAL = "temporal"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class StructuralWiring:
|
|
65
|
+
"""Protocol-internal intermediate between DFS traversal and IR emission.
|
|
66
|
+
|
|
67
|
+
The DFS walk produces these; the wiring emitter callback transforms them
|
|
68
|
+
into domain-specific IR (e.g. ``WiringIR`` for GDS, flow edges for OGS).
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
source_block: str
|
|
72
|
+
source_port: str
|
|
73
|
+
target_block: str
|
|
74
|
+
target_port: str
|
|
75
|
+
direction: FlowDirection
|
|
76
|
+
origin: WiringOrigin
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
# Stage 1: flatten_blocks
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def flatten_blocks[B](
|
|
85
|
+
root: Block,
|
|
86
|
+
block_compiler: Callable[[AtomicBlock], B],
|
|
87
|
+
) -> list[B]:
|
|
88
|
+
"""Flatten the composition tree and map each leaf through *block_compiler*.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
root: Root of the composition tree.
|
|
92
|
+
block_compiler: Callback that converts an AtomicBlock into domain IR.
|
|
93
|
+
For GDS, this produces ``BlockIR``; OGS can produce ``OpenGameIR``.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Ordered list of compiled block IR objects.
|
|
97
|
+
"""
|
|
98
|
+
return [block_compiler(b) for b in root.flatten()]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
# Stage 2: extract_wirings
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def extract_wirings[W](
|
|
107
|
+
root: Block,
|
|
108
|
+
wiring_emitter: Callable[[StructuralWiring], W] | None = None,
|
|
109
|
+
) -> list[W]:
|
|
110
|
+
"""Walk the composition tree and emit all wirings through *wiring_emitter*.
|
|
111
|
+
|
|
112
|
+
The DFS traversal discovers explicit wirings, auto-wired connections,
|
|
113
|
+
feedback wirings, and temporal wirings — each tagged with a
|
|
114
|
+
``WiringOrigin``. The emitter callback transforms each
|
|
115
|
+
``StructuralWiring`` into domain-specific IR.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
root: Root of the composition tree.
|
|
119
|
+
wiring_emitter: Callback that converts a ``StructuralWiring`` into
|
|
120
|
+
domain IR. If None, uses the default GDS emitter that produces
|
|
121
|
+
``WiringIR``.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Ordered list of emitted wiring IR objects.
|
|
125
|
+
"""
|
|
126
|
+
if wiring_emitter is None:
|
|
127
|
+
wiring_emitter = _default_wiring_emitter # type: ignore[assignment]
|
|
128
|
+
|
|
129
|
+
structural: list[StructuralWiring] = []
|
|
130
|
+
_walk_structural_wirings(root, structural)
|
|
131
|
+
return [wiring_emitter(sw) for sw in structural]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ---------------------------------------------------------------------------
|
|
135
|
+
# Stage 3: extract_hierarchy
|
|
136
|
+
# ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def extract_hierarchy(root: Block) -> HierarchyNodeIR:
|
|
140
|
+
"""Build a ``HierarchyNodeIR`` tree from the composition tree.
|
|
141
|
+
|
|
142
|
+
Sequential and parallel chains are flattened from binary trees into
|
|
143
|
+
n-ary groups for cleaner visualization.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
root: Root of the composition tree.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Root ``HierarchyNodeIR`` with flattened chains.
|
|
150
|
+
"""
|
|
151
|
+
counter = [0]
|
|
152
|
+
hierarchy = _extract_hierarchy(root, counter)
|
|
153
|
+
return _flatten_sequential_chains(hierarchy)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
# compile_system — thin wrapper over the three stages
|
|
158
|
+
# ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
|
|
41
161
|
def compile_system(
|
|
42
162
|
name: str,
|
|
43
163
|
root: Block,
|
|
44
164
|
block_compiler: Callable[[AtomicBlock], BlockIR] | None = None,
|
|
165
|
+
wiring_emitter: Callable[[StructuralWiring], WiringIR] | None = None,
|
|
45
166
|
composition_type: CompositionType = CompositionType.SEQUENTIAL,
|
|
46
167
|
source: str = "",
|
|
168
|
+
inputs: list[InputIR] | None = None,
|
|
47
169
|
) -> SystemIR:
|
|
48
170
|
"""Compile a Block tree into a flat SystemIR.
|
|
49
171
|
|
|
@@ -52,34 +174,36 @@ def compile_system(
|
|
|
52
174
|
root: Root of the composition tree.
|
|
53
175
|
block_compiler: Domain-specific function to convert AtomicBlock → BlockIR.
|
|
54
176
|
If None, uses a default that extracts name + interface.
|
|
177
|
+
wiring_emitter: Domain-specific function to convert StructuralWiring →
|
|
178
|
+
WiringIR. If None, uses the default GDS emitter.
|
|
55
179
|
composition_type: Top-level composition type.
|
|
56
180
|
source: Source identifier.
|
|
181
|
+
inputs: External inputs to include in the SystemIR. Layer 0 never
|
|
182
|
+
infers inputs — domain packages supply them.
|
|
57
183
|
"""
|
|
58
184
|
if block_compiler is None:
|
|
59
185
|
block_compiler = _default_block_compiler
|
|
60
186
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
# 2. Wire
|
|
66
|
-
wirings = _extract_wirings(root)
|
|
67
|
-
|
|
68
|
-
# 3. Hierarchy
|
|
69
|
-
counter = [0]
|
|
70
|
-
hierarchy = _extract_hierarchy(root, counter)
|
|
71
|
-
hierarchy = _flatten_sequential_chains(hierarchy)
|
|
187
|
+
blocks = flatten_blocks(root, block_compiler)
|
|
188
|
+
wirings = extract_wirings(root, wiring_emitter)
|
|
189
|
+
hierarchy = extract_hierarchy(root)
|
|
72
190
|
|
|
73
191
|
return SystemIR(
|
|
74
192
|
name=name,
|
|
75
|
-
blocks=
|
|
193
|
+
blocks=blocks,
|
|
76
194
|
wirings=wirings,
|
|
195
|
+
inputs=inputs or [],
|
|
77
196
|
composition_type=composition_type,
|
|
78
197
|
hierarchy=hierarchy,
|
|
79
198
|
source=source,
|
|
80
199
|
)
|
|
81
200
|
|
|
82
201
|
|
|
202
|
+
# ---------------------------------------------------------------------------
|
|
203
|
+
# Default callbacks
|
|
204
|
+
# ---------------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
|
|
83
207
|
def _default_block_compiler(block: AtomicBlock) -> BlockIR:
|
|
84
208
|
"""Default block compiler — extracts name and interface slots."""
|
|
85
209
|
return BlockIR(
|
|
@@ -93,6 +217,18 @@ def _default_block_compiler(block: AtomicBlock) -> BlockIR:
|
|
|
93
217
|
)
|
|
94
218
|
|
|
95
219
|
|
|
220
|
+
def _default_wiring_emitter(sw: StructuralWiring) -> WiringIR:
|
|
221
|
+
"""Default wiring emitter — converts StructuralWiring to WiringIR."""
|
|
222
|
+
return WiringIR(
|
|
223
|
+
source=sw.source_block,
|
|
224
|
+
target=sw.target_block,
|
|
225
|
+
label=sw.source_port,
|
|
226
|
+
direction=sw.direction,
|
|
227
|
+
is_feedback=sw.origin == WiringOrigin.FEEDBACK,
|
|
228
|
+
is_temporal=sw.origin == WiringOrigin.TEMPORAL,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
96
232
|
def _ports_to_sig(ports: tuple[Port, ...]) -> str:
|
|
97
233
|
"""Convert a tuple of Ports to the IR signature string format."""
|
|
98
234
|
if not ports:
|
|
@@ -101,48 +237,53 @@ def _ports_to_sig(ports: tuple[Port, ...]) -> str:
|
|
|
101
237
|
|
|
102
238
|
|
|
103
239
|
# ---------------------------------------------------------------------------
|
|
104
|
-
#
|
|
240
|
+
# DFS wiring traversal (produces StructuralWiring intermediates)
|
|
105
241
|
# ---------------------------------------------------------------------------
|
|
106
242
|
|
|
107
243
|
|
|
108
|
-
def
|
|
109
|
-
"""Recursively walk the
|
|
110
|
-
wirings: list[WiringIR] = []
|
|
111
|
-
_walk_wirings(block, wirings)
|
|
112
|
-
return wirings
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def _walk_wirings(block: Block, wirings: list[WiringIR]) -> None:
|
|
116
|
-
"""Recursively walk the composition tree, collecting all wirings."""
|
|
244
|
+
def _walk_structural_wirings(block: Block, out: list[StructuralWiring]) -> None:
|
|
245
|
+
"""Recursively walk the composition tree, collecting StructuralWirings."""
|
|
117
246
|
if isinstance(block, AtomicBlock):
|
|
118
247
|
return
|
|
119
248
|
|
|
120
249
|
if isinstance(block, StackComposition):
|
|
121
|
-
|
|
122
|
-
|
|
250
|
+
_walk_structural_wirings(block.first, out)
|
|
251
|
+
_walk_structural_wirings(block.second, out)
|
|
123
252
|
|
|
124
253
|
for w in block.wiring:
|
|
125
|
-
|
|
254
|
+
out.append(_wiring_to_structural(w, WiringOrigin.EXPLICIT))
|
|
126
255
|
|
|
127
256
|
if not block.wiring:
|
|
128
|
-
_auto_wire_stack(block.first, block.second,
|
|
257
|
+
_auto_wire_stack(block.first, block.second, out)
|
|
129
258
|
|
|
130
259
|
elif isinstance(block, ParallelComposition):
|
|
131
|
-
|
|
132
|
-
|
|
260
|
+
_walk_structural_wirings(block.left, out)
|
|
261
|
+
_walk_structural_wirings(block.right, out)
|
|
133
262
|
|
|
134
263
|
elif isinstance(block, FeedbackLoop):
|
|
135
|
-
|
|
264
|
+
_walk_structural_wirings(block.inner, out)
|
|
136
265
|
for fw in block.feedback_wiring:
|
|
137
|
-
|
|
266
|
+
out.append(_wiring_to_structural(fw, WiringOrigin.FEEDBACK))
|
|
138
267
|
|
|
139
268
|
elif isinstance(block, TemporalLoop):
|
|
140
|
-
|
|
269
|
+
_walk_structural_wirings(block.inner, out)
|
|
141
270
|
for w in block.temporal_wiring:
|
|
142
|
-
|
|
271
|
+
out.append(_wiring_to_structural(w, WiringOrigin.TEMPORAL))
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _wiring_to_structural(wiring: Wiring, origin: WiringOrigin) -> StructuralWiring:
|
|
275
|
+
"""Convert a DSL Wiring to a StructuralWiring intermediate."""
|
|
276
|
+
return StructuralWiring(
|
|
277
|
+
source_block=wiring.source_block,
|
|
278
|
+
source_port=wiring.source_port,
|
|
279
|
+
target_block=wiring.target_block,
|
|
280
|
+
target_port=wiring.target_port,
|
|
281
|
+
direction=wiring.direction,
|
|
282
|
+
origin=origin,
|
|
283
|
+
)
|
|
143
284
|
|
|
144
285
|
|
|
145
|
-
def _auto_wire_stack(first: Block, second: Block,
|
|
286
|
+
def _auto_wire_stack(first: Block, second: Block, out: list[StructuralWiring]) -> None:
|
|
146
287
|
"""Auto-wire matching forward_out→forward_in ports in stack compositions."""
|
|
147
288
|
first_leaves = _get_leaf_names(first)
|
|
148
289
|
second_leaves = _get_leaf_names(second)
|
|
@@ -156,32 +297,18 @@ def _auto_wire_stack(first: Block, second: Block, wirings: list[WiringIR]) -> No
|
|
|
156
297
|
target = (
|
|
157
298
|
_find_port_owner(second, in_port, "forward_in") or second_leaves[0]
|
|
158
299
|
)
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
300
|
+
out.append(
|
|
301
|
+
StructuralWiring(
|
|
302
|
+
source_block=source,
|
|
303
|
+
source_port=out_port.name,
|
|
304
|
+
target_block=target,
|
|
305
|
+
target_port=in_port.name,
|
|
164
306
|
direction=FlowDirection.COVARIANT,
|
|
307
|
+
origin=WiringOrigin.AUTO,
|
|
165
308
|
)
|
|
166
309
|
)
|
|
167
310
|
|
|
168
311
|
|
|
169
|
-
def _wiring_to_ir(
|
|
170
|
-
wiring: Wiring,
|
|
171
|
-
is_feedback: bool = False,
|
|
172
|
-
is_temporal: bool = False,
|
|
173
|
-
) -> WiringIR:
|
|
174
|
-
"""Convert a DSL Wiring to an IR WiringIR."""
|
|
175
|
-
return WiringIR(
|
|
176
|
-
source=wiring.source_block,
|
|
177
|
-
target=wiring.target_block,
|
|
178
|
-
label=wiring.source_port,
|
|
179
|
-
direction=wiring.direction,
|
|
180
|
-
is_feedback=is_feedback,
|
|
181
|
-
is_temporal=is_temporal,
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
|
|
185
312
|
def _get_leaf_names(block: Block) -> list[str]:
|
|
186
313
|
"""Get names of all leaf (atomic) blocks."""
|
|
187
314
|
return [b.name for b in block.flatten()]
|
|
@@ -201,16 +328,11 @@ def _find_port_owner(block: Block, target_port: Port, slot: str) -> str | None:
|
|
|
201
328
|
# ---------------------------------------------------------------------------
|
|
202
329
|
|
|
203
330
|
|
|
204
|
-
def _sanitize_id(name: str) -> str:
|
|
205
|
-
"""Convert a name to a valid ID (alphanumeric + underscore only)."""
|
|
206
|
-
return re.sub(r"[^A-Za-z0-9_]", "_", name)
|
|
207
|
-
|
|
208
|
-
|
|
209
331
|
def _extract_hierarchy(block: Block, counter: list[int]) -> HierarchyNodeIR:
|
|
210
332
|
"""Recursively build a HierarchyNodeIR from the composition tree."""
|
|
211
333
|
if isinstance(block, AtomicBlock):
|
|
212
334
|
return HierarchyNodeIR(
|
|
213
|
-
id=f"leaf_{
|
|
335
|
+
id=f"leaf_{sanitize_id(block.name)}",
|
|
214
336
|
name=block.name,
|
|
215
337
|
composition_type=None,
|
|
216
338
|
block_name=block.name,
|
|
@@ -7,6 +7,7 @@ generic models with their own block types and metadata.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
import re
|
|
10
11
|
from enum import StrEnum
|
|
11
12
|
from typing import Any
|
|
12
13
|
|
|
@@ -15,6 +16,18 @@ from pydantic import BaseModel, Field
|
|
|
15
16
|
from gds.parameters import ParameterSchema
|
|
16
17
|
|
|
17
18
|
|
|
19
|
+
def sanitize_id(name: str) -> str:
|
|
20
|
+
"""Convert an arbitrary name to a valid IR/Mermaid identifier.
|
|
21
|
+
|
|
22
|
+
Replaces any character that is not alphanumeric or underscore with ``_``.
|
|
23
|
+
Prepends ``_`` if the result starts with a digit.
|
|
24
|
+
"""
|
|
25
|
+
sanitized = re.sub(r"[^A-Za-z0-9_]", "_", name)
|
|
26
|
+
if sanitized and sanitized[0].isdigit():
|
|
27
|
+
sanitized = "_" + sanitized
|
|
28
|
+
return sanitized
|
|
29
|
+
|
|
30
|
+
|
|
18
31
|
class FlowDirection(StrEnum):
|
|
19
32
|
"""Direction of an information flow in a block composition.
|
|
20
33
|
|
|
@@ -60,7 +73,9 @@ class WiringIR(BaseModel):
|
|
|
60
73
|
"""A directed connection (edge) between blocks in the IR.
|
|
61
74
|
|
|
62
75
|
``is_feedback`` and ``is_temporal`` flags distinguish special wiring
|
|
63
|
-
categories for verification.
|
|
76
|
+
categories for verification. The ``category`` field is an open string
|
|
77
|
+
that domain packages can use for domain-specific edge classification;
|
|
78
|
+
the generic protocol only interprets ``"dataflow"``.
|
|
64
79
|
"""
|
|
65
80
|
|
|
66
81
|
source: str
|
|
@@ -70,6 +85,7 @@ class WiringIR(BaseModel):
|
|
|
70
85
|
direction: FlowDirection
|
|
71
86
|
is_feedback: bool = False
|
|
72
87
|
is_temporal: bool = False
|
|
88
|
+
category: str = "dataflow"
|
|
73
89
|
|
|
74
90
|
|
|
75
91
|
class HierarchyNodeIR(BaseModel):
|
|
@@ -87,6 +103,18 @@ class HierarchyNodeIR(BaseModel):
|
|
|
87
103
|
exit_condition: str = ""
|
|
88
104
|
|
|
89
105
|
|
|
106
|
+
class InputIR(BaseModel, frozen=True):
|
|
107
|
+
"""An external input to the system.
|
|
108
|
+
|
|
109
|
+
Layer 0 defines only ``name`` and a generic ``metadata`` bag.
|
|
110
|
+
Domain packages store their richer fields (e.g., input_type, schema_hint)
|
|
111
|
+
inside ``metadata`` when projecting to SystemIR.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
name: str
|
|
115
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
116
|
+
|
|
117
|
+
|
|
90
118
|
class SystemIR(BaseModel):
|
|
91
119
|
"""A complete composed system — the top-level IR unit.
|
|
92
120
|
|
|
@@ -97,7 +125,7 @@ class SystemIR(BaseModel):
|
|
|
97
125
|
name: str
|
|
98
126
|
blocks: list[BlockIR] = Field(default_factory=list)
|
|
99
127
|
wirings: list[WiringIR] = Field(default_factory=list)
|
|
100
|
-
inputs: list[
|
|
128
|
+
inputs: list[InputIR] = Field(default_factory=list)
|
|
101
129
|
composition_type: CompositionType = CompositionType.SEQUENTIAL
|
|
102
130
|
hierarchy: HierarchyNodeIR | None = None
|
|
103
131
|
source: str = ""
|
|
@@ -98,27 +98,100 @@ def check_g002_signature_completeness(system: SystemIR) -> list[Finding]:
|
|
|
98
98
|
|
|
99
99
|
|
|
100
100
|
def check_g003_direction_consistency(system: SystemIR) -> list[Finding]:
|
|
101
|
-
"""G-003:
|
|
102
|
-
contravariant wirings should not be typed as forward.
|
|
101
|
+
"""G-003: Validate direction flag consistency and contravariant port-slot matching.
|
|
103
102
|
|
|
104
|
-
|
|
105
|
-
|
|
103
|
+
Two validations:
|
|
104
|
+
|
|
105
|
+
A) Flag consistency — ``direction``, ``is_feedback``, ``is_temporal`` must
|
|
106
|
+
not contradict:
|
|
107
|
+
- COVARIANT + is_feedback → ERROR (feedback implies contravariant)
|
|
108
|
+
- CONTRAVARIANT + is_temporal → ERROR (temporal implies covariant)
|
|
109
|
+
|
|
110
|
+
B) Contravariant port-slot matching — for CONTRAVARIANT wirings, the label
|
|
111
|
+
must be a token-subset of the source's backward_out (signature[3]) or
|
|
112
|
+
the target's backward_in (signature[2]). G-001 already covers the
|
|
113
|
+
covariant side.
|
|
106
114
|
"""
|
|
107
115
|
findings = []
|
|
116
|
+
block_sigs = {b.name: b.signature for b in system.blocks}
|
|
117
|
+
|
|
108
118
|
for wiring in system.wirings:
|
|
109
|
-
#
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
119
|
+
# A) Flag consistency
|
|
120
|
+
if wiring.direction == FlowDirection.COVARIANT and wiring.is_feedback:
|
|
121
|
+
findings.append(
|
|
122
|
+
Finding(
|
|
123
|
+
check_id="G-003",
|
|
124
|
+
severity=Severity.ERROR,
|
|
125
|
+
message=(
|
|
126
|
+
f"Wiring {wiring.label!r} "
|
|
127
|
+
f"({wiring.source} -> {wiring.target}): "
|
|
128
|
+
f"COVARIANT + is_feedback — contradiction"
|
|
129
|
+
),
|
|
130
|
+
source_elements=[wiring.source, wiring.target],
|
|
131
|
+
passed=False,
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
if wiring.direction == FlowDirection.CONTRAVARIANT and wiring.is_temporal:
|
|
137
|
+
findings.append(
|
|
138
|
+
Finding(
|
|
139
|
+
check_id="G-003",
|
|
140
|
+
severity=Severity.ERROR,
|
|
141
|
+
message=(
|
|
142
|
+
f"Wiring {wiring.label!r} "
|
|
143
|
+
f"({wiring.source} -> {wiring.target}): "
|
|
144
|
+
f"CONTRAVARIANT + is_temporal — contradiction"
|
|
145
|
+
),
|
|
146
|
+
source_elements=[wiring.source, wiring.target],
|
|
147
|
+
passed=False,
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
# B) Contravariant port-slot matching (G-001 covers covariant)
|
|
153
|
+
if wiring.direction == FlowDirection.CONTRAVARIANT:
|
|
154
|
+
if wiring.source not in block_sigs or wiring.target not in block_sigs:
|
|
155
|
+
# Non-block endpoints — G-004 handles dangling references
|
|
156
|
+
continue
|
|
157
|
+
|
|
158
|
+
src_bwd_out = block_sigs[wiring.source][3] # backward_out
|
|
159
|
+
tgt_bwd_in = block_sigs[wiring.target][2] # backward_in
|
|
160
|
+
|
|
161
|
+
if not src_bwd_out and not tgt_bwd_in:
|
|
162
|
+
findings.append(
|
|
163
|
+
Finding(
|
|
164
|
+
check_id="G-003",
|
|
165
|
+
severity=Severity.ERROR,
|
|
166
|
+
message=(
|
|
167
|
+
f"Wiring {wiring.label!r} "
|
|
168
|
+
f"({wiring.source} -> {wiring.target}): "
|
|
169
|
+
f"CONTRAVARIANT but both backward "
|
|
170
|
+
f"ports are empty"
|
|
171
|
+
),
|
|
172
|
+
source_elements=[wiring.source, wiring.target],
|
|
173
|
+
passed=False,
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
continue
|
|
177
|
+
|
|
178
|
+
compatible = tokens_subset(wiring.label, src_bwd_out) or tokens_subset(
|
|
179
|
+
wiring.label, tgt_bwd_in
|
|
180
|
+
)
|
|
181
|
+
findings.append(
|
|
182
|
+
Finding(
|
|
183
|
+
check_id="G-003",
|
|
184
|
+
severity=Severity.ERROR,
|
|
185
|
+
message=(
|
|
186
|
+
f"Wiring {wiring.label!r}: "
|
|
187
|
+
f"{wiring.source} bwd_out={src_bwd_out!r} -> "
|
|
188
|
+
f"{wiring.target} bwd_in={tgt_bwd_in!r}"
|
|
189
|
+
+ ("" if compatible else " — MISMATCH")
|
|
190
|
+
),
|
|
191
|
+
source_elements=[wiring.source, wiring.target],
|
|
192
|
+
passed=compatible,
|
|
193
|
+
)
|
|
120
194
|
)
|
|
121
|
-
)
|
|
122
195
|
|
|
123
196
|
return findings
|
|
124
197
|
|
|
@@ -127,10 +200,8 @@ def check_g004_dangling_wirings(system: SystemIR) -> list[Finding]:
|
|
|
127
200
|
"""G-004: Flag wirings whose source or target is not in the system."""
|
|
128
201
|
findings = []
|
|
129
202
|
known_names = {b.name for b in system.blocks}
|
|
130
|
-
# Also include input names
|
|
131
203
|
for inp in system.inputs:
|
|
132
|
-
|
|
133
|
-
known_names.add(inp["name"])
|
|
204
|
+
known_names.add(inp.name)
|
|
134
205
|
|
|
135
206
|
for wiring in system.wirings:
|
|
136
207
|
src_ok = wiring.source in known_names
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "gds-framework"
|
|
3
|
-
|
|
3
|
+
dynamic = ["version"]
|
|
4
4
|
description = "Generalized Dynamical Systems — typed compositional specifications for complex systems"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -47,6 +47,9 @@ Documentation = "https://blockscience.github.io/gds-core"
|
|
|
47
47
|
requires = ["hatchling"]
|
|
48
48
|
build-backend = "hatchling.build"
|
|
49
49
|
|
|
50
|
+
[tool.hatch.version]
|
|
51
|
+
path = "gds/__init__.py"
|
|
52
|
+
|
|
50
53
|
[tool.hatch.build.targets.wheel]
|
|
51
54
|
packages = ["gds"]
|
|
52
55
|
|