Metadata-Version: 2.4
Name: hardwave
Version: 0.1.0
Summary: Hardware simulation and development platform
Home-page: https://github.com/HardwaveDev/hardwave
Author: Hardwave
Author-email: hello@hardwave.dev
License: MIT
Project-URL: Bug Reports, https://github.com/HardwaveDev/hardwave/issues
Project-URL: Source, https://github.com/HardwaveDev/hardwave
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.24.0
Requires-Dist: scipy>=1.10.0
Requires-Dist: scikit-learn>=1.3.0
Requires-Dist: pandas>=2.0.0
Requires-Dist: httpx>=0.27.0
Requires-Dist: cryptography>=42.0.0
Dynamic: author
Dynamic: author-email
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: license
Dynamic: license-file
Dynamic: project-url
Dynamic: requires-dist
Dynamic: requires-python
Dynamic: summary

# Hardwave

**Hardware simulation and development platform: design circuits and systems in Python before touching any hardware.**

Hardwave models hardware as a **dataflow graph of components**. Each component is a typed black box with input ports, output ports, and a solver that turns inputs into outputs. Connect components into a graph, run the simulation engine, and get results; from a simple voltage divider to a full robot drivetrain.

---

## Contents

1. [What Hardwave Does](#what-hardwave-does)
2. [Core Concepts](#core-concepts)
3. [Installation](#installation)
4. [Quick Start](#quick-start)
5. [Module Structure](#module-structure)
6. [Working with Types](#working-with-types)
7. [Defining Components](#defining-components)
8. [Building Simulation Graphs](#building-simulation-graphs)
9. [Running Simulations](#running-simulations)
10. [Composite Components](#composite-components)
11. [Solver System](#solver-system)
12. [Transient Simulation](#transient-simulation)
13. [Telemetry and ML Solvers](#telemetry-and-ml-solvers)
14. [Standard Library Reference](#standard-library-reference)
15. [Error Handling](#error-handling)
16. [Design Principles](#design-principles)

---

## What Hardwave Does

Hardware prototyping is expensive and slow. You design something, order parts, assemble it, test it, and discover a motor draws twice the expected current. Then you iterate. This cycle takes days and costs money.

Hardwave lets you close that loop in software:

- **Simulate** electrical, mechanical, and thermal behaviour before buying anything
- **Compose** components into systems; a resistor, a motor driver, a microcontroller, all with the same interface
- **Calibrate** simulation models against real telemetry collected from physical hardware
- **Swap solvers**, start with a formula, replace it with an ML model trained on real data, all without changing any wiring

---

## Core Concepts

| Concept | Description |
|---|---|
| **Type** | A named, physically-meaningful signal (e.g. `DCVoltage`, `Torque`, `Temperature`) |
| **Port** | A typed input or output slot on a component |
| **Component** | A node in the simulation graph with ports and a solver |
| **Solver** | The computation that transforms inputs to outputs (formula, ODE, lookup table, ML model) |
| **Composite** | A component built from other connected sub-components; externally identical to a primitive |
| **Graph** | A directed acyclic graph of connected component instances |
| **Engine** | Runs the graph; propagates values through it and returns a result |

Everything is a component. A resistor is a component. An Arduino is a component. A robot arm is a component. The abstraction never breaks.

---

## Installation

**Requirements:** Python 3.11+

```bash
pip install hardwave
```

Or install dependencies directly:

```bash
pip install numpy scipy scikit-learn pandas
```

Then in Python:

```python
import hardwave           # core platform
import hardwave.stdlib    # standard library (types + components)
```

Importing `hardwave.stdlib` registers all 30 built-in types and 28 primitive components with the global registries. You do not need to import submodules individually.

---

## Quick Start

### Simulate a Voltage Divider

```python
import hardwave
import hardwave.stdlib

from hardwave.simulation import SimulationGraph, SimulationEngine
from hardwave.stdlib.components.passive import VoltageSource, Resistor

# Build the graph
graph = SimulationGraph()
graph.add_component(VoltageSource("vs", param_values={"voltage": 9.0}))
graph.add_component(Resistor("r1", param_values={"resistance": 1_000.0}))
graph.add_component(Resistor("r2", param_values={"resistance": 2_000.0}))

graph.connect("vs", "voltage_out", "r1", "voltage")
graph.connect("vs", "voltage_out", "r2", "voltage")

# Validate, then simulate
graph.validate()
result = SimulationEngine(graph).run(inputs={})

print(result.get_output("r1", "current"))   # → 0.009  A  (9 mA)
print(result.get_output("r2", "current"))   # → 0.0045 A  (4.5 mA)
print(result.get_output("r1", "power"))     # → 0.081  W
```

The simulation matches hand calculation: I = V / R = 9 / 1000 = 9 mA, exactly.

---

## Module Structure

```
hardwave/
├── types/           HardwaveType base class + TypeRegistry singleton
├── ports/           Port, PortDirection, input_port(), output_port()
├── components/      Component ABC, ComponentMeta, Parameter, @component decorator
│   ├── composite.py CompositeComponent, BuildContext, Connection
│   └── registry.py  ComponentRegistry singleton
├── solvers/         Solver ABC + FormulaSolver, ODESolver, LookupTableSolver, MLModelSolver
├── simulation/      SimulationGraph, SimulationEngine, SimulationConfig, SimulationResult
├── visualization/   VisualizationSpec, VisualizationKind
├── telemetry/       TelemetryRecord, TelemetryStore, ModelTrainer
├── serialization/   Serializable protocol, schema version constants
├── diagnostics/     Diagnostic, DiagnosticLevel, full exception hierarchy
└── stdlib/
    ├── types/       30 pre-registered hardware signal types
    └── components/  28 pre-built primitive components
```

---

## Working with Types

Types define the **semantic meaning** of signals. `DCVoltage` and `Current` are both floats at runtime, but Hardwave treats them as incompatible, you cannot accidentally wire a voltage output into a current input.

### Built-in Types (selected)

```python
import hardwave.stdlib

from hardwave.types import TypeRegistry
reg = TypeRegistry.instance()

reg.get("DCVoltage")       # V, min=-10000, max=10000
reg.get("Current")         # A
reg.get("Resistance")      # Ω, min=0
reg.get("Power")           # W
reg.get("Torque")          # N·m
reg.get("AngularVelocity") # rad/s
reg.get("Temperature")     # °C, min=-273.15
reg.get("PWMSignal")       # dict {frequency_hz, duty_cycle}
reg.get("ControlSignal")   # dimensionless, 0.0–1.0

# List everything
for t in reg.list_types():
    print(f"{t.name:25s} {t.unit}")
```

### Defining a Custom Type

```python
from hardwave.types import HardwaveType, TypeRegistry

Luminosity = HardwaveType(
    name="Luminosity",
    unit="lux",
    description="Illuminance",
    min_value=0.0,
)
TypeRegistry.instance().register(Luminosity)
```

### Composite / Bus Types

```python
from dataclasses import dataclass
from typing import Any, Type
from hardwave.types import HardwaveType, TypeRegistry

@dataclass
class CANFrame(HardwaveType):
    name: str = "CANFrame"
    unit: str = "-"
    description: str = "CAN bus frame payload"
    python_type: Type = dict

    def validate(self, value: Any) -> bool:
        return (
            isinstance(value, dict)
            and "arbitration_id" in value
            and "data" in value
        )

TypeRegistry.instance().register(CANFrame())
```

---

## Defining Components

The simplest way to define a component is with the `@component` decorator:

```python
from hardwave import component, input_port, output_port, Component

@component(
    name="Thermistor",
    display_name="NTC Thermistor",
    version="1.0.0",
    description="NTC thermistor: resistance decreases with temperature",
    docs="Uses the Steinhart-Hart equation approximation.",
    category="Sensors",
    tags=["sensor", "temperature", "thermistor"],
)
class Thermistor(Component):
    ports = [
        input_port("temperature", "Temperature", "Ambient temperature (°C)"),
        output_port("resistance", "Resistance", "Thermistor resistance at given temperature"),
    ]

    parameters = [
        Parameter("r_nominal",  "Nominal Resistance", "Ω",   float, 10_000.0),
        Parameter("t_nominal",  "Nominal Temperature", "°C", float, 25.0),
        Parameter("beta",       "Beta Coefficient",    "K",  float, 3950.0),
    ]

    def solve(self, inputs: dict) -> dict:
        import math
        T_c = inputs["temperature"]
        T_k = T_c + 273.15
        T0 = self.get_param("t_nominal") + 273.15
        R0 = self.get_param("r_nominal")
        B  = self.get_param("beta")
        R  = R0 * math.exp(B * (1 / T_k - 1 / T0))
        return {"resistance": R}
```

The decorator automatically:
- Attaches `ComponentMeta` to the class
- Registers the class with `ComponentRegistry`

### Manual definition (without decorator)

```python
from hardwave.components import Component, ComponentMeta, ComponentRegistry, Parameter
from hardwave.ports import input_port, output_port

class MyGasSensor(Component):
    meta = ComponentMeta(
        name="MyGasSensor",
        display_name="Custom Gas Sensor",
        version="1.0.0",
        description="Reads CO2 concentration",
        docs="Returns PPM concentration from a voltage signal.",
        category="Sensors/Gas",
    )
    ports = [
        input_port("analog_in", "AnalogSignal", "Sensor output voltage (0–5V)"),
        output_port("co2_ppm", "AnalogSignal",  "CO2 concentration in PPM"),
    ]
    parameters = [
        Parameter("sensitivity", "Sensitivity", "V/PPM", float, 0.002),
    ]

    def solve(self, inputs: dict) -> dict:
        V = inputs["analog_in"]
        ppm = V / self.get_param("sensitivity")
        return {"co2_ppm": ppm}

ComponentRegistry.instance().register(MyGasSensor)
```

---

## Building Simulation Graphs

```python
from hardwave.simulation import SimulationGraph

graph = SimulationGraph()

# Add component instances
graph.add_component(VoltageSource("psu",  param_values={"voltage": 12.0}))
graph.add_component(VoltageRegulator("ldo", param_values={"output_voltage": 5.0}))
graph.add_component(Resistor("load", param_values={"resistance": 100.0}))

# Wire them together
graph.connect("psu",  "voltage_out",  "ldo",  "v_in")
graph.connect("ldo",  "v_out",        "load", "voltage")
# load_current must be provided externally or have a default

# Validate before running
diagnostics = graph.validate()
for d in diagnostics:
    print(d)   # warnings only; errors raise GraphValidationError
```

### Connection rules enforced at wiring time

| Error | Cause |
|---|---|
| `PortNotFoundError` | Port name does not exist on that component |
| `PortDirectionError` | Connecting output→output or input→input |
| `TypeMismatchError` | Source and target types are incompatible |
| `DuplicateConnectionError` | Input port already has a connected source |

### Disconnect and remove

```python
graph.disconnect("load", "voltage")          # remove connection feeding load.voltage
graph.remove_component("load")               # remove instance and all its connections
```

---

## Running Simulations

### Steady-state

```python
from hardwave.simulation import SimulationEngine, SimulationConfig
from hardwave.solvers import SimulationDomain

engine = SimulationEngine(graph)

result = engine.run(
    inputs={
        "ldo": {"load_current": 0.05},   # 50 mA load
    },
    config=SimulationConfig(domain=SimulationDomain.STEADY_STATE),
)

print(result.get_output("ldo",  "v_out"))        # 5.0
print(result.get_output("ldo",  "power_loss"))   # (12-5)*0.05 = 0.35 W
print(result.get_output("load", "current"))      # 0.05
```

### Accessing results

```python
# Scalar output
value = result.get_output("instance_id", "port_name")

# Dict (JSON-serializable)
d = result.to_dict()

# Simulation warnings
for diag in result.diagnostics():
    print(diag)
```

---

## Composite Components

A **composite component** wraps an internal sub-graph. From the outside it looks identical to a primitive.

```python
from hardwave.components import (
    CompositeComponent, BuildContext, Connection,
    ComponentMeta, ComponentRegistry, Parameter,
)
from hardwave.ports import input_port, output_port
from hardwave.stdlib.components.passive import Resistor

class VoltageDivider(CompositeComponent):
    meta = ComponentMeta(
        name="VoltageDivider",
        display_name="Voltage Divider",
        version="1.0.0",
        description="Two-resistor voltage divider",
        docs="Splits an input voltage across two resistors.",
        category="Electrical/Passive",
    )
    ports = [
        input_port("v_in",  "DCVoltage", "Input voltage"),
        output_port("i_r1", "Current",   "Current through R1"),
        output_port("i_r2", "Current",   "Current through R2"),
    ]
    parameters = [
        Parameter("r1", "R1", "Ω", float, 1_000.0),
        Parameter("r2", "R2", "Ω", float, 2_000.0),
    ]

    def build(self, ctx: BuildContext) -> None:
        ctx.add(Resistor("r1", param_values={"resistance": self.get_param("r1")}))
        ctx.add(Resistor("r2", param_values={"resistance": self.get_param("r2")}))

        # Fan the external input to both internal ports
        ctx.map_input("v_in", "r1", "voltage")
        ctx.map_input("v_in", "r2", "voltage")

        # Expose internal outputs externally
        ctx.map_output("i_r1", "r1", "current")
        ctx.map_output("i_r2", "r2", "current")

        # Internal connections (none needed here, both take the same input)

ComponentRegistry.instance().register(VoltageDivider)

# Use it like any other component
graph = SimulationGraph()
graph.add_component(VoltageSource("vs", param_values={"voltage": 9.0}))
graph.add_component(VoltageDivider("div"))
graph.connect("vs", "voltage_out", "div", "v_in")
result = SimulationEngine(graph).run(inputs={})
print(result.get_output("div", "i_r1"))  # 0.009 A
print(result.get_output("div", "i_r2"))  # 0.0045 A
```

Internal sub-components can also be connected to each other using `ctx.connect(Connection(...))` for more complex topologies.

---

## Solver System

Every component has a **solver** that computes outputs from inputs. The default solver is the component's own `solve()` method, but you can inject a more sophisticated one at any time without changing the component's interface.

### `FormulaSolver` - wrap any callable

```python
from hardwave.solvers import FormulaSolver

def my_formula(inputs, component):
    V = inputs["voltage"]
    R = component.get_param("resistance")
    return {"current": V / R, "power": V**2 / R}

r = Resistor("r1", param_values={"resistance": 470.0})
r.set_solver(FormulaSolver(my_formula))
```

### `LookupTableSolver` - interpolate measured data

```python
from hardwave.solvers import LookupTableSolver

# Motor torque-speed curve from datasheet
table = {
    "speed_rpm": [0,   500,  1000, 1500, 2000, 2500, 3000],
    "torque_nm": [1.2, 1.15, 1.05, 0.90, 0.70, 0.42, 0.0],
    "efficiency": [0.0, 0.55, 0.72, 0.80, 0.81, 0.75, 0.0],
}

motor.set_solver(LookupTableSolver(
    table,
    input_port="speed_rpm",
    output_ports=["torque_nm", "efficiency"],
    method="linear",   # or "cubic", "nearest"
))
```

### `ODESolver` - physics-based transient models

```python
from hardwave.solvers import ODESolver

# RC circuit: dV/dt = (V_in - V_c) / (R * C)
def state_fn(t, state, inputs, comp):
    R, C = comp.get_param("resistance"), comp.get_param("capacitance")
    return {"V_c": (inputs["v_in"] - state["V_c"]) / (R * C)}

def output_fn(t, state, inputs, comp):
    return {"v_out": state["V_c"]}

rc_solver = ODESolver(
    state_fn=state_fn,
    output_fn=output_fn,
    initial_state={"V_c": 0.0},
    integrator="rk4",          # "euler" | "rk4" | "scipy_ivp"
)
my_cap.set_solver(rc_solver)
```

### `MLModelSolver` - learned from data

See [Telemetry and ML Solvers](#telemetry-and-ml-solvers) below.

---

## Transient Simulation

Transient simulation steps through time and returns a time series for every output port. Built-in components like `DCMotor`, `Capacitor`, `Inductor`, and `LiPoCell` use `ODESolver` internally and automatically participate in transient runs.

```python
from hardwave.simulation import SimulationGraph, SimulationEngine, SimulationConfig
from hardwave.solvers import SimulationDomain
from hardwave.stdlib.components.motors import DCMotor

graph = SimulationGraph()
graph.add_component(DCMotor("motor", param_values={
    "winding_resistance": 2.0,
    "winding_inductance": 5e-4,
    "back_emf_constant":  0.05,
    "rotor_inertia":      1e-4,
}))

engine = SimulationEngine(graph)
config = SimulationConfig(
    domain=SimulationDomain.TRANSIENT,
    t_start=0.0,
    t_end=1.0,
    dt=0.0001,        # 0.1 ms - use small dt for stiff electrical dynamics
)
engine.configure(config)

result = engine.run(
    inputs={"motor": {"voltage": 12.0, "load_torque": 0.2}},
    config=config,
)

# Time series as numpy arrays
t, omega = result.get_time_series("motor", "angular_velocity")
t, current = result.get_time_series("motor", "current")

print(f"Final speed:   {omega[-1]:.1f} rad/s")
print(f"Final current: {current[-1]*1000:.1f} mA")

# Export to pandas for plotting
df = result.to_dataframe()
print(df.head())
# Columns: time, motor.angular_velocity, motor.current, motor.winding_temp

# Serialize the result
import json
print(json.dumps(result.to_dict(), indent=2)[:300])
```

### Interactive / real-time step mode

```python
state = {}
for step in range(1000):
    t = step * 0.001
    outputs, state = engine.step(
        inputs={"motor": {"voltage": 12.0, "load_torque": 0.2}},
        t=t,
        state=state,
    )
    omega = outputs["motor"]["angular_velocity"]
    # Send omega to a display, controller, etc.
```

---

## Telemetry and ML Solvers

### Recording telemetry

```python
from hardwave.telemetry import TelemetryRecord, TelemetryStore

store = TelemetryStore()

# Log a measurement from a physical motor
store.append(TelemetryRecord(
    component_class="DCMotor",
    timestamp=1_700_000_000.0,
    inputs={"voltage": 12.0, "load_torque": 0.3},
    outputs={"angular_velocity": 190.5, "winding_temp": 42.1},
    environment={"ambient_temp": 22.0},
    source="user",
))

# Components log their own telemetry automatically during simulation
# and can be read back:
motor_instance.export_telemetry()   # list of dicts
```

### Training an ML solver

Once you have collected enough real-world data you can train a model to replace the physics formula:

```python
from hardwave.telemetry import ModelTrainer

trainer = ModelTrainer(store, component_class="DCMotor")

solver = trainer.train(
    input_ports=["voltage", "load_torque"],
    output_ports=["angular_velocity", "winding_temp"],
    min_samples=500,
    model_type="gradient_boosting",   # "linear" | "gradient_boosting" | "neural_net"
)

# Evaluate on a held-out test set
metrics = trainer.evaluate(solver, test_fraction=0.2)
print(metrics["overall_r2"])   # e.g. 0.998

# Inject into any motor instance, no other code changes needed
motor.set_solver(solver)
```

### Export / import telemetry

```python
# Save to newline-delimited JSON
store.export_jsonl("DCMotor", "data/dc_motor_telemetry.jsonl")

# Load back
store2 = TelemetryStore.from_jsonl("data/dc_motor_telemetry.jsonl")

# Convert to pandas DataFrame for analysis
df = store.to_dataframe("DCMotor")
# Columns: timestamp, component_variant, instance_id, source,
#          input.voltage, input.load_torque, output.angular_velocity, ...
```

---

## Standard Library Reference

### Types (30 registered)

#### Electrical
| Name | Unit | Description |
|---|---|---|
| `DCVoltage` | V | DC electrical potential |
| `ACVoltage` | V_rms | AC RMS voltage |
| `Current` | A | Electrical current |
| `Resistance` | Ω | Electrical resistance |
| `Capacitance` | F | Capacitance |
| `Inductance` | H | Inductance |
| `Power` | W | Electrical power |
| `Frequency` | Hz | Signal frequency |
| `AnalogSignal` | V | Generic analog 0–5V |
| `PWMSignal` | — | dict `{frequency_hz, duty_cycle}` |
| `DigitalSignal` | — | 0 or 1 |
| `I2CBus` | — | dict `{sda, scl, address, data}` |
| `SPIBus` | — | dict `{mosi, miso, sclk, cs}` |
| `UARTStream` | — | dict `{baud_rate, data}` |

#### Mechanical
| Name | Unit |
|---|---|
| `Torque` | N·m |
| `AngularVelocity` | rad/s |
| `AngularPosition` | rad |
| `LinearForce` | N |
| `LinearVelocity` | m/s |
| `LinearPosition` | m |
| `Pressure` | Pa |
| `MechanicalPower` | W |

#### Thermal
| Name | Unit |
|---|---|
| `Temperature` | °C |
| `HeatFlux` | W/m² |
| `ThermalResistance` | °C/W |

#### Control & Data
| Name | Unit |
|---|---|
| `ControlSignal` | — (0.0–1.0) |
| `BooleanSignal` | — |
| `Timestamp` | s |
| `ByteArray` | — |
| `JSONPayload` | — |

---

### Components (28 registered)

#### Electrical / Passive
| Component | Key Ports | Solver |
|---|---|---|
| `Resistor` | voltage → current, power | Formula (Ohm's Law) |
| `Capacitor` | current → voltage, stored_energy | ODE (dV/dt = I/C) |
| `Inductor` | voltage → current, stored_energy | ODE (dI/dt = V/L) |
| `Diode` | anode_voltage, cathode_voltage → current, power_loss | Formula (Shockley) |
| `VoltageSource` | set_voltage → voltage_out | Formula |
| `CurrentSource` | set_current → current_out | Formula |
| `Ground` | — → voltage (0V) | Formula |

#### Electrical / Active
| Component | Key Ports | Solver |
|---|---|---|
| `NPNTransistor` | base_current, vce → collector_current, power | Formula (piecewise) |
| `NMOSFET` | gate_voltage, vds → drain_current, power | Formula (square-law) |
| `OpAmp` | v_plus, v_minus, supply → v_out | Formula (ideal, rail-clamped) |
| `VoltageRegulator` | v_in, load_current → v_out, power_loss | Formula |

#### Electromechanical
| Component | Key Ports | Solver |
|---|---|---|
| `DCMotor` | voltage, load_torque → angular_velocity, current, winding_temp | ODE (coupled E-M-T) |
| `StepperMotor` | step_pulses, load_torque → angular_position, current | Discrete |
| `ServoMotor` | pwm_signal, load_torque → angular_position, current | Discrete |
| `BrushlessMotor` | phase_voltages, load_torque → angular_velocity, phase_currents | ODE |
| `LinearActuator` | pwm_signal, load_force → linear_position, current | ODE |

#### Sensors
| Component | Key Ports | Solver |
|---|---|---|
| `TemperatureSensor` | ambient_temperature → analog_out | Formula + Gaussian noise |
| `IMU` | linear_acceleration, angular_velocity → i2c_out | Formula |
| `Encoder` | angular_position → pulse_count, angular_velocity | Formula |
| `UltrasonicSensor` | target_distance → pulse_out | Formula + noise + latency |
| `Camera` | scene_description → image_frame | Placeholder (V2) |

#### Microcontrollers
| Component | Key Ports | Solver |
|---|---|---|
| `Arduino` | gpio_inputs → gpio_outputs, uart | Firmware callable (signal-level) |
| `ESP32` | gpio_inputs, wifi_rx → gpio_outputs, wifi_tx, uart | Firmware callable |
| `RaspberryPi` | gpio_inputs → gpio_outputs, uart, i2c, spi | Firmware callable |

> MCU firmware is modelled at the signal level; what values appear on pins at each time step. Actual code execution is a V2 feature. Pass a Python callable as the `firmware` parameter to drive GPIO outputs.

#### Power
| Component | Key Ports | Solver |
|---|---|---|
| `LiPoCell` | charge_current, discharge_current → terminal_voltage, soc, temperature | ODE |
| `BatteryPack` | charge_current, discharge_current → terminal_voltage, capacity_ah | Composite |
| `BuckConverter` | v_in, duty_cycle, load_current → v_out, efficiency | Formula |
| `BoostConverter` | v_in, duty_cycle, load_current → v_out, efficiency | Formula |

---

## Error Handling

Hardwave uses a typed exception hierarchy:

```
HardwaveError
├── TypeRegistrationError      # Registering a type name that already exists
├── TypeNotFoundError          # Looking up a type not in the registry
├── ComponentRegistrationError # Duplicate component name or invalid class
├── ComponentNotFoundError     # Looking up a component not in the registry
├── PortNotFoundError          # Port name not found on component
├── TypeMismatchError          # Incompatible types on a connection
├── PortDirectionError         # Connecting input→input or output→output
├── DuplicateConnectionError   # Input port already has a source
├── GraphValidationError       # Graph fails validation (carries list of Diagnostics)
├── SolverError                # Solver computation failure
├── SimulationError            # Engine-level failure
├── SimulationResultError      # Accessing time-series on a steady-state result
└── DeserializationError       # Corrupt or incompatible serialized data
```

`GraphValidationError` carries a full list of `Diagnostic` objects:

```python
from hardwave.diagnostics import GraphValidationError, DiagnosticLevel

try:
    graph.validate()
except GraphValidationError as e:
    for d in e.diagnostics:
        print(f"[{d.level.value}] {d.code}: {d.message}")
        if d.suggestion:
            print(f"  → {d.suggestion}")
```

---

## Design Principles

1. **Everything is a component.** A resistor, an Arduino, a robot arm; the abstraction never breaks.
2. **Components are defined in code.** Python classes are the source of truth; a UI layer wraps them.
3. **Solvers are pluggable.** Swap a formula for a lookup table or an ML model without touching any wiring.
4. **Strict typing at the boundary.** Type errors are caught at graph construction time, not at simulation time.
5. **Composability is first-class.** A composite component is indistinguishable from a primitive at its interface.
6. **Simulation semantics are explicit.** Time domain is declared per solver, not inferred.
7. **Data drives fidelity.** Components start as formulas and grow into empirically-calibrated ML models.

---

## Development

```bash
# Install in editable mode (includes all dependencies)
pip install -e .

# Run a quick sanity check
python -c "import hardwave; import hardwave.stdlib; print('OK')"
```
