> ## 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.

# Aggregating Input Ports

> Wire multiple connections to one input port and combine values with built-in or custom aggregation

By default, each input port accepts **one** incoming connection. Use `aggregating_input_port()` when several upstream outputs should feed the same input (for example summing branch currents at a node, averaging redundant temperature sensors, or applying custom combine logic).

## Fan-out vs fan-in

| Pattern     | Supported today           | How                                                                         |
| ----------- | ------------------------- | --------------------------------------------------------------------------- |
| **Fan-out** | Yes                       | One output → many inputs (connect the same source port to multiple targets) |
| **Fan-in**  | Yes, on aggregating ports | Many outputs → one input (declare `aggregating_input_port()` on the target) |

Regular `input_port()` ports still raise `DuplicateConnectionError` if you connect a second source.

## Built-in aggregation

```python theme={null}
from hardwave import Component, ComponentMeta, ComponentRegistry
from hardwave.ports import aggregating_input_port, output_port, PortAggregation

class CurrentSumNode(Component):
    meta = ComponentMeta(
        name="CurrentSumNode",
        display_name="Current Sum Node",
        version="1.0.0",
        description="Sums all incoming branch currents",
        docs="",
        category="Electrical",
    )
    ports = [
        aggregating_input_port(
            "current_in",
            "Current",
            aggregation=PortAggregation.SUM,
            description="Total current from all connected branches",
        ),
        output_port("total_current", "Current", "Net current into the node"),
    ]
    parameters = []

    def solve(self, inputs: dict) -> dict:
        return {"total_current": inputs["current_in"]}

ComponentRegistry.instance().register(CurrentSumNode)
```

### Built-in modes

| `PortAggregation` | Behaviour                                    |
| ----------------- | -------------------------------------------- |
| `SUM`             | Add all values                               |
| `MEAN`            | Arithmetic mean                              |
| `MIN` / `MAX`     | Minimum / maximum                            |
| `PRODUCT`         | Multiply all values                          |
| `FIRST` / `LAST`  | First or last value (connection order)       |
| `CUSTOM`          | Set automatically when you pass `aggregate=` |

Optional limits:

```python theme={null}
aggregating_input_port(
    "current_in",
    "Current",
    aggregation=PortAggregation.SUM,
    min_connections=2,   # validation error if fewer wires (unless external input)
    max_connections=4,   # raises MaxConnectionsExceededError at connect time
)
```

## Custom aggregation (callable wins entirely)

Pass an `aggregate` callable to fully control how values are combined. When present, it **replaces** any built-in `aggregation` mode. The engine passes the raw list of upstream values to your function.

```python theme={null}
from hardwave.ports import aggregating_input_port

def signed_current_sum(values, component, port):
    return sum(values)

def abs_mean(values, component, port):
    return sum(abs(v) for v in values) / len(values)

ports = [
    aggregating_input_port("current_in", "Current", aggregate=signed_current_sum),
]
```

### Aggregator signature

```python theme={null}
def my_aggregator(
    values: list,       # upstream values in connection order
    component: Component,
    port: Port,
) -> Any:
    ...
```

The return value is what `solve(inputs)` receives under `port.name`: always a **single** value, never a list.

## Wiring a graph

```python theme={null}
from hardwave.simulation import SimulationGraph, SimulationEngine
from hardwave.stdlib.components.passive import CurrentSource

graph = SimulationGraph()
graph.add_component(CurrentSource("src_a", param_values={"current": 1.0}))
graph.add_component(CurrentSource("src_b", param_values={"current": 2.5}))
graph.add_component(CurrentSumNode("node"))

graph.connect("src_a", "current_out", "node", "current_in")
graph.connect("src_b", "current_out", "node", "current_in")  # second wire: OK on aggregating ports

graph.validate()
result = SimulationEngine(graph).run(inputs={})
print(result.get_output("node", "total_current"))  # 3.5
```

## Disconnecting one source

```python theme={null}
# Remove all connections to a port
graph.disconnect("node", "current_in")

# Remove one specific source
graph.disconnect("node", "current_in", "src_a", "current_out")

# Inspect incoming connections
graph.connections_to("node", "current_in")
```

## Serialization

`Port.to_dict()` serializes the aggregation **mode** (`"sum"`, `"custom"`, etc.) but not the callable. Custom functions live on the component class at definition time (the same pattern as `solve()`). When you deserialize a graph with `SimulationGraph.from_dict()`, the registered component class supplies `aggregation_fn`.

## Composite components

Internal sub-graphs use the same aggregation rules. If two internal connections target the same aggregating input port, values are combined before the sub-component's solver runs.

## Related errors

| Error                         | Cause                                                              |
| ----------------------------- | ------------------------------------------------------------------ |
| `DuplicateConnectionError`    | Second wire to a regular `input_port()`                            |
| `MaxConnectionsExceededError` | More connections than `max_connections`                            |
| `INSUFFICIENT_CONNECTIONS`    | Fewer wires than `min_connections` at validation                   |
| `PortConfigurationError`      | `CUSTOM` aggregation declared without `aggregate=` at registration |
