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.
Files changed (54) hide show
  1. hardwave/__init__.py +118 -0
  2. hardwave/components/__init__.py +15 -0
  3. hardwave/components/base.py +253 -0
  4. hardwave/components/composite.py +237 -0
  5. hardwave/components/decorators.py +56 -0
  6. hardwave/components/registry.py +160 -0
  7. hardwave/diagnostics/__init__.py +37 -0
  8. hardwave/diagnostics/diagnostic.py +119 -0
  9. hardwave/ports/__init__.py +3 -0
  10. hardwave/ports/port.py +117 -0
  11. hardwave/premium/__init__.py +224 -0
  12. hardwave/premium/client.py +291 -0
  13. hardwave/premium/exceptions.py +29 -0
  14. hardwave/premium/loader.py +140 -0
  15. hardwave/premium/solver.py +44 -0
  16. hardwave/serialization/__init__.py +3 -0
  17. hardwave/serialization/schema.py +25 -0
  18. hardwave/simulation/__init__.py +10 -0
  19. hardwave/simulation/engine.py +310 -0
  20. hardwave/simulation/graph.py +450 -0
  21. hardwave/simulation/result.py +135 -0
  22. hardwave/solvers/__init__.py +14 -0
  23. hardwave/solvers/base.py +59 -0
  24. hardwave/solvers/formula.py +28 -0
  25. hardwave/solvers/lookup.py +79 -0
  26. hardwave/solvers/ml.py +80 -0
  27. hardwave/solvers/ode.py +153 -0
  28. hardwave/stdlib/__init__.py +7 -0
  29. hardwave/stdlib/components/__init__.py +7 -0
  30. hardwave/stdlib/components/active.py +267 -0
  31. hardwave/stdlib/components/mcu.py +205 -0
  32. hardwave/stdlib/components/motors.py +432 -0
  33. hardwave/stdlib/components/passive.py +433 -0
  34. hardwave/stdlib/components/power.py +313 -0
  35. hardwave/stdlib/components/sensors.py +263 -0
  36. hardwave/stdlib/types/__init__.py +17 -0
  37. hardwave/stdlib/types/control.py +70 -0
  38. hardwave/stdlib/types/electrical.py +193 -0
  39. hardwave/stdlib/types/mechanical.py +70 -0
  40. hardwave/stdlib/types/thermal.py +37 -0
  41. hardwave/telemetry/__init__.py +5 -0
  42. hardwave/telemetry/record.py +61 -0
  43. hardwave/telemetry/store.py +108 -0
  44. hardwave/telemetry/trainer.py +196 -0
  45. hardwave/types/__init__.py +4 -0
  46. hardwave/types/base.py +73 -0
  47. hardwave/types/registry.py +78 -0
  48. hardwave/visualization/__init__.py +3 -0
  49. hardwave/visualization/spec.py +60 -0
  50. hardwave-0.1.0.dist-info/METADATA +822 -0
  51. hardwave-0.1.0.dist-info/RECORD +54 -0
  52. hardwave-0.1.0.dist-info/WHEEL +5 -0
  53. hardwave-0.1.0.dist-info/licenses/LICENSE +21 -0
  54. 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