> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hardwave.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Building Graphs

> Assemble simulation graphs, wire components, and validate connections in Hardwave

A `SimulationGraph` is a directed acyclic graph of component instances connected by typed ports. Hardwave validates wiring at construction time so type errors surface before any simulation runs.

## Adding components

```python theme={null}
from hardwave.simulation import SimulationGraph
from hardwave.stdlib.components.passive import VoltageSource, Resistor, VoltageRegulator

graph = SimulationGraph()

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}))
```

Each instance has a unique `instance_id` string used in connections and result queries.

## Connecting ports

```python theme={null}
graph.connect("psu",  "voltage_out",  "ldo",  "v_in")
graph.connect("ldo",  "v_out",        "load", "voltage")
```

Each `connect()` call creates a directed edge from an output port to an input port.

### Fan-out and fan-in

* **Fan-out**: connect one output to many inputs (always allowed).
* **Fan-in**: connect many outputs to one input only when the target port is declared with `aggregating_input_port()`.

```python theme={null}
# Fan-out: same voltage to two resistors
graph.connect("vs", "voltage_out", "r1", "voltage")
graph.connect("vs", "voltage_out", "r2", "voltage")

# Fan-in: two current sources into a summing node
graph.connect("src_a", "current_out", "node", "current_in")
graph.connect("src_b", "current_out", "node", "current_in")
```

## Connection rules

Hardwave enforces these rules 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`    | Second wire to a regular `input_port()`    |
| `MaxConnectionsExceededError` | More connections than `max_connections`    |

<Card title="Aggregating Input Ports" icon="merge" href="/guides/aggregating-ports">
  Declare multi-connection ports and custom aggregation
</Card>

## Validation

Always validate before running:

```python theme={null}
diagnostics = graph.validate(
    external_inputs={
        "ldo": {"load_current": 0.05},
    }
)
for d in diagnostics:
    print(d)  # warnings only; errors raise GraphValidationError
```

`validate()` checks that every required input port is either:

* Connected to an upstream output
* Has a default value
* Will be satisfied by an external input passed to `run()`

It also runs cycle detection (Kahn's algorithm). Cycles raise `GraphValidationError` before simulation starts. Aggregating ports with `min_connections` raise `INSUFFICIENT_CONNECTIONS` when too few wires are present.

<Warning>
  If you add extra components to a graph, every required port on **every** component must be satisfied at validation time, even if nothing is wired to that component's outputs.
</Warning>

## External inputs

Some ports are driven from outside the graph (ambient temperature, user commands, test fixtures):

```python theme={null}
result = engine.run(
    inputs={
        "ldo": {"load_current": 0.05},   # 50 mA load
    },
)
```

Pass the same external inputs to `validate()` so validation matches the run configuration.

## Editing graphs

```python theme={null}
graph.disconnect("load", "voltage")   # remove all connections feeding load.voltage
graph.disconnect("node", "current_in", "src_a", "current_out")  # remove one source
graph.connections_to("node", "current_in")  # inspect incoming wires
graph.remove_component("load")        # remove instance and all its connections
```

During `engine.run()`, Hardwave calls `graph.topological_order()` to determine a safe evaluation sequence. For each component it collects input values from upstream outputs (aggregating multi-connection ports when declared), external inputs, or port defaults, calls the solver, and stores outputs for downstream components.

## Next steps

<CardGroup cols={2}>
  <Card title="Steady-State Simulation" icon="equals" href="/simulation/steady-state">
    Solve the graph once and read scalar outputs
  </Card>

  <Card title="Saving Graphs" icon="floppy-disk" href="/graphs/saving">
    Serialize, reload, and save snapshots to the cloud dashboard
  </Card>
</CardGroup>
