BioSignal & SignalSpec
The communication-step kernel uses a typed signal family plus declared port contracts.
SignalSpec
Ports are declared as name -> SignalSpec mappings:
import biosimulant as biosim
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_typekinddtypeshapeschemaemitted_uniton outputsaccepted_profileson inputsinterpolationmax_agestale_policydescription
Typed runtime signals
Do not instantiate BioSignal directly. Emit one of:
ScalarSignalArraySignalRecordSignalEventSignal
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_ageandstale_policygovern 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
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, returningNoneon 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): constructsScalarSignal,ArraySignal,RecordSignal, orEventSignalfrom aSignalSpec.
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