<!-- Source: https://docs.biosimulant.com/references/signals -->

# BioSignal & SignalSpec

The communication-step kernel uses a typed signal family plus declared port contracts.

## SignalSpec

Ports are declared as `name -> SignalSpec` mappings:

```python

def inputs(self):
    return {
        "current": biosim.SignalSpec.scalar(
            dtype="float64",
            max_age=0.001,
            stale_policy="error",
        ),
        "state_vector": biosim.SignalSpec.array(
            dtype="float32",
            shape=(10,),
        ),
        "state": biosim.SignalSpec.record(
            schema={"v": "float64", "u": "float64"},
        ),
        "spikes": biosim.SignalSpec.event(
            schema={"ids": "list[int]"},
        ),
    }
```

`SignalSpec` can declare:

- `signal_type`
- `kind`
- `dtype`
- `shape`
- `schema`
- `emitted_unit` on outputs
- `accepted_profiles` on inputs
- `interpolation`
- `max_age`
- `stale_policy`
- `description`

## Typed runtime signals

Do not instantiate `BioSignal` directly. Runtime payloads are one of:

- `ScalarSignal`
- `ArraySignal`
- `RecordSignal`
- `EventSignal`

A module that implements `execute()` can return raw values. BioWorld wraps each
one in the matching signal class using the declared output spec and stamps its
timestamp:

```python
def execute(self, inputs, *, context):
    return {"current": 12.5}
```

You only build typed signals by hand on the temporal compatibility path, where a
module implements `advance_window()` and returns signals from `get_outputs()`:

```python
def get_outputs(self):
    return {
        "current": biosim.ScalarSignal(
            source="stimulus",
            name="current",
            value=12.5,
            emitted_at=0.1,
            spec=self.outputs()["current"],
        )
    }
```

## Runtime semantics

- Signals preserve their source timestamp as `emitted_at`.
- State signals are delivered with hold-last-value semantics until replaced by a non-empty output mapping from the same source module.
- Event signals are delivered once per connection per source timestamp.
- `max_age` and `stale_policy` govern stale-read handling on input ports.

## On interpolation

`SignalSpec.interpolation` is currently a declared port policy, not a runtime interpolation engine. The world reads committed boundary values from the signal store. Use `zoh` or `none` as the runtime mental model today, even if a port spec declares `linear`.

## Receiving signals

`execute()` receives committed signals in `inputs`:

```python
def execute(self, inputs, *, context):
    current = inputs.get("current")
    if current is not None:
        self.I_ext = float(current.value)
    ...
```

Always treat inputs as optional. A port may have no committed upstream signal at a given boundary.

Modules on the temporal compatibility path get the same mapping through
`set_inputs(signals)` before `advance_window()` runs.

## Helper functions

`biosimulant.signals` includes small helpers for common adapter code:

- `unwrap_payload(value, max_depth=1)`: unwraps typed signal objects and one exact `{"payload": value}` carrier by default.
- `coerce_float(value, keys=("value", "count", "payload"))`: extracts a float from scalar-like signals and record carriers, returning `None` on invalid input.
- `scalar_or_record_input(unit, description, dtype="float64")`: declares a scalar input that also accepts a record payload carrier.
- `make_signal(spec, source, name, value, emitted_at)`: constructs `ScalarSignal`, `ArraySignal`, `RecordSignal`, or `EventSignal` from a `SignalSpec`.

```python
from biosimulant.signals import coerce_float, scalar_or_record_input

def inputs(self):
    return {"growth_rate": scalar_or_record_input("1/hour", "Growth rate.")}

def execute(self, inputs, *, context):
    value = coerce_float(inputs.get("growth_rate"))
    if value is not None:
        self.growth_rate = value
    ...
```

## See Also

- [BioModule API](/references/biomodule-api)
- [BioWorld API](/references/bioworld-api)
- [Artifact Outputs](/references/artifact-outputs)
- [How to Create a BioModule](/how-to/create-biomodule)
