hardwave 0.1.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.
- hardwave/__init__.py +118 -0
- hardwave/components/__init__.py +15 -0
- hardwave/components/base.py +253 -0
- hardwave/components/composite.py +237 -0
- hardwave/components/decorators.py +56 -0
- hardwave/components/registry.py +160 -0
- hardwave/diagnostics/__init__.py +37 -0
- hardwave/diagnostics/diagnostic.py +119 -0
- hardwave/ports/__init__.py +3 -0
- hardwave/ports/port.py +117 -0
- hardwave/premium/__init__.py +224 -0
- hardwave/premium/client.py +291 -0
- hardwave/premium/exceptions.py +29 -0
- hardwave/premium/loader.py +140 -0
- hardwave/premium/solver.py +44 -0
- hardwave/serialization/__init__.py +3 -0
- hardwave/serialization/schema.py +25 -0
- hardwave/simulation/__init__.py +10 -0
- hardwave/simulation/engine.py +310 -0
- hardwave/simulation/graph.py +450 -0
- hardwave/simulation/result.py +135 -0
- hardwave/solvers/__init__.py +14 -0
- hardwave/solvers/base.py +59 -0
- hardwave/solvers/formula.py +28 -0
- hardwave/solvers/lookup.py +79 -0
- hardwave/solvers/ml.py +80 -0
- hardwave/solvers/ode.py +153 -0
- hardwave/stdlib/__init__.py +7 -0
- hardwave/stdlib/components/__init__.py +7 -0
- hardwave/stdlib/components/active.py +267 -0
- hardwave/stdlib/components/mcu.py +205 -0
- hardwave/stdlib/components/motors.py +432 -0
- hardwave/stdlib/components/passive.py +433 -0
- hardwave/stdlib/components/power.py +313 -0
- hardwave/stdlib/components/sensors.py +263 -0
- hardwave/stdlib/types/__init__.py +17 -0
- hardwave/stdlib/types/control.py +70 -0
- hardwave/stdlib/types/electrical.py +193 -0
- hardwave/stdlib/types/mechanical.py +70 -0
- hardwave/stdlib/types/thermal.py +37 -0
- hardwave/telemetry/__init__.py +5 -0
- hardwave/telemetry/record.py +61 -0
- hardwave/telemetry/store.py +108 -0
- hardwave/telemetry/trainer.py +196 -0
- hardwave/types/__init__.py +4 -0
- hardwave/types/base.py +73 -0
- hardwave/types/registry.py +78 -0
- hardwave/visualization/__init__.py +3 -0
- hardwave/visualization/spec.py +60 -0
- hardwave-0.1.0.dist-info/METADATA +822 -0
- hardwave-0.1.0.dist-info/RECORD +54 -0
- hardwave-0.1.0.dist-info/WHEEL +5 -0
- hardwave-0.1.0.dist-info/licenses/LICENSE +21 -0
- hardwave-0.1.0.dist-info/top_level.txt +1 -0
hardwave/__init__.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hardwave Platform — hardware simulation and development platform.
|
|
3
|
+
|
|
4
|
+
Importing this package sets up all core infrastructure. Import `hardwave.stdlib`
|
|
5
|
+
to also register all standard-library types and components.
|
|
6
|
+
|
|
7
|
+
Quick start:
|
|
8
|
+
import hardwave
|
|
9
|
+
import hardwave.stdlib
|
|
10
|
+
|
|
11
|
+
from hardwave.simulation import SimulationGraph, SimulationEngine, SimulationConfig
|
|
12
|
+
from hardwave.solvers import SimulationDomain
|
|
13
|
+
from hardwave.stdlib.components.passive import Resistor, VoltageSource, Ground
|
|
14
|
+
|
|
15
|
+
graph = SimulationGraph()
|
|
16
|
+
graph.add_component(vs := VoltageSource("vs", param_values={"voltage": 9.0}))
|
|
17
|
+
graph.add_component(r1 := Resistor("r1", param_values={"resistance": 1000.0}))
|
|
18
|
+
graph.add_component(r2 := Resistor("r2", param_values={"resistance": 2000.0}))
|
|
19
|
+
graph.connect("vs", "voltage_out", "r1", "voltage")
|
|
20
|
+
graph.connect("vs", "voltage_out", "r2", "voltage")
|
|
21
|
+
graph.validate()
|
|
22
|
+
|
|
23
|
+
engine = SimulationEngine(graph)
|
|
24
|
+
result = engine.run(inputs={})
|
|
25
|
+
print(result.get_output("r1", "current")) # 0.009
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
__version__ = "0.1.0"
|
|
29
|
+
|
|
30
|
+
# Core infrastructure (no side effects — registries are empty until populated)
|
|
31
|
+
from hardwave.diagnostics import (
|
|
32
|
+
Diagnostic,
|
|
33
|
+
DiagnosticLevel,
|
|
34
|
+
HardwaveError,
|
|
35
|
+
TypeRegistrationError,
|
|
36
|
+
TypeNotFoundError,
|
|
37
|
+
ComponentRegistrationError,
|
|
38
|
+
ComponentNotFoundError,
|
|
39
|
+
PortNotFoundError,
|
|
40
|
+
TypeMismatchError,
|
|
41
|
+
PortDirectionError,
|
|
42
|
+
DuplicateConnectionError,
|
|
43
|
+
GraphValidationError,
|
|
44
|
+
SolverError,
|
|
45
|
+
SimulationError,
|
|
46
|
+
SimulationResultError,
|
|
47
|
+
DeserializationError,
|
|
48
|
+
)
|
|
49
|
+
from hardwave.premium.exceptions import (
|
|
50
|
+
PremiumError,
|
|
51
|
+
PremiumNotConfiguredError,
|
|
52
|
+
PremiumAuthError,
|
|
53
|
+
PremiumSyncError,
|
|
54
|
+
PremiumSolveError,
|
|
55
|
+
PremiumGraphSaveError,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
from hardwave.types import HardwaveType, TypeRegistry
|
|
59
|
+
from hardwave.ports import Port, PortDirection, input_port, output_port
|
|
60
|
+
from hardwave.components import (
|
|
61
|
+
Component,
|
|
62
|
+
ComponentMeta,
|
|
63
|
+
Parameter,
|
|
64
|
+
CompositeComponent,
|
|
65
|
+
BuildContext,
|
|
66
|
+
Connection,
|
|
67
|
+
ComponentRegistry,
|
|
68
|
+
component,
|
|
69
|
+
)
|
|
70
|
+
from hardwave.solvers import (
|
|
71
|
+
Solver,
|
|
72
|
+
SimulationDomain,
|
|
73
|
+
FormulaSolver,
|
|
74
|
+
LookupTableSolver,
|
|
75
|
+
ODESolver,
|
|
76
|
+
MLModelSolver,
|
|
77
|
+
)
|
|
78
|
+
from hardwave.simulation import (
|
|
79
|
+
SimulationGraph,
|
|
80
|
+
SimulationEngine,
|
|
81
|
+
SimulationConfig,
|
|
82
|
+
SimulationResult,
|
|
83
|
+
)
|
|
84
|
+
from hardwave.visualization import VisualizationSpec, VisualizationKind
|
|
85
|
+
from hardwave.telemetry import TelemetryRecord, TelemetryStore, ModelTrainer
|
|
86
|
+
from hardwave.serialization import Serializable, SCHEMA_VERSION
|
|
87
|
+
|
|
88
|
+
__all__ = [
|
|
89
|
+
# Diagnostics
|
|
90
|
+
"Diagnostic", "DiagnosticLevel",
|
|
91
|
+
"HardwaveError", "TypeRegistrationError", "TypeNotFoundError",
|
|
92
|
+
"ComponentRegistrationError", "ComponentNotFoundError",
|
|
93
|
+
"PortNotFoundError", "TypeMismatchError", "PortDirectionError",
|
|
94
|
+
"DuplicateConnectionError", "GraphValidationError", "SolverError",
|
|
95
|
+
"SimulationError", "SimulationResultError", "DeserializationError",
|
|
96
|
+
# Premium
|
|
97
|
+
"PremiumError", "PremiumNotConfiguredError", "PremiumAuthError",
|
|
98
|
+
"PremiumSyncError", "PremiumSolveError", "PremiumGraphSaveError",
|
|
99
|
+
# Types
|
|
100
|
+
"HardwaveType", "TypeRegistry",
|
|
101
|
+
# Ports
|
|
102
|
+
"Port", "PortDirection", "input_port", "output_port",
|
|
103
|
+
# Components
|
|
104
|
+
"Component", "ComponentMeta", "Parameter",
|
|
105
|
+
"CompositeComponent", "BuildContext", "Connection",
|
|
106
|
+
"ComponentRegistry", "component",
|
|
107
|
+
# Solvers
|
|
108
|
+
"Solver", "SimulationDomain", "FormulaSolver",
|
|
109
|
+
"LookupTableSolver", "ODESolver", "MLModelSolver",
|
|
110
|
+
# Simulation
|
|
111
|
+
"SimulationGraph", "SimulationEngine", "SimulationConfig", "SimulationResult",
|
|
112
|
+
# Visualization
|
|
113
|
+
"VisualizationSpec", "VisualizationKind",
|
|
114
|
+
# Telemetry
|
|
115
|
+
"TelemetryRecord", "TelemetryStore", "ModelTrainer",
|
|
116
|
+
# Serialization
|
|
117
|
+
"Serializable", "SCHEMA_VERSION",
|
|
118
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from hardwave.components.base import Component, ComponentMeta, Parameter
|
|
2
|
+
from hardwave.components.composite import CompositeComponent, BuildContext, Connection
|
|
3
|
+
from hardwave.components.registry import ComponentRegistry
|
|
4
|
+
from hardwave.components.decorators import component
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"Component",
|
|
8
|
+
"ComponentMeta",
|
|
9
|
+
"Parameter",
|
|
10
|
+
"CompositeComponent",
|
|
11
|
+
"BuildContext",
|
|
12
|
+
"Connection",
|
|
13
|
+
"ComponentRegistry",
|
|
14
|
+
"component",
|
|
15
|
+
]
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Component base class, ComponentMeta, and Parameter.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Any, ClassVar, Optional, Type, TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from hardwave.ports.port import Port
|
|
12
|
+
from hardwave.diagnostics import PortNotFoundError
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from hardwave.solvers.base import Solver
|
|
16
|
+
from hardwave.visualization.spec import VisualizationSpec
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
# ComponentMeta
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class ComponentMeta:
|
|
25
|
+
"""
|
|
26
|
+
Static metadata for a component class.
|
|
27
|
+
|
|
28
|
+
This is attached to the component class itself (not instances) and is
|
|
29
|
+
used by the registry and UI.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
name: str # Unique identifier, e.g. "DCMotor"
|
|
33
|
+
display_name: str # Human-readable, e.g. "DC Brushed Motor"
|
|
34
|
+
version: str # Semantic version, e.g. "1.0.0"
|
|
35
|
+
description: str # One-line summary
|
|
36
|
+
docs: str # Full markdown documentation
|
|
37
|
+
category: str # Hierarchy path, e.g. "Electrical/Motors"
|
|
38
|
+
tags: list[str] = field(default_factory=list)
|
|
39
|
+
author: str = ""
|
|
40
|
+
source_url: str = "" # Link to datasheet, GitHub repo, etc.
|
|
41
|
+
|
|
42
|
+
def to_dict(self) -> dict:
|
|
43
|
+
return {
|
|
44
|
+
"name": self.name,
|
|
45
|
+
"display_name": self.display_name,
|
|
46
|
+
"version": self.version,
|
|
47
|
+
"description": self.description,
|
|
48
|
+
"docs": self.docs,
|
|
49
|
+
"category": self.category,
|
|
50
|
+
"tags": list(self.tags),
|
|
51
|
+
"author": self.author,
|
|
52
|
+
"source_url": self.source_url,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
# Parameter
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class Parameter:
|
|
62
|
+
"""
|
|
63
|
+
A static configuration value on a component.
|
|
64
|
+
|
|
65
|
+
Unlike ports, parameters do not carry signals between components.
|
|
66
|
+
They are set at instantiation time and remain constant during simulation.
|
|
67
|
+
|
|
68
|
+
Parameters can also be exposed as input ports to allow them to be driven
|
|
69
|
+
by other components (e.g., a variable resistor whose resistance is the
|
|
70
|
+
output of another component). Set `can_be_port=True` to enable this.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
name: str
|
|
74
|
+
display_name: str
|
|
75
|
+
unit: str
|
|
76
|
+
python_type: Type
|
|
77
|
+
default: Any
|
|
78
|
+
description: str = ""
|
|
79
|
+
min_value: Optional[float] = None
|
|
80
|
+
max_value: Optional[float] = None
|
|
81
|
+
can_be_port: bool = False # If True, component may expose this as an input port
|
|
82
|
+
|
|
83
|
+
def to_dict(self) -> dict:
|
|
84
|
+
return {
|
|
85
|
+
"name": self.name,
|
|
86
|
+
"display_name": self.display_name,
|
|
87
|
+
"unit": self.unit,
|
|
88
|
+
"python_type": self.python_type.__name__,
|
|
89
|
+
"default": self.default,
|
|
90
|
+
"description": self.description,
|
|
91
|
+
"min_value": self.min_value,
|
|
92
|
+
"max_value": self.max_value,
|
|
93
|
+
"can_be_port": self.can_be_port,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
# Component base class
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
class Component(ABC):
|
|
102
|
+
"""
|
|
103
|
+
Base class for all hardware components.
|
|
104
|
+
|
|
105
|
+
Subclasses must:
|
|
106
|
+
1. Define `meta: ClassVar[ComponentMeta]`
|
|
107
|
+
2. Define `ports: ClassVar[list[Port]]`
|
|
108
|
+
3. Implement `solve(inputs: dict) -> dict`
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
# --- Class-level declarations (must be set by subclass) ---
|
|
112
|
+
meta: ClassVar[ComponentMeta]
|
|
113
|
+
ports: ClassVar[list[Port]]
|
|
114
|
+
parameters: ClassVar[list[Parameter]] = []
|
|
115
|
+
visualizations: ClassVar[list["VisualizationSpec"]] = []
|
|
116
|
+
|
|
117
|
+
def __init__(
|
|
118
|
+
self,
|
|
119
|
+
instance_id: str,
|
|
120
|
+
param_values: Optional[dict] = None,
|
|
121
|
+
) -> None:
|
|
122
|
+
"""
|
|
123
|
+
Args:
|
|
124
|
+
instance_id: Unique identifier for this instance within a project.
|
|
125
|
+
e.g. "motor_1", "r3"
|
|
126
|
+
param_values: Override values for class-level parameters.
|
|
127
|
+
"""
|
|
128
|
+
self.instance_id = instance_id
|
|
129
|
+
self._param_values: dict = dict(param_values) if param_values else {}
|
|
130
|
+
self._solver: Optional["Solver"] = None # Injected solver (optional override)
|
|
131
|
+
self._telemetry_log: list[dict] = []
|
|
132
|
+
|
|
133
|
+
# ------------------------------------------------------------------
|
|
134
|
+
# Port access
|
|
135
|
+
# ------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
@classmethod
|
|
138
|
+
def get_port(cls, name: str) -> Port:
|
|
139
|
+
"""Return the port with the given name. Raises PortNotFoundError if absent."""
|
|
140
|
+
for p in cls.ports:
|
|
141
|
+
if p.name == name:
|
|
142
|
+
return p
|
|
143
|
+
raise PortNotFoundError(
|
|
144
|
+
f"Port '{name}' not found on component '{cls.meta.name}'. "
|
|
145
|
+
f"Available ports: {[p.name for p in cls.ports]}"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def input_ports(cls) -> list[Port]:
|
|
150
|
+
"""Return all INPUT ports."""
|
|
151
|
+
return [p for p in cls.ports if p.is_input]
|
|
152
|
+
|
|
153
|
+
@classmethod
|
|
154
|
+
def output_ports(cls) -> list[Port]:
|
|
155
|
+
"""Return all OUTPUT ports."""
|
|
156
|
+
return [p for p in cls.ports if p.is_output]
|
|
157
|
+
|
|
158
|
+
# ------------------------------------------------------------------
|
|
159
|
+
# Parameter access
|
|
160
|
+
# ------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
def get_param(self, name: str) -> Any:
|
|
163
|
+
"""
|
|
164
|
+
Return the effective value for a parameter.
|
|
165
|
+
Instance-level override takes priority over class-level default.
|
|
166
|
+
"""
|
|
167
|
+
if name in self._param_values:
|
|
168
|
+
return self._param_values[name]
|
|
169
|
+
for p in self.parameters:
|
|
170
|
+
if p.name == name:
|
|
171
|
+
return p.default
|
|
172
|
+
raise KeyError(
|
|
173
|
+
f"Parameter '{name}' not found on component '{self.meta.name}'"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def set_param(self, name: str, value: Any) -> None:
|
|
177
|
+
"""Set an instance-level parameter override."""
|
|
178
|
+
param_names = [p.name for p in self.parameters]
|
|
179
|
+
if name not in param_names:
|
|
180
|
+
raise KeyError(
|
|
181
|
+
f"Parameter '{name}' not found on component '{self.meta.name}'. "
|
|
182
|
+
f"Available: {param_names}"
|
|
183
|
+
)
|
|
184
|
+
self._param_values[name] = value
|
|
185
|
+
|
|
186
|
+
# ------------------------------------------------------------------
|
|
187
|
+
# Solver
|
|
188
|
+
# ------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
@abstractmethod
|
|
191
|
+
def solve(self, inputs: dict) -> dict:
|
|
192
|
+
"""
|
|
193
|
+
Transform a dict of input values into a dict of output values.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
inputs: Dict mapping input port names to their current values.
|
|
197
|
+
Only ports that are connected or have defaults are included.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Dict mapping output port names to computed values.
|
|
201
|
+
Must include all output port names defined in cls.ports.
|
|
202
|
+
|
|
203
|
+
Raises:
|
|
204
|
+
SolverError: If the computation cannot complete (e.g., division by zero,
|
|
205
|
+
model inference failure, physics divergence).
|
|
206
|
+
"""
|
|
207
|
+
...
|
|
208
|
+
|
|
209
|
+
def set_solver(self, solver: "Solver") -> None:
|
|
210
|
+
"""
|
|
211
|
+
Inject an alternative solver.
|
|
212
|
+
When set, the simulation engine calls solver.solve(inputs, component)
|
|
213
|
+
instead of self.solve(inputs).
|
|
214
|
+
This allows swapping in an ML-based solver without modifying the component.
|
|
215
|
+
"""
|
|
216
|
+
self._solver = solver
|
|
217
|
+
|
|
218
|
+
def _execute_solve(self, inputs: dict) -> dict:
|
|
219
|
+
"""
|
|
220
|
+
Called by the simulation engine to get outputs.
|
|
221
|
+
Routes to injected solver if present, otherwise calls self.solve().
|
|
222
|
+
"""
|
|
223
|
+
if self._solver is not None:
|
|
224
|
+
return self._solver.solve(inputs, self)
|
|
225
|
+
return self.solve(inputs)
|
|
226
|
+
|
|
227
|
+
# ------------------------------------------------------------------
|
|
228
|
+
# Telemetry
|
|
229
|
+
# ------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
def log_telemetry(self, inputs: dict, outputs: dict, timestamp: float) -> None:
|
|
232
|
+
"""
|
|
233
|
+
Record an inputs/outputs pair with a timestamp.
|
|
234
|
+
Used to accumulate real-world data for model training.
|
|
235
|
+
"""
|
|
236
|
+
self._telemetry_log.append(
|
|
237
|
+
{
|
|
238
|
+
"timestamp": timestamp,
|
|
239
|
+
"inputs": dict(inputs),
|
|
240
|
+
"outputs": dict(outputs),
|
|
241
|
+
}
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
def export_telemetry(self) -> list[dict]:
|
|
245
|
+
"""Return accumulated telemetry records."""
|
|
246
|
+
return list(self._telemetry_log)
|
|
247
|
+
|
|
248
|
+
# ------------------------------------------------------------------
|
|
249
|
+
# Utility
|
|
250
|
+
# ------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
def __repr__(self) -> str:
|
|
253
|
+
return f"{type(self).__name__}(instance_id={self.instance_id!r})"
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CompositeComponent, BuildContext, and Connection.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from abc import abstractmethod
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from hardwave.components.base import Component
|
|
12
|
+
from hardwave.diagnostics import (
|
|
13
|
+
PortNotFoundError,
|
|
14
|
+
SolverError,
|
|
15
|
+
HardwaveError,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
# Connection
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class Connection:
|
|
25
|
+
"""
|
|
26
|
+
A directed edge in the component graph.
|
|
27
|
+
Connects an output port of one component instance to an input port of another.
|
|
28
|
+
|
|
29
|
+
source_id: instance_id of the source component
|
|
30
|
+
source_port: name of the output port on the source
|
|
31
|
+
target_id: instance_id of the target component
|
|
32
|
+
target_port: name of the input port on the target
|
|
33
|
+
"""
|
|
34
|
+
source_id: str
|
|
35
|
+
source_port: str
|
|
36
|
+
target_id: str
|
|
37
|
+
target_port: str
|
|
38
|
+
|
|
39
|
+
def to_dict(self) -> dict:
|
|
40
|
+
return {
|
|
41
|
+
"source_id": self.source_id,
|
|
42
|
+
"source_port": self.source_port,
|
|
43
|
+
"target_id": self.target_id,
|
|
44
|
+
"target_port": self.target_port,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# BuildContext
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
class BuildContext:
|
|
53
|
+
"""
|
|
54
|
+
Context object passed to CompositeComponent.build().
|
|
55
|
+
Provides the API for constructing the internal sub-graph.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(self) -> None:
|
|
59
|
+
self._components: dict[str, Component] = {}
|
|
60
|
+
self._connections: list[Connection] = []
|
|
61
|
+
# Maps external input port name → list of (internal_component_id, internal_port)
|
|
62
|
+
# (fan-out: one external input can feed multiple internal ports)
|
|
63
|
+
self._input_map: dict[str, list[tuple[str, str]]] = {}
|
|
64
|
+
# Maps external output port name → (internal_component_id, internal_port)
|
|
65
|
+
self._output_map: dict[str, tuple[str, str]] = {}
|
|
66
|
+
|
|
67
|
+
def add(self, component: Component) -> Component:
|
|
68
|
+
"""Register a sub-component instance. Returns the instance for chaining."""
|
|
69
|
+
self._components[component.instance_id] = component
|
|
70
|
+
return component
|
|
71
|
+
|
|
72
|
+
def connect(self, connection: Connection) -> None:
|
|
73
|
+
"""Add a directed edge between two sub-component ports."""
|
|
74
|
+
self._connections.append(connection)
|
|
75
|
+
|
|
76
|
+
def map_input(
|
|
77
|
+
self,
|
|
78
|
+
external_port: str,
|
|
79
|
+
internal_component_id: str,
|
|
80
|
+
internal_port: str,
|
|
81
|
+
) -> None:
|
|
82
|
+
"""
|
|
83
|
+
Route an input value arriving at the composite's external port
|
|
84
|
+
to a sub-component's input port.
|
|
85
|
+
Multiple calls with the same external_port create fan-out: the same
|
|
86
|
+
external value is fed to all mapped internal ports.
|
|
87
|
+
"""
|
|
88
|
+
if external_port not in self._input_map:
|
|
89
|
+
self._input_map[external_port] = []
|
|
90
|
+
self._input_map[external_port].append((internal_component_id, internal_port))
|
|
91
|
+
|
|
92
|
+
def map_output(
|
|
93
|
+
self,
|
|
94
|
+
external_port: str,
|
|
95
|
+
internal_component_id: str,
|
|
96
|
+
internal_port: str,
|
|
97
|
+
) -> None:
|
|
98
|
+
"""
|
|
99
|
+
Route a sub-component's output port value to the composite's
|
|
100
|
+
external output port.
|
|
101
|
+
"""
|
|
102
|
+
self._output_map[external_port] = (internal_component_id, internal_port)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
# CompositeComponent
|
|
107
|
+
# ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
class CompositeComponent(Component):
|
|
110
|
+
"""
|
|
111
|
+
A component built from a sub-graph of connected sub-components.
|
|
112
|
+
|
|
113
|
+
Subclasses define the internal topology by overriding `build()`.
|
|
114
|
+
The `solve()` method is provided by CompositeComponent and runs the
|
|
115
|
+
internal graph, propagating values from the composite's own input
|
|
116
|
+
ports through the sub-graph and collecting the final outputs.
|
|
117
|
+
|
|
118
|
+
Subclasses must:
|
|
119
|
+
1. Define `meta`, `ports` (the EXTERNAL interface), and `parameters`
|
|
120
|
+
as for any Component.
|
|
121
|
+
2. Implement `build(ctx: BuildContext) -> None` which:
|
|
122
|
+
a. Adds sub-component instances via ctx.add(component_instance)
|
|
123
|
+
b. Connects them via ctx.connect(Connection(...))
|
|
124
|
+
c. Maps the composite's external ports to internal ports via
|
|
125
|
+
ctx.map_input(...) and ctx.map_output(...)
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
def __init__(
|
|
129
|
+
self,
|
|
130
|
+
instance_id: str,
|
|
131
|
+
param_values: Optional[dict] = None,
|
|
132
|
+
) -> None:
|
|
133
|
+
super().__init__(instance_id, param_values)
|
|
134
|
+
self._build_context: Optional[BuildContext] = None
|
|
135
|
+
|
|
136
|
+
def _get_context(self) -> BuildContext:
|
|
137
|
+
"""Lazily build and cache the internal sub-graph."""
|
|
138
|
+
if self._build_context is None:
|
|
139
|
+
ctx = BuildContext()
|
|
140
|
+
self.build(ctx)
|
|
141
|
+
self._build_context = ctx
|
|
142
|
+
return self._build_context
|
|
143
|
+
|
|
144
|
+
@abstractmethod
|
|
145
|
+
def build(self, ctx: BuildContext) -> None:
|
|
146
|
+
"""Define the internal sub-graph."""
|
|
147
|
+
...
|
|
148
|
+
|
|
149
|
+
def solve(self, inputs: dict) -> dict:
|
|
150
|
+
"""Run the internal sub-graph. Do not override."""
|
|
151
|
+
ctx = self._get_context()
|
|
152
|
+
|
|
153
|
+
# values[component_id][port_name] = value
|
|
154
|
+
values: dict[str, dict] = {cid: {} for cid in ctx._components}
|
|
155
|
+
|
|
156
|
+
# Feed external inputs into the sub-graph via input mappings (fan-out supported)
|
|
157
|
+
for ext_port, targets in ctx._input_map.items():
|
|
158
|
+
if ext_port in inputs:
|
|
159
|
+
for (int_comp_id, int_port) in targets:
|
|
160
|
+
values[int_comp_id][int_port] = inputs[ext_port]
|
|
161
|
+
|
|
162
|
+
# Topological sort of the internal sub-graph
|
|
163
|
+
order = self._topological_order(ctx)
|
|
164
|
+
|
|
165
|
+
# Run each sub-component in order
|
|
166
|
+
for comp in order:
|
|
167
|
+
cid = comp.instance_id
|
|
168
|
+
comp_inputs: dict = {}
|
|
169
|
+
|
|
170
|
+
# Gather from established values (from upstream outputs or injected inputs)
|
|
171
|
+
for port in comp.input_ports():
|
|
172
|
+
if port.name in values[cid]:
|
|
173
|
+
comp_inputs[port.name] = values[cid][port.name]
|
|
174
|
+
elif not port.required and port.default_value is not None:
|
|
175
|
+
comp_inputs[port.name] = port.default_value
|
|
176
|
+
|
|
177
|
+
# Execute
|
|
178
|
+
try:
|
|
179
|
+
comp_outputs = comp._execute_solve(comp_inputs)
|
|
180
|
+
except Exception as exc:
|
|
181
|
+
raise SolverError(
|
|
182
|
+
f"CompositeComponent '{self.instance_id}': sub-component "
|
|
183
|
+
f"'{cid}' failed: {exc}"
|
|
184
|
+
) from exc
|
|
185
|
+
|
|
186
|
+
# Store outputs
|
|
187
|
+
for k, v in comp_outputs.items():
|
|
188
|
+
values[cid][k] = v
|
|
189
|
+
|
|
190
|
+
# Propagate to downstream components via connections
|
|
191
|
+
for conn in ctx._connections:
|
|
192
|
+
if conn.source_id == cid and conn.source_port in comp_outputs:
|
|
193
|
+
values[conn.target_id][conn.target_port] = comp_outputs[conn.source_port]
|
|
194
|
+
|
|
195
|
+
# Collect external outputs via output mappings
|
|
196
|
+
result: dict = {}
|
|
197
|
+
for ext_port, (int_comp_id, int_port) in ctx._output_map.items():
|
|
198
|
+
if int_comp_id in values and int_port in values[int_comp_id]:
|
|
199
|
+
result[ext_port] = values[int_comp_id][int_port]
|
|
200
|
+
else:
|
|
201
|
+
raise SolverError(
|
|
202
|
+
f"CompositeComponent '{self.instance_id}': output mapping "
|
|
203
|
+
f"'{ext_port}' → '{int_comp_id}.{int_port}' produced no value"
|
|
204
|
+
)
|
|
205
|
+
return result
|
|
206
|
+
|
|
207
|
+
# ------------------------------------------------------------------
|
|
208
|
+
# Internal topological sort
|
|
209
|
+
# ------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
def _topological_order(self, ctx: BuildContext) -> list[Component]:
|
|
212
|
+
"""Kahn's algorithm on the internal sub-graph."""
|
|
213
|
+
in_degree: dict[str, int] = {cid: 0 for cid in ctx._components}
|
|
214
|
+
adjacency: dict[str, list[str]] = {cid: [] for cid in ctx._components}
|
|
215
|
+
|
|
216
|
+
for conn in ctx._connections:
|
|
217
|
+
if conn.source_id in adjacency and conn.target_id in in_degree:
|
|
218
|
+
adjacency[conn.source_id].append(conn.target_id)
|
|
219
|
+
in_degree[conn.target_id] += 1
|
|
220
|
+
|
|
221
|
+
queue = [cid for cid, deg in in_degree.items() if deg == 0]
|
|
222
|
+
order: list[str] = []
|
|
223
|
+
|
|
224
|
+
while queue:
|
|
225
|
+
cid = queue.pop(0)
|
|
226
|
+
order.append(cid)
|
|
227
|
+
for neighbour in adjacency[cid]:
|
|
228
|
+
in_degree[neighbour] -= 1
|
|
229
|
+
if in_degree[neighbour] == 0:
|
|
230
|
+
queue.append(neighbour)
|
|
231
|
+
|
|
232
|
+
if len(order) != len(ctx._components):
|
|
233
|
+
raise SolverError(
|
|
234
|
+
f"CompositeComponent '{self.instance_id}' contains a cycle "
|
|
235
|
+
"in its internal sub-graph"
|
|
236
|
+
)
|
|
237
|
+
return [ctx._components[cid] for cid in order]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@component decorator: attaches ComponentMeta and auto-registers the class.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Callable, Optional, Type
|
|
8
|
+
|
|
9
|
+
from hardwave.components.base import Component, ComponentMeta
|
|
10
|
+
from hardwave.components.registry import ComponentRegistry
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def component(
|
|
14
|
+
name: str,
|
|
15
|
+
display_name: str,
|
|
16
|
+
version: str,
|
|
17
|
+
description: str,
|
|
18
|
+
docs: str,
|
|
19
|
+
category: str,
|
|
20
|
+
tags: Optional[list[str]] = None,
|
|
21
|
+
author: str = "",
|
|
22
|
+
source_url: str = "",
|
|
23
|
+
) -> Callable[[Type[Component]], Type[Component]]:
|
|
24
|
+
"""
|
|
25
|
+
Class decorator that attaches ComponentMeta and registers the class
|
|
26
|
+
with the global ComponentRegistry.
|
|
27
|
+
|
|
28
|
+
Usage:
|
|
29
|
+
@component(
|
|
30
|
+
name="Resistor",
|
|
31
|
+
display_name="Resistor",
|
|
32
|
+
version="1.0.0",
|
|
33
|
+
description="Ideal resistor",
|
|
34
|
+
docs="Implements Ohm's Law: V = IR.",
|
|
35
|
+
category="Electrical/Passive",
|
|
36
|
+
)
|
|
37
|
+
class Resistor(Component):
|
|
38
|
+
...
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def decorator(cls: Type[Component]) -> Type[Component]:
|
|
42
|
+
cls.meta = ComponentMeta(
|
|
43
|
+
name=name,
|
|
44
|
+
display_name=display_name,
|
|
45
|
+
version=version,
|
|
46
|
+
description=description,
|
|
47
|
+
docs=docs,
|
|
48
|
+
category=category,
|
|
49
|
+
tags=tags or [],
|
|
50
|
+
author=author,
|
|
51
|
+
source_url=source_url,
|
|
52
|
+
)
|
|
53
|
+
ComponentRegistry.instance().register(cls)
|
|
54
|
+
return cls
|
|
55
|
+
|
|
56
|
+
return decorator
|