<!-- Source: https://docs.biosimulant.com/references/library/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. Emit one of:

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

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

```python
def set_inputs(self, signals):
    current = signals.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.

## 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 set_inputs(self, signals):
    value = coerce_float(signals.get("growth_rate"))
    if value is not None:
        self.growth_rate = value
```

## See Also

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