Bioware interface
sc_neurocore.bioware is an experimental research interface for moving a
finite multi-electrode-array (MEA) frame through spike detection, Address-Event
Representation (AER), stochastic bitstream encoding, and an optogenetic pulse
proposal. It is not a medical device, a clinical controller, or a tissue-safety
certification surface.
The maintained implementation is Python-only. Earlier generated Go, Julia,
Mojo, and Rust files were non-executable placeholders with no dispatch path and
have been removed. The separately maintained Julia plasticity solvers remain
documented in Julia solvers; they are not a second
implementation of this closed-loop orchestration.
Pythonfrom sc_neurocore.bioware import (
AERToSCConverter,
BioHybridSession,
CultureHealth,
MEAConfig,
MEAToAERTranscoder,
SCToOptoEncoder,
SpikeDetector,
)
Maintained boundaries
The historical module
sc_neurocore.bioware.bioware is a compatibility facade. Implementations are
owned by focused modules:
| Module |
Responsibility |
bioware_contracts.py |
Stable MEA, spike, AER, pulse, and frame-result records |
bioware_validation.py |
Shared finite-value, shape, integer, and bitstream guards |
bioware_acquisition.py |
MAD noise estimation, threshold detection, sorting, artifact blanking |
bioware_encoding.py |
MEA→AER, AER→SC, SC→opto, and rate decoding |
bioware_plasticity.py |
Pair-STDP, BCM, and homeostatic Q8.8 adapters |
bioware_analysis.py |
Culture heuristic, LFP bands, latency, and network bursts |
bioware_experiment.py |
Pharmacology prototype and multi-well metadata |
bioware_audit.py |
Deterministic in-memory audit records and checksum |
bioware_session.py |
One-frame closed-loop orchestration |
bioware_fitness.py |
Legacy evolutionary fitness adapter |
Package-root objects, historical qualified names, and pickle lookup paths are
preserved. The facade contains no implementation definitions.
Signal contract
MEA frame and spike detection
Input must be a non-empty, finite numeric NumPy matrix with shape
(samples, channels). Its channel count must exactly match MEAConfig.
For channel (c), the detector estimates a robust noise scale
[
\hat\sigma_c = \frac{\operatorname{median}t |V
]}|}{0.6745
and detects crossings of
[
|V_{t,c}| > \alpha \hat\sigma_c,
]
where spike_threshold_sigma is (alpha). The 0.6745 scaling is the
standard Gaussian MAD conversion used in extracellular spike detection; see
the primary-culture methods discussion in
Maccione et al..
The refractory interval is enforced independently per channel. Waveform
snippets are edge-padded to a fixed length.
SpikeSorter is a deliberately small PCA + K-Means research adapter. It uses a
fixed random_state=0 by default, validates uniform waveform lengths, and
requires the optional scikit-learn dependency only when enough waveforms are
available to fit clusters.
AER epoch
MEAToAERTranscoder converts a spike timestamp and explicit origin to a clock
tick:
[
\tau = \left\lfloor (t_{spike} - t_0) f_{hw} \right\rfloor.
]
The maintained packet has a 16-bit unsigned timestamp. Therefore
(0 \le \tau \le 65535); negative relative time and overflow are errors.
The implementation never silently wraps timestamps. At the default 1 MHz
clock, one frame must fit within approximately 65.536 ms. Longer recordings
must be split into explicit epochs by the caller.
BioHybridSession treats detector timestamps as frame-relative. Its
t_start_s argument is non-negative experiment time for optional experiment
models; it does not change the frame-local AER origin.
AER to stochastic bitstreams
For valid events inside window_ticks, the converter counts events per neuron.
If (k_n) is the count for neuron (n), it uses
[
p_n = \frac{k_n}{\max_j k_j}.
]
p_n is encoded by a deterministic 16-bit LFSR into a bitstream of length
bitstream_length (default 256). This is not an IID Bernoulli sampler and does
not use a smoothing constant. The configured neuron count and window are hard
bounds rather than unused metadata.
Stochastic bitstreams to optical pulses
For bitstream density (d_n), the encoder proposes
[
I_n = d_n I_{max}, \qquad
T_n = T_{min} + d_n(T_{max} - T_{min}).
]
intensity_mw_mm2 is irradiance. Optical power is computed with the illuminated
area:
[
P_n\,[\mathrm{mW}] = I_n\,[\mathrm{mW/mm^2}]\,A_n\,[\mathrm{mm^2}].
]
The encoder accumulates (P_n) and omits a channel if including it would exceed
max_total_power_mw. This software budget is a consistency guard, not a
biological safety limit. Real optical safety depends on wavelength, duty cycle,
geometry, absorption, scattering, thermal transport, and experimental
calibration.
Plasticity adapters
Pair STDP
BiologicalSTDP implements the parameterised exponential pair rule
[
\Delta w =
\begin{cases}
A_+e^{-\Delta t/\tau_+}, & \Delta t > 0,\
-A_-e^{\Delta t/\tau_-}, & \Delta t < 0,\
0, & \Delta t = 0.
\end{cases}
]
Defaults are tau_plus_ms = tau_minus_ms = 20, a_plus = 0.01, and
a_minus = 0.012. These are configurable model parameters, not claimed fits to
one specific preparation. The biological timing reference is
Bi and Poo (1998).
BCM and homeostasis
BCMPlasticity uses
[
\Delta w = \eta x y(y-\theta), \qquad
\theta \leftarrow \theta + \frac{\Delta t}{\tau_\theta}(y^2-\theta),
]
following the sliding-threshold family introduced by
Bienenstock, Cooper, and Munro.
HomeostaticPlasticity applies a bounded Q8.8 proportional update:
[
\theta_{next}^{q88} = \operatorname{clip}\left(
\theta^{q88} + \left\lfloor
\frac{\Delta t}{\tau_h}(r-r^*)\,256
\right\rfloor,\theta_{min}^{q88},\theta_{max}^{q88}\right).
]
BioHybridSession stores stdp and optional homeostatic policies for caller
coordination, but process_frame does not update plasticity implicitly.
Session semantics
BioHybridSession.process_frame executes these stages synchronously:
- validate the frame, experiment time, AER epoch, and converter window;
- optionally blank stimulus artifacts;
- detect and optionally sort spikes;
- optionally apply the pharmacology rate prototype;
- transcode to AER and deterministic SC bitstreams;
- optionally pass decoded rates to an ArcaneZenith object;
- propose optogenetic pulses and calculate a culture-health snapshot;
- create a typed
BioHybridFrameResult, record optional latency, and only
then advance round_count.
An exception before completion leaves round_count unchanged. The detector may
still retain its internal noise estimate, and an external Zenith object may
have its own state; callers needing distributed transactions must manage those
resources explicitly.
Pythonimport numpy as np
config = MEAConfig(num_channels=8, sample_rate_hz=20_000.0)
session = BioHybridSession(
mea_config=config,
detector=SpikeDetector(config),
transcoder=MEAToAERTranscoder(hw_clock_hz=1e6),
sc_converter=AERToSCConverter(
window_ticks=0x10000,
bitstream_length=512,
num_neurons=config.num_channels,
),
opto_encoder=SCToOptoEncoder(
illuminated_area_mm2=1.0,
max_total_power_mw=50.0,
),
)
# 1,000 / 20,000 Hz = 50 ms, inside one 16-bit epoch at 1 MHz.
frame = np.zeros((1_000, config.num_channels), dtype=np.float64)
result = session.process_frame(frame, t_start_s=0.0)
assert result.round == result["round"] == 1
Analysis, experiment, and audit limits
CultureHealth returns a bounded aggregate heuristic from channel rates. It
is not a viability assay or clinical endpoint.
extract_lfp_power is an FFT band-power helper, not a full spectral
estimator with windowing, leakage correction, or uncertainty intervals.
detect_network_bursts is a binned threshold heuristic.
PharmModel implements application time, onset interpolation, and a firing
gain. wash_time_s is reserved configuration and is not yet applied as a
washout curve.
BioAuditLog is an ordered, tamper-evident in-memory record. Its canonical
SHA-256 includes schema name, experiment identity, and entries. It does not
provide durable storage, signatures, access control, or regulatory
compliance.
- The legacy fitness key
energy_mw equals 0.5 * spike_count for compatibility.
It is a dimensionless optimisation proxy, not measured power or energy.
Verification and benchmark evidence
Focused verification executes 200 tests and covers all 1,052 production
statements and 384 branches in the Bioware package. Source and tests are
responsibility-sized, the module import graph is acyclic, the facade and package
objects are identical, and historical pickle paths remain valid.
benchmarks/results/bench_bioware.json is a 30-sample, interleaved comparison
of parent c4e492ff5 and the modular working tree. Both produced exactly 6,865
canonical bytes with SHA-256
2491dc73a2de93a45a1cc944539c170b151403e42b973b18806143f318b7d669:
| Local diagnostic metric |
Parent median |
Modular candidate median |
Delta |
| Pipeline |
2.801 ms |
3.345 ms |
+19.41% |
| Import |
34.771 ms |
50.265 ms |
+44.56% |
| Subprocess wall |
579.937 ms |
535.945 ms |
-7.59% |
| Maximum RSS |
37,076 KiB |
37,154 KiB |
+0.21% |
The capture used taskset affinity but no exclusive isolated core, the CPU
governor was powersave, and host load was high. These numbers are local
regression context only. Rerun on reserved isolated hardware before publishing
performance claims.
BashPYTHONPATH=src .venv/bin/pytest tests/test_bioware -q
.venv/bin/python benchmarks/bench_bioware.py \
--baseline-root <clean-parent-tree> \
--baseline-ref c4e492ff5 \
--candidate-root . \
--candidate-ref working-tree \
--iterations 30 \
--warmups 2 \
--output benchmarks/results/bench_bioware.json
The closed-loop biohybrid research context is exemplified by
Kagan et al. (2022); that work
does not validate this software implementation or its safety.
API reference
sc_neurocore.bioware.bioware
Historical import facade for the biological-hardware interface.
Implementations live in responsibility-specific sibling modules. This module
retains the established import, qualified-name, object-identity, and pickle
contracts for callers of :mod:sc_neurocore.bioware.bioware.
ArtifactRejector
dataclass
Blanks stimulation artifacts from voltage data.
Zeros the voltage trace in a window around each stimulation onset.
Source code in src/sc_neurocore/bioware/bioware_acquisition.py
| Python |
|---|
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248 | @dataclass
class ArtifactRejector:
"""Blanks stimulation artifacts from voltage data.
Zeros the voltage trace in a window around each stimulation onset.
"""
blanking_pre_ms: float = 0.5
blanking_post_ms: float = 2.0
def __post_init__(self) -> None:
"""Validate non-negative artifact-blanking intervals."""
require_nonnegative(self.blanking_pre_ms, "blanking_pre_ms")
require_nonnegative(self.blanking_post_ms, "blanking_post_ms")
def blank(
self,
voltage_data: np.ndarray[Any, Any],
stim_times_s: List[float],
sample_rate_hz: float,
) -> np.ndarray[Any, Any]:
"""Return voltage data with stimulus artifacts blanked."""
validate_voltage_matrix(voltage_data)
require_positive(sample_rate_hz, "sample_rate_hz")
result = voltage_data.copy()
pre_samples = int(self.blanking_pre_ms * sample_rate_hz / 1000.0)
post_samples = int(self.blanking_post_ms * sample_rate_hz / 1000.0)
duration_s = result.shape[0] / sample_rate_hz
for t_s in stim_times_s:
require_nonnegative(t_s, "stimulus time")
if t_s >= duration_s:
raise ValueError("stimulus time must fall inside the voltage frame")
center = int(t_s * sample_rate_hz)
start = max(0, center - pre_samples)
end = min(result.shape[0], center + post_samples)
result[start:end, :] = 0.0
return result
|
__post_init__()
Validate non-negative artifact-blanking intervals.
Source code in src/sc_neurocore/bioware/bioware_acquisition.py
| Python |
|---|
| def __post_init__(self) -> None:
"""Validate non-negative artifact-blanking intervals."""
require_nonnegative(self.blanking_pre_ms, "blanking_pre_ms")
require_nonnegative(self.blanking_post_ms, "blanking_post_ms")
|
blank(voltage_data, stim_times_s, sample_rate_hz)
Return voltage data with stimulus artifacts blanked.
Source code in src/sc_neurocore/bioware/bioware_acquisition.py
| Python |
|---|
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248 | def blank(
self,
voltage_data: np.ndarray[Any, Any],
stim_times_s: List[float],
sample_rate_hz: float,
) -> np.ndarray[Any, Any]:
"""Return voltage data with stimulus artifacts blanked."""
validate_voltage_matrix(voltage_data)
require_positive(sample_rate_hz, "sample_rate_hz")
result = voltage_data.copy()
pre_samples = int(self.blanking_pre_ms * sample_rate_hz / 1000.0)
post_samples = int(self.blanking_post_ms * sample_rate_hz / 1000.0)
duration_s = result.shape[0] / sample_rate_hz
for t_s in stim_times_s:
require_nonnegative(t_s, "stimulus time")
if t_s >= duration_s:
raise ValueError("stimulus time must fall inside the voltage frame")
center = int(t_s * sample_rate_hz)
start = max(0, center - pre_samples)
end = min(result.shape[0], center + post_samples)
result[start:end, :] = 0.0
return result
|
SpikeDetector
dataclass
Threshold-based spike detector for MEA voltage traces.
Uses adaptive threshold: threshold = mean ± sigma * noise_estimate
where noise_estimate = median(|x|) / 0.6745 (robust RMS).
Supports configurable refractory period to prevent double-counting.
Source code in src/sc_neurocore/bioware/bioware_acquisition.py
| Python |
|---|
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117 | @dataclass
class SpikeDetector:
"""Threshold-based spike detector for MEA voltage traces.
Uses adaptive threshold: threshold = mean ± sigma * noise_estimate
where noise_estimate = median(|x|) / 0.6745 (robust RMS).
Supports configurable refractory period to prevent double-counting.
"""
config: MEAConfig
refractory_samples: int = 30
_noise_estimates: Optional[np.ndarray[Any, Any]] = field(default=None, repr=False)
def __post_init__(self) -> None:
"""Validate detector configuration and refractory interval."""
if not isinstance(self.config, MEAConfig):
raise TypeError("config must be an MEAConfig")
require_nonnegative_int(self.refractory_samples, "refractory_samples")
def estimate_noise(self, voltage_data: np.ndarray[Any, Any]) -> np.ndarray[Any, Any]:
"""Estimate per-channel noise from voltage data.
Uses median absolute deviation (MAD) for robustness against spikes.
voltage_data: shape (num_samples, num_channels)
"""
validate_voltage_matrix(voltage_data, expected_channels=self.config.num_channels)
mad: np.ndarray[Any, Any] = np.median(np.abs(voltage_data), axis=0) / 0.6745
self._noise_estimates = mad
return mad
def detect(
self, voltage_data: np.ndarray[Any, Any], snippet_ms: float = 2.0
) -> List[DetectedSpike]:
"""Detect spikes in multi-channel voltage data.
voltage_data: shape (num_samples, num_channels)
Returns list of DetectedSpike events.
"""
validate_voltage_matrix(voltage_data, expected_channels=self.config.num_channels)
require_positive(snippet_ms, "snippet_ms")
n_samples, n_channels = voltage_data.shape
if self._noise_estimates is None:
self.estimate_noise(voltage_data)
noise_estimates = cast(np.ndarray[Any, Any], self._noise_estimates)
spikes = []
dt = 1.0 / self.config.sample_rate_hz
sigma = self.config.spike_threshold_sigma
half = int(snippet_ms * self.config.sample_rate_hz / 2000.0)
if half < 1:
raise ValueError("snippet_ms is shorter than one sample on each side")
for ch in range(n_channels):
threshold = sigma * noise_estimates[ch]
above = np.abs(voltage_data[:, ch]) > threshold
crossings = np.where(np.diff(above.astype(int)) == 1)[0]
last_spike_idx = -self.refractory_samples - 1
for idx in crossings:
if idx - last_spike_idx < self.refractory_samples:
continue
last_spike_idx = idx
amp = float(voltage_data[idx, ch])
ts = idx * dt
# Extract waveform snippet
start = max(0, idx - half)
end = min(n_samples, idx + half)
# Pad if too close to edges. The raw slice length is at most
# 2*half == target_len (it is min(n, idx+half) - max(0, idx-half)),
# and for an edge spike (idx < half) the slice starts at 0 so
# pad_before = half - idx exactly closes the gap: pad_after is
# never negative and the padded waveform is exactly target_len.
raw_wave = voltage_data[start:end, ch].copy()
target_len = int(2 * half)
if len(raw_wave) < target_len:
pad_before = max(0, half - idx)
pad_after = max(0, target_len - len(raw_wave) - pad_before)
raw_wave = np.pad(raw_wave, (pad_before, pad_after), "constant")
spikes.append(
DetectedSpike(
channel=ch,
timestamp_s=ts,
amplitude_uv=amp,
unit_id=ch,
waveform=raw_wave,
)
)
return spikes
|
__post_init__()
Validate detector configuration and refractory interval.
Source code in src/sc_neurocore/bioware/bioware_acquisition.py
| Python |
|---|
| def __post_init__(self) -> None:
"""Validate detector configuration and refractory interval."""
if not isinstance(self.config, MEAConfig):
raise TypeError("config must be an MEAConfig")
require_nonnegative_int(self.refractory_samples, "refractory_samples")
|
estimate_noise(voltage_data)
Estimate per-channel noise from voltage data.
Uses median absolute deviation (MAD) for robustness against spikes.
voltage_data: shape (num_samples, num_channels)
Source code in src/sc_neurocore/bioware/bioware_acquisition.py
| Python |
|---|
47
48
49
50
51
52
53
54
55
56 | def estimate_noise(self, voltage_data: np.ndarray[Any, Any]) -> np.ndarray[Any, Any]:
"""Estimate per-channel noise from voltage data.
Uses median absolute deviation (MAD) for robustness against spikes.
voltage_data: shape (num_samples, num_channels)
"""
validate_voltage_matrix(voltage_data, expected_channels=self.config.num_channels)
mad: np.ndarray[Any, Any] = np.median(np.abs(voltage_data), axis=0) / 0.6745
self._noise_estimates = mad
return mad
|
detect(voltage_data, snippet_ms=2.0)
Detect spikes in multi-channel voltage data.
voltage_data: shape (num_samples, num_channels)
Returns list of DetectedSpike events.
Source code in src/sc_neurocore/bioware/bioware_acquisition.py
| Python |
|---|
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117 | def detect(
self, voltage_data: np.ndarray[Any, Any], snippet_ms: float = 2.0
) -> List[DetectedSpike]:
"""Detect spikes in multi-channel voltage data.
voltage_data: shape (num_samples, num_channels)
Returns list of DetectedSpike events.
"""
validate_voltage_matrix(voltage_data, expected_channels=self.config.num_channels)
require_positive(snippet_ms, "snippet_ms")
n_samples, n_channels = voltage_data.shape
if self._noise_estimates is None:
self.estimate_noise(voltage_data)
noise_estimates = cast(np.ndarray[Any, Any], self._noise_estimates)
spikes = []
dt = 1.0 / self.config.sample_rate_hz
sigma = self.config.spike_threshold_sigma
half = int(snippet_ms * self.config.sample_rate_hz / 2000.0)
if half < 1:
raise ValueError("snippet_ms is shorter than one sample on each side")
for ch in range(n_channels):
threshold = sigma * noise_estimates[ch]
above = np.abs(voltage_data[:, ch]) > threshold
crossings = np.where(np.diff(above.astype(int)) == 1)[0]
last_spike_idx = -self.refractory_samples - 1
for idx in crossings:
if idx - last_spike_idx < self.refractory_samples:
continue
last_spike_idx = idx
amp = float(voltage_data[idx, ch])
ts = idx * dt
# Extract waveform snippet
start = max(0, idx - half)
end = min(n_samples, idx + half)
# Pad if too close to edges. The raw slice length is at most
# 2*half == target_len (it is min(n, idx+half) - max(0, idx-half)),
# and for an edge spike (idx < half) the slice starts at 0 so
# pad_before = half - idx exactly closes the gap: pad_after is
# never negative and the padded waveform is exactly target_len.
raw_wave = voltage_data[start:end, ch].copy()
target_len = int(2 * half)
if len(raw_wave) < target_len:
pad_before = max(0, half - idx)
pad_after = max(0, target_len - len(raw_wave) - pad_before)
raw_wave = np.pad(raw_wave, (pad_before, pad_after), "constant")
spikes.append(
DetectedSpike(
channel=ch,
timestamp_s=ts,
amplitude_uv=amp,
unit_id=ch,
waveform=raw_wave,
)
)
return spikes
|
SpikeSorter
dataclass
Research spike sorter using PCA feature extraction and K-Means clustering.
Projects uniform waveforms onto their dominant principal components before
clustering them into units. Fitting requires the optional scikit-learn
dependency; incomplete waveform sets remain explicitly unassigned.
Source code in src/sc_neurocore/bioware/bioware_acquisition.py
| Python |
|---|
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208 | @dataclass
class SpikeSorter:
"""Research spike sorter using PCA feature extraction and K-Means clustering.
Projects uniform waveforms onto their dominant principal components before
clustering them into units. Fitting requires the optional ``scikit-learn``
dependency; incomplete waveform sets remain explicitly unassigned.
"""
num_units: int = 4
n_components: int = 3
random_state: int = 0
_pca: Any = field(default=None, repr=False)
_kmeans: Any = field(default=None, repr=False)
def __post_init__(self) -> None:
"""Validate cluster count, projection width, and deterministic seed."""
require_positive_int(self.num_units, "num_units")
require_positive_int(self.n_components, "n_components")
require_nonnegative_int(self.random_state, "random_state")
def fit(self, spikes: List[DetectedSpike]) -> None:
"""Fit PCA and KMeans models sequentially on available waveforms.
Silently no-ops (leaves ``_pca``/``_kmeans`` as ``None``) when
fewer than ``num_units`` waveforms are present — sklearn is only
imported in the path that actually needs it, so empty or
amplitude-only spike lists don't require scikit-learn.
"""
waveforms = [s.waveform for s in spikes if s.waveform is not None]
if len(waveforms) < self.num_units:
self._pca = None
self._kmeans = None
return
try:
from sklearn.cluster import KMeans
from sklearn.decomposition import PCA
except ImportError as exc:
raise ImportError(
"SpikeSorter.fit requires scikit-learn to cluster waveforms. "
"Install with `pip install scikit-learn` or "
"`pip install 'sc-neurocore[bioware]'`."
) from exc
waveform_lengths = {waveform.shape for waveform in waveforms}
if len(waveform_lengths) != 1:
raise ValueError("all spike waveforms must have the same shape")
waves_array = np.vstack(waveforms)
self._pca = PCA(n_components=min(self.n_components, len(waveforms), waves_array.shape[1]))
features = self._pca.fit_transform(waves_array)
self._kmeans = KMeans(
n_clusters=self.num_units,
n_init=10,
random_state=self.random_state,
)
self._kmeans.fit(features)
def assign(self, spikes: List[DetectedSpike]) -> List[DetectedSpike]:
"""Assign cluster IDs based on PCA feature projections."""
if self._pca is None or self._kmeans is None:
return spikes
result = []
for s in spikes:
if s.waveform is None:
result.append(s)
continue
expected_features = int(self._pca.n_features_in_)
if s.waveform.size != expected_features:
raise ValueError(
f"spike waveform has {s.waveform.size} samples; expected {expected_features}"
)
features = self._pca.transform(s.waveform.reshape(1, -1))
unit = int(self._kmeans.predict(features)[0])
result.append(
DetectedSpike(
channel=s.channel,
timestamp_s=s.timestamp_s,
amplitude_uv=s.amplitude_uv,
unit_id=unit,
waveform=s.waveform,
)
)
return result
|
__post_init__()
Validate cluster count, projection width, and deterministic seed.
Source code in src/sc_neurocore/bioware/bioware_acquisition.py
| Python |
|---|
| def __post_init__(self) -> None:
"""Validate cluster count, projection width, and deterministic seed."""
require_positive_int(self.num_units, "num_units")
require_positive_int(self.n_components, "n_components")
require_nonnegative_int(self.random_state, "random_state")
|
fit(spikes)
Fit PCA and KMeans models sequentially on available waveforms.
Silently no-ops (leaves _pca/_kmeans as None) when
fewer than num_units waveforms are present — sklearn is only
imported in the path that actually needs it, so empty or
amplitude-only spike lists don't require scikit-learn.
Source code in src/sc_neurocore/bioware/bioware_acquisition.py
| Python |
|---|
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177 | def fit(self, spikes: List[DetectedSpike]) -> None:
"""Fit PCA and KMeans models sequentially on available waveforms.
Silently no-ops (leaves ``_pca``/``_kmeans`` as ``None``) when
fewer than ``num_units`` waveforms are present — sklearn is only
imported in the path that actually needs it, so empty or
amplitude-only spike lists don't require scikit-learn.
"""
waveforms = [s.waveform for s in spikes if s.waveform is not None]
if len(waveforms) < self.num_units:
self._pca = None
self._kmeans = None
return
try:
from sklearn.cluster import KMeans
from sklearn.decomposition import PCA
except ImportError as exc:
raise ImportError(
"SpikeSorter.fit requires scikit-learn to cluster waveforms. "
"Install with `pip install scikit-learn` or "
"`pip install 'sc-neurocore[bioware]'`."
) from exc
waveform_lengths = {waveform.shape for waveform in waveforms}
if len(waveform_lengths) != 1:
raise ValueError("all spike waveforms must have the same shape")
waves_array = np.vstack(waveforms)
self._pca = PCA(n_components=min(self.n_components, len(waveforms), waves_array.shape[1]))
features = self._pca.fit_transform(waves_array)
self._kmeans = KMeans(
n_clusters=self.num_units,
n_init=10,
random_state=self.random_state,
)
self._kmeans.fit(features)
|
assign(spikes)
Assign cluster IDs based on PCA feature projections.
Source code in src/sc_neurocore/bioware/bioware_acquisition.py
| Python |
|---|
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208 | def assign(self, spikes: List[DetectedSpike]) -> List[DetectedSpike]:
"""Assign cluster IDs based on PCA feature projections."""
if self._pca is None or self._kmeans is None:
return spikes
result = []
for s in spikes:
if s.waveform is None:
result.append(s)
continue
expected_features = int(self._pca.n_features_in_)
if s.waveform.size != expected_features:
raise ValueError(
f"spike waveform has {s.waveform.size} samples; expected {expected_features}"
)
features = self._pca.transform(s.waveform.reshape(1, -1))
unit = int(self._kmeans.predict(features)[0])
result.append(
DetectedSpike(
channel=s.channel,
timestamp_s=s.timestamp_s,
amplitude_uv=s.amplitude_uv,
unit_id=unit,
waveform=s.waveform,
)
)
return result
|
CultureHealth
dataclass
Monitor organoid/culture viability from MEA activity.
Source code in src/sc_neurocore/bioware/bioware_analysis.py
| Python |
|---|
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77 | @dataclass
class CultureHealth:
"""Monitor organoid/culture viability from MEA activity."""
min_active_channels: int = 5
min_firing_rate_hz: float = 0.1
max_firing_rate_hz: float = 100.0
burst_threshold_hz: float = 50.0
def __post_init__(self) -> None:
"""Validate rate thresholds used by the aggregate health heuristic."""
require_positive_int(self.min_active_channels, "min_active_channels")
require_nonnegative(self.min_firing_rate_hz, "min_firing_rate_hz")
require_positive(self.max_firing_rate_hz, "max_firing_rate_hz")
if self.max_firing_rate_hz <= self.min_firing_rate_hz:
raise ValueError("max_firing_rate_hz must exceed min_firing_rate_hz")
require_nonnegative(self.burst_threshold_hz, "burst_threshold_hz")
def assess(self, spike_counts: np.ndarray[Any, Any], duration_s: float) -> Dict[str, float]:
"""Assess culture health from spike activity.
spike_counts: per-channel spike counts over duration_s
"""
if not isinstance(spike_counts, np.ndarray):
raise TypeError("spike_counts must be a NumPy array")
if spike_counts.ndim != 1 or spike_counts.size == 0:
raise ValueError("spike_counts must be a non-empty one-dimensional array")
if not np.issubdtype(spike_counts.dtype, np.number):
raise TypeError("spike_counts must have a numeric dtype")
if not np.all(np.isfinite(spike_counts)) or np.any(spike_counts < 0):
raise ValueError("spike_counts must contain finite non-negative values")
require_positive(duration_s, "duration_s")
rates = spike_counts / duration_s
active = np.sum(rates > self.min_firing_rate_hz)
mean_rate = float(np.mean(rates[rates > 0])) if np.any(rates > 0) else 0.0
bursting = np.sum(rates > self.burst_threshold_hz)
health_score = 1.0
if active < self.min_active_channels:
health_score *= active / self.min_active_channels
if mean_rate > self.max_firing_rate_hz:
health_score *= self.max_firing_rate_hz / mean_rate
return {
"active_channels": int(active),
"mean_firing_rate_hz": mean_rate,
"bursting_channels": int(bursting),
"health_score": float(np.clip(health_score, 0.0, 1.0)),
"is_viable": bool(health_score > 0.5),
}
|
__post_init__()
Validate rate thresholds used by the aggregate health heuristic.
Source code in src/sc_neurocore/bioware/bioware_analysis.py
| Python |
|---|
| def __post_init__(self) -> None:
"""Validate rate thresholds used by the aggregate health heuristic."""
require_positive_int(self.min_active_channels, "min_active_channels")
require_nonnegative(self.min_firing_rate_hz, "min_firing_rate_hz")
require_positive(self.max_firing_rate_hz, "max_firing_rate_hz")
if self.max_firing_rate_hz <= self.min_firing_rate_hz:
raise ValueError("max_firing_rate_hz must exceed min_firing_rate_hz")
require_nonnegative(self.burst_threshold_hz, "burst_threshold_hz")
|
assess(spike_counts, duration_s)
Assess culture health from spike activity.
spike_counts: per-channel spike counts over duration_s
Source code in src/sc_neurocore/bioware/bioware_analysis.py
| Python |
|---|
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77 | def assess(self, spike_counts: np.ndarray[Any, Any], duration_s: float) -> Dict[str, float]:
"""Assess culture health from spike activity.
spike_counts: per-channel spike counts over duration_s
"""
if not isinstance(spike_counts, np.ndarray):
raise TypeError("spike_counts must be a NumPy array")
if spike_counts.ndim != 1 or spike_counts.size == 0:
raise ValueError("spike_counts must be a non-empty one-dimensional array")
if not np.issubdtype(spike_counts.dtype, np.number):
raise TypeError("spike_counts must have a numeric dtype")
if not np.all(np.isfinite(spike_counts)) or np.any(spike_counts < 0):
raise ValueError("spike_counts must contain finite non-negative values")
require_positive(duration_s, "duration_s")
rates = spike_counts / duration_s
active = np.sum(rates > self.min_firing_rate_hz)
mean_rate = float(np.mean(rates[rates > 0])) if np.any(rates > 0) else 0.0
bursting = np.sum(rates > self.burst_threshold_hz)
health_score = 1.0
if active < self.min_active_channels:
health_score *= active / self.min_active_channels
if mean_rate > self.max_firing_rate_hz:
health_score *= self.max_firing_rate_hz / mean_rate
return {
"active_channels": int(active),
"mean_firing_rate_hz": mean_rate,
"bursting_channels": int(bursting),
"health_score": float(np.clip(health_score, 0.0, 1.0)),
"is_viable": bool(health_score > 0.5),
}
|
LFPBand
dataclass
Frequency band definition for LFP extraction.
Source code in src/sc_neurocore/bioware/bioware_analysis.py
| Python |
|---|
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95 | @dataclass
class LFPBand:
"""Frequency band definition for LFP extraction."""
name: str
low_hz: float
high_hz: float
def __post_init__(self) -> None:
"""Validate a named half-open frequency interval."""
if not self.name or not self.name.strip():
raise ValueError("LFP band name must not be empty")
require_nonnegative(self.low_hz, "low_hz")
require_positive(self.high_hz, "high_hz")
if self.high_hz <= self.low_hz:
raise ValueError("high_hz must exceed low_hz")
|
__post_init__()
Validate a named half-open frequency interval.
Source code in src/sc_neurocore/bioware/bioware_analysis.py
| Python |
|---|
| def __post_init__(self) -> None:
"""Validate a named half-open frequency interval."""
if not self.name or not self.name.strip():
raise ValueError("LFP band name must not be empty")
require_nonnegative(self.low_hz, "low_hz")
require_positive(self.high_hz, "high_hz")
if self.high_hz <= self.low_hz:
raise ValueError("high_hz must exceed low_hz")
|
LatencyBudget
dataclass
Tracks and enforces closed-loop latency requirements.
Source code in src/sc_neurocore/bioware/bioware_analysis.py
| Python |
|---|
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205 | @dataclass
class LatencyBudget:
"""Tracks and enforces closed-loop latency requirements."""
max_latency_us: float = 1000.0 # 1 ms default
history: List[float] = field(default_factory=list)
violations: int = 0
def __post_init__(self) -> None:
"""Validate budget, history, and violation accounting."""
require_positive(self.max_latency_us, "max_latency_us")
for latency_us in self.history:
require_nonnegative(latency_us, "history latency_us")
require_nonnegative_int(self.violations, "violations")
if self.violations > len(self.history):
raise ValueError("violations cannot exceed the number of history samples")
def record(self, latency_us: float) -> bool:
"""Record a latency measurement. Returns True if within budget."""
require_nonnegative(latency_us, "latency_us")
self.history.append(latency_us)
if latency_us > self.max_latency_us:
self.violations += 1
return False
return True
@property
def mean_latency_us(self) -> float:
"""Return the arithmetic mean of recorded loop latencies.
Returns
-------
float
Mean latency in microseconds, or ``0.0`` before any samples exist.
"""
return float(np.mean(self.history)) if self.history else 0.0
@property
def p99_latency_us(self) -> float:
"""Return the 99th percentile closed-loop latency.
Returns
-------
float
99th percentile latency in microseconds, or ``0.0`` for an empty
history.
"""
return float(np.percentile(self.history, 99)) if self.history else 0.0
@property
def compliance_ratio(self) -> float:
"""Return the fraction of samples inside the latency budget.
Returns
-------
float
Ratio in ``[0.0, 1.0]``; an empty history is defined as fully
compliant.
"""
if not self.history:
return 1.0
return 1.0 - self.violations / len(self.history)
|
mean_latency_us
property
Return the arithmetic mean of recorded loop latencies.
Returns
float
Mean latency in microseconds, or 0.0 before any samples exist.
p99_latency_us
property
Return the 99th percentile closed-loop latency.
Returns
float
99th percentile latency in microseconds, or 0.0 for an empty
history.
compliance_ratio
property
Return the fraction of samples inside the latency budget.
Returns
float
Ratio in [0.0, 1.0]; an empty history is defined as fully
compliant.
__post_init__()
Validate budget, history, and violation accounting.
Source code in src/sc_neurocore/bioware/bioware_analysis.py
| Python |
|---|
152
153
154
155
156
157
158
159 | def __post_init__(self) -> None:
"""Validate budget, history, and violation accounting."""
require_positive(self.max_latency_us, "max_latency_us")
for latency_us in self.history:
require_nonnegative(latency_us, "history latency_us")
require_nonnegative_int(self.violations, "violations")
if self.violations > len(self.history):
raise ValueError("violations cannot exceed the number of history samples")
|
record(latency_us)
Record a latency measurement. Returns True if within budget.
Source code in src/sc_neurocore/bioware/bioware_analysis.py
| Python |
|---|
161
162
163
164
165
166
167
168 | def record(self, latency_us: float) -> bool:
"""Record a latency measurement. Returns True if within budget."""
require_nonnegative(latency_us, "latency_us")
self.history.append(latency_us)
if latency_us > self.max_latency_us:
self.violations += 1
return False
return True
|
NetworkBurst
dataclass
Detected network-wide synchronised burst event.
Source code in src/sc_neurocore/bioware/bioware_analysis.py
| Python |
|---|
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222 | @dataclass
class NetworkBurst:
"""Detected network-wide synchronised burst event."""
onset_s: float
duration_s: float
participating_channels: int
total_spikes: int
def __post_init__(self) -> None:
"""Validate a detected network-burst summary."""
require_nonnegative(self.onset_s, "onset_s")
require_positive(self.duration_s, "duration_s")
require_positive_int(self.participating_channels, "participating_channels")
require_positive_int(self.total_spikes, "total_spikes")
|
__post_init__()
Validate a detected network-burst summary.
Source code in src/sc_neurocore/bioware/bioware_analysis.py
| Python |
|---|
| def __post_init__(self) -> None:
"""Validate a detected network-burst summary."""
require_nonnegative(self.onset_s, "onset_s")
require_positive(self.duration_s, "duration_s")
require_positive_int(self.participating_channels, "participating_channels")
require_positive_int(self.total_spikes, "total_spikes")
|
BioAuditEntry
dataclass
One audit entry for a bio-hybrid session.
Source code in src/sc_neurocore/bioware/bioware_audit.py
| Python |
|---|
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50 | @dataclass
class BioAuditEntry:
"""One audit entry for a bio-hybrid session."""
round_number: int
timestamp_iso: str
num_spikes: int
num_opto_pulses: int
latency_us: float
health_score: float
notes: str = ""
def __post_init__(self) -> None:
"""Validate one timestamped session-audit record."""
require_nonnegative_int(self.round_number, "round_number")
if not self.timestamp_iso or not self.timestamp_iso.strip():
raise ValueError("timestamp_iso must not be empty")
try:
datetime.fromisoformat(self.timestamp_iso)
except ValueError as exc:
raise ValueError("timestamp_iso must be an ISO-8601 date or datetime") from exc
require_nonnegative_int(self.num_spikes, "num_spikes")
require_nonnegative_int(self.num_opto_pulses, "num_opto_pulses")
require_nonnegative(self.latency_us, "latency_us")
require_nonnegative(self.health_score, "health_score")
if self.health_score > 1.0:
raise ValueError("health_score must be <= 1")
if not isinstance(self.notes, str):
raise TypeError("notes must be a string")
|
__post_init__()
Validate one timestamped session-audit record.
Source code in src/sc_neurocore/bioware/bioware_audit.py
| Python |
|---|
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50 | def __post_init__(self) -> None:
"""Validate one timestamped session-audit record."""
require_nonnegative_int(self.round_number, "round_number")
if not self.timestamp_iso or not self.timestamp_iso.strip():
raise ValueError("timestamp_iso must not be empty")
try:
datetime.fromisoformat(self.timestamp_iso)
except ValueError as exc:
raise ValueError("timestamp_iso must be an ISO-8601 date or datetime") from exc
require_nonnegative_int(self.num_spikes, "num_spikes")
require_nonnegative_int(self.num_opto_pulses, "num_opto_pulses")
require_nonnegative(self.latency_us, "latency_us")
require_nonnegative(self.health_score, "health_score")
if self.health_score > 1.0:
raise ValueError("health_score must be <= 1")
if not isinstance(self.notes, str):
raise TypeError("notes must be a string")
|
BioAuditLog
dataclass
Tamper-evident in-memory audit log for bio-hybrid experiments.
Source code in src/sc_neurocore/bioware/bioware_audit.py
| Python |
|---|
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134 | @dataclass
class BioAuditLog:
"""Tamper-evident in-memory audit log for bio-hybrid experiments."""
entries: List[BioAuditEntry] = field(default_factory=list)
experiment_id: str = ""
def __post_init__(self) -> None:
"""Validate experiment identity and strictly ordered audit entries."""
if not isinstance(self.experiment_id, str):
raise TypeError("experiment_id must be a string")
if self.experiment_id and not self.experiment_id.strip():
raise ValueError("experiment_id must not be whitespace only")
previous_round = -1
for entry in self.entries:
if not isinstance(entry, BioAuditEntry):
raise TypeError("entries must contain BioAuditEntry instances")
if entry.round_number <= previous_round:
raise ValueError("audit entry round numbers must increase strictly")
previous_round = entry.round_number
def log(self, entry: BioAuditEntry) -> None:
"""Append one audit entry to the session log.
Parameters
----------
entry:
Timestamped closed-loop session summary to retain in append order.
"""
if not isinstance(entry, BioAuditEntry):
raise TypeError("entry must be a BioAuditEntry")
if self.entries and entry.round_number <= self.entries[-1].round_number:
raise ValueError("audit entry round numbers must increase strictly")
self.entries.append(entry)
@property
def total_rounds(self) -> int:
"""Return the number of recorded audit entries.
Returns
-------
int
Count of entries currently stored in the log.
"""
return len(self.entries)
def to_list(self) -> List[Dict[str, Any]]:
"""Serialise audit entries to deterministic dictionaries.
Returns
-------
list[dict[str, Any]]
JSON-compatible records used by ``checksum`` and external evidence
sinks.
"""
return [
{
"round": e.round_number,
"timestamp": e.timestamp_iso,
"spikes": e.num_spikes,
"opto_pulses": e.num_opto_pulses,
"latency_us": e.latency_us,
"health_score": e.health_score,
"notes": e.notes,
}
for e in self.entries
]
def checksum(self) -> str:
"""Return a cross-environment SHA-256 over identity and log contents."""
payload = {
"schema": "sc-neurocore.bioware-audit.v1",
"experiment_id": self.experiment_id,
"entries": self.to_list(),
}
data = json.dumps(
payload,
sort_keys=True,
separators=(",", ":"),
allow_nan=False,
).encode("utf-8")
return hashlib.sha256(data).hexdigest()
|
total_rounds
property
Return the number of recorded audit entries.
Returns
int
Count of entries currently stored in the log.
__post_init__()
Validate experiment identity and strictly ordered audit entries.
Source code in src/sc_neurocore/bioware/bioware_audit.py
| Python |
|---|
60
61
62
63
64
65
66
67
68
69
70
71
72 | def __post_init__(self) -> None:
"""Validate experiment identity and strictly ordered audit entries."""
if not isinstance(self.experiment_id, str):
raise TypeError("experiment_id must be a string")
if self.experiment_id and not self.experiment_id.strip():
raise ValueError("experiment_id must not be whitespace only")
previous_round = -1
for entry in self.entries:
if not isinstance(entry, BioAuditEntry):
raise TypeError("entries must contain BioAuditEntry instances")
if entry.round_number <= previous_round:
raise ValueError("audit entry round numbers must increase strictly")
previous_round = entry.round_number
|
log(entry)
Append one audit entry to the session log.
Parameters
entry:
Timestamped closed-loop session summary to retain in append order.
Source code in src/sc_neurocore/bioware/bioware_audit.py
| Python |
|---|
74
75
76
77
78
79
80
81
82
83
84
85
86 | def log(self, entry: BioAuditEntry) -> None:
"""Append one audit entry to the session log.
Parameters
----------
entry:
Timestamped closed-loop session summary to retain in append order.
"""
if not isinstance(entry, BioAuditEntry):
raise TypeError("entry must be a BioAuditEntry")
if self.entries and entry.round_number <= self.entries[-1].round_number:
raise ValueError("audit entry round numbers must increase strictly")
self.entries.append(entry)
|
to_list()
Serialise audit entries to deterministic dictionaries.
Returns
list[dict[str, Any]]
JSON-compatible records used by checksum and external evidence
sinks.
Source code in src/sc_neurocore/bioware/bioware_audit.py
| Python |
|---|
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119 | def to_list(self) -> List[Dict[str, Any]]:
"""Serialise audit entries to deterministic dictionaries.
Returns
-------
list[dict[str, Any]]
JSON-compatible records used by ``checksum`` and external evidence
sinks.
"""
return [
{
"round": e.round_number,
"timestamp": e.timestamp_iso,
"spikes": e.num_spikes,
"opto_pulses": e.num_opto_pulses,
"latency_us": e.latency_us,
"health_score": e.health_score,
"notes": e.notes,
}
for e in self.entries
]
|
checksum()
Return a cross-environment SHA-256 over identity and log contents.
Source code in src/sc_neurocore/bioware/bioware_audit.py
| Python |
|---|
121
122
123
124
125
126
127
128
129
130
131
132
133
134 | def checksum(self) -> str:
"""Return a cross-environment SHA-256 over identity and log contents."""
payload = {
"schema": "sc-neurocore.bioware-audit.v1",
"experiment_id": self.experiment_id,
"entries": self.to_list(),
}
data = json.dumps(
payload,
sort_keys=True,
separators=(",", ":"),
allow_nan=False,
).encode("utf-8")
return hashlib.sha256(data).hexdigest()
|
AEREvent
dataclass
Address-Event Representation packet.
Compatible with sc_aer_encoder.v format:
Source code in src/sc_neurocore/bioware/bioware_contracts.py
| Python |
|---|
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144 | @dataclass
class AEREvent:
"""Address-Event Representation packet.
Compatible with sc_aer_encoder.v format:
{valid, neuron_id, timestamp}
"""
neuron_id: int
timestamp: int # clock ticks (not real time)
valid: bool = True
weight: int = 256 # Q8.8 = 1.0
def __post_init__(self) -> None:
"""Validate the maintained unsigned 16-bit AER packet fields."""
require_nonnegative_int(self.neuron_id, "neuron_id")
require_nonnegative_int(self.timestamp, "timestamp")
if self.timestamp > 0xFFFF:
raise ValueError("timestamp must fit the 16-bit AER field")
if not isinstance(self.valid, bool):
raise TypeError("valid must be a bool")
require_nonnegative_int(self.weight, "weight")
if self.weight > 0xFFFF:
raise ValueError("weight must fit an unsigned 16-bit Q8.8 field")
|
__post_init__()
Validate the maintained unsigned 16-bit AER packet fields.
Source code in src/sc_neurocore/bioware/bioware_contracts.py
| Python |
|---|
134
135
136
137
138
139
140
141
142
143
144 | def __post_init__(self) -> None:
"""Validate the maintained unsigned 16-bit AER packet fields."""
require_nonnegative_int(self.neuron_id, "neuron_id")
require_nonnegative_int(self.timestamp, "timestamp")
if self.timestamp > 0xFFFF:
raise ValueError("timestamp must fit the 16-bit AER field")
if not isinstance(self.valid, bool):
raise TypeError("valid must be a bool")
require_nonnegative_int(self.weight, "weight")
if self.weight > 0xFFFF:
raise ValueError("weight must fit an unsigned 16-bit Q8.8 field")
|
BioHybridFrameResult
dataclass
Strictly typed output packet detailing a full closed-loop step.
Behaves both as a dataclass (result.round) and, for backward
compatibility with pre-dataclass callers, as a mapping view of its
fields (result["round"], "latency_us" in result,
dict(result)). The mapping surface is read-only.
Source code in src/sc_neurocore/bioware/bioware_contracts.py
| Python |
|---|
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282 | @dataclass
class BioHybridFrameResult:
"""Strictly typed output packet detailing a full closed-loop step.
Behaves both as a dataclass (``result.round``) and, for backward
compatibility with pre-dataclass callers, as a mapping view of its
fields (``result["round"]``, ``"latency_us" in result``,
``dict(result)``). The mapping surface is read-only.
"""
round: int
num_spikes: int
num_aer_events: int
num_bitstreams: int
num_opto_pulses: int
latency_us: float
health: Dict[str, Any]
spikes: List[DetectedSpike]
aer_events: List[AEREvent]
bitstreams: Dict[int, np.ndarray[Any, Any]]
opto_pulses: List[OptogeneticPulse]
def __post_init__(self) -> None:
"""Validate counts and payload cardinalities for one closed-loop frame."""
require_nonnegative_int(self.round, "round")
for name, value in (
("num_spikes", self.num_spikes),
("num_aer_events", self.num_aer_events),
("num_bitstreams", self.num_bitstreams),
("num_opto_pulses", self.num_opto_pulses),
):
require_nonnegative_int(value, name)
require_nonnegative(self.latency_us, "latency_us")
if self.num_spikes != len(self.spikes):
raise ValueError("num_spikes must equal len(spikes)")
if self.num_aer_events != len(self.aer_events):
raise ValueError("num_aer_events must equal len(aer_events)")
if self.num_bitstreams != len(self.bitstreams):
raise ValueError("num_bitstreams must equal len(bitstreams)")
if self.num_opto_pulses != len(self.opto_pulses):
raise ValueError("num_opto_pulses must equal len(opto_pulses)")
for neuron_id, bitstream in self.bitstreams.items():
require_nonnegative_int(neuron_id, "bitstream neuron_id")
validate_binary_bitstream(
bitstream,
name=f"bitstreams[{neuron_id}]",
allow_empty=True,
)
def __getitem__(self, key: str) -> Any:
"""Return a dataclass field through the legacy mapping interface.
Parameters
----------
key:
Public dataclass field name to read.
Returns
-------
Any
The underlying field value, preserving object identity for mutable
payloads such as ``health`` and ``bitstreams``.
Raises
------
KeyError
If ``key`` is not a public field name.
"""
if not isinstance(key, str) or key.startswith("_"):
raise KeyError(key)
try:
return getattr(self, key)
except AttributeError as exc:
raise KeyError(key) from exc
def __contains__(self, key: object) -> bool:
"""Return whether ``key`` names a public result field.
Parameters
----------
key:
Candidate mapping key.
Returns
-------
bool
``True`` only for string keys matching declared dataclass fields.
"""
if not isinstance(key, str):
return False
return key in {f.name for f in fields(self)}
def keys(self) -> List[str]:
"""Return the mapping-view field names in dataclass declaration order.
Returns
-------
list[str]
Public field names accepted by ``__getitem__`` and ``__contains__``.
"""
return [f.name for f in fields(self)]
|
__post_init__()
Validate counts and payload cardinalities for one closed-loop frame.
Source code in src/sc_neurocore/bioware/bioware_contracts.py
| Python |
|---|
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229 | def __post_init__(self) -> None:
"""Validate counts and payload cardinalities for one closed-loop frame."""
require_nonnegative_int(self.round, "round")
for name, value in (
("num_spikes", self.num_spikes),
("num_aer_events", self.num_aer_events),
("num_bitstreams", self.num_bitstreams),
("num_opto_pulses", self.num_opto_pulses),
):
require_nonnegative_int(value, name)
require_nonnegative(self.latency_us, "latency_us")
if self.num_spikes != len(self.spikes):
raise ValueError("num_spikes must equal len(spikes)")
if self.num_aer_events != len(self.aer_events):
raise ValueError("num_aer_events must equal len(aer_events)")
if self.num_bitstreams != len(self.bitstreams):
raise ValueError("num_bitstreams must equal len(bitstreams)")
if self.num_opto_pulses != len(self.opto_pulses):
raise ValueError("num_opto_pulses must equal len(opto_pulses)")
for neuron_id, bitstream in self.bitstreams.items():
require_nonnegative_int(neuron_id, "bitstream neuron_id")
validate_binary_bitstream(
bitstream,
name=f"bitstreams[{neuron_id}]",
allow_empty=True,
)
|
__getitem__(key)
Return a dataclass field through the legacy mapping interface.
Parameters
key:
Public dataclass field name to read.
Returns
Any
The underlying field value, preserving object identity for mutable
payloads such as health and bitstreams.
Raises
KeyError
If key is not a public field name.
Source code in src/sc_neurocore/bioware/bioware_contracts.py
| Python |
|---|
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255 | def __getitem__(self, key: str) -> Any:
"""Return a dataclass field through the legacy mapping interface.
Parameters
----------
key:
Public dataclass field name to read.
Returns
-------
Any
The underlying field value, preserving object identity for mutable
payloads such as ``health`` and ``bitstreams``.
Raises
------
KeyError
If ``key`` is not a public field name.
"""
if not isinstance(key, str) or key.startswith("_"):
raise KeyError(key)
try:
return getattr(self, key)
except AttributeError as exc:
raise KeyError(key) from exc
|
__contains__(key)
Return whether key names a public result field.
Parameters
key:
Candidate mapping key.
Returns
bool
True only for string keys matching declared dataclass fields.
Source code in src/sc_neurocore/bioware/bioware_contracts.py
| Python |
|---|
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272 | def __contains__(self, key: object) -> bool:
"""Return whether ``key`` names a public result field.
Parameters
----------
key:
Candidate mapping key.
Returns
-------
bool
``True`` only for string keys matching declared dataclass fields.
"""
if not isinstance(key, str):
return False
return key in {f.name for f in fields(self)}
|
keys()
Return the mapping-view field names in dataclass declaration order.
Returns
list[str]
Public field names accepted by __getitem__ and __contains__.
Source code in src/sc_neurocore/bioware/bioware_contracts.py
| Python |
|---|
274
275
276
277
278
279
280
281
282 | def keys(self) -> List[str]:
"""Return the mapping-view field names in dataclass declaration order.
Returns
-------
list[str]
Public field names accepted by ``__getitem__`` and ``__contains__``.
"""
return [f.name for f in fields(self)]
|
DetectedSpike
dataclass
One detected spike event from MEA data.
Source code in src/sc_neurocore/bioware/bioware_contracts.py
| Python |
|---|
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118 | @dataclass
class DetectedSpike:
"""One detected spike event from MEA data."""
channel: int
timestamp_s: float
amplitude_uv: float
unit_id: int = 0 # cluster assignment
waveform: Optional[np.ndarray[Any, Any]] = None
def __post_init__(self) -> None:
"""Validate spike identity, timing, amplitude, and optional waveform."""
require_nonnegative_int(self.channel, "channel")
require_nonnegative(self.timestamp_s, "timestamp_s")
require_finite(self.amplitude_uv, "amplitude_uv")
require_nonnegative_int(self.unit_id, "unit_id")
if self.waveform is None:
return
if not isinstance(self.waveform, np.ndarray):
raise TypeError("waveform must be a NumPy array when provided")
if self.waveform.ndim != 1 or self.waveform.size == 0:
raise ValueError("waveform must be a non-empty one-dimensional array")
if not np.issubdtype(self.waveform.dtype, np.number):
raise TypeError("waveform must have a numeric dtype")
if not np.all(np.isfinite(self.waveform)):
raise ValueError("waveform must contain only finite values")
|
__post_init__()
Validate spike identity, timing, amplitude, and optional waveform.
Source code in src/sc_neurocore/bioware/bioware_contracts.py
| Python |
|---|
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118 | def __post_init__(self) -> None:
"""Validate spike identity, timing, amplitude, and optional waveform."""
require_nonnegative_int(self.channel, "channel")
require_nonnegative(self.timestamp_s, "timestamp_s")
require_finite(self.amplitude_uv, "amplitude_uv")
require_nonnegative_int(self.unit_id, "unit_id")
if self.waveform is None:
return
if not isinstance(self.waveform, np.ndarray):
raise TypeError("waveform must be a NumPy array when provided")
if self.waveform.ndim != 1 or self.waveform.size == 0:
raise ValueError("waveform must be a non-empty one-dimensional array")
if not np.issubdtype(self.waveform.dtype, np.number):
raise TypeError("waveform must have a numeric dtype")
if not np.all(np.isfinite(self.waveform)):
raise ValueError("waveform must contain only finite values")
|
MEAConfig
dataclass
Multi-electrode array configuration.
Source code in src/sc_neurocore/bioware/bioware_contracts.py
| Python |
|---|
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87 | @dataclass
class MEAConfig:
"""Multi-electrode array configuration."""
layout: MEALayout = MEALayout.MEA_60
num_channels: int = 60
sample_rate_hz: float = 20_000.0
voltage_gain: float = 1000.0
noise_floor_uv: float = 5.0
spike_threshold_sigma: float = 5.0
electrode_pitch_um: float = 200.0
def __post_init__(self) -> None:
"""Validate physical and acquisition configuration boundaries."""
if not isinstance(self.layout, MEALayout):
raise TypeError("layout must be a MEALayout")
require_positive_int(self.num_channels, "num_channels")
require_positive(self.sample_rate_hz, "sample_rate_hz")
require_positive(self.voltage_gain, "voltage_gain")
require_nonnegative(self.noise_floor_uv, "noise_floor_uv")
require_positive(self.spike_threshold_sigma, "spike_threshold_sigma")
require_positive(self.electrode_pitch_um, "electrode_pitch_um")
@classmethod
def from_layout(cls, layout: MEALayout) -> MEAConfig:
"""Create a configuration preset for a standard MEA layout.
Parameters
----------
layout:
Standard electrode layout whose channel count and pitch should seed
the returned configuration.
Returns
-------
MEAConfig
Configuration with the requested layout and canonical channel/pitch
preset while retaining the default sampling and detector gains.
"""
if not isinstance(layout, MEALayout):
raise TypeError("layout must be a MEALayout")
presets: Dict[MEALayout, Dict[str, Any]] = {
MEALayout.MEA_60: dict(num_channels=60, electrode_pitch_um=200.0),
MEALayout.MEA_120: dict(num_channels=120, electrode_pitch_um=100.0),
MEALayout.MEA_256: dict(num_channels=256, electrode_pitch_um=100.0),
MEALayout.MEA_4096: dict(num_channels=4096, electrode_pitch_um=17.5),
MEALayout.CUSTOM: dict(num_channels=60, electrode_pitch_um=200.0),
}
return cls(layout=layout, **presets[layout])
|
__post_init__()
Validate physical and acquisition configuration boundaries.
Source code in src/sc_neurocore/bioware/bioware_contracts.py
| Python |
|---|
51
52
53
54
55
56
57
58
59
60 | def __post_init__(self) -> None:
"""Validate physical and acquisition configuration boundaries."""
if not isinstance(self.layout, MEALayout):
raise TypeError("layout must be a MEALayout")
require_positive_int(self.num_channels, "num_channels")
require_positive(self.sample_rate_hz, "sample_rate_hz")
require_positive(self.voltage_gain, "voltage_gain")
require_nonnegative(self.noise_floor_uv, "noise_floor_uv")
require_positive(self.spike_threshold_sigma, "spike_threshold_sigma")
require_positive(self.electrode_pitch_um, "electrode_pitch_um")
|
from_layout(layout)
classmethod
Create a configuration preset for a standard MEA layout.
Parameters
layout:
Standard electrode layout whose channel count and pitch should seed
the returned configuration.
Returns
MEAConfig
Configuration with the requested layout and canonical channel/pitch
preset while retaining the default sampling and detector gains.
Source code in src/sc_neurocore/bioware/bioware_contracts.py
| Python |
|---|
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87 | @classmethod
def from_layout(cls, layout: MEALayout) -> MEAConfig:
"""Create a configuration preset for a standard MEA layout.
Parameters
----------
layout:
Standard electrode layout whose channel count and pitch should seed
the returned configuration.
Returns
-------
MEAConfig
Configuration with the requested layout and canonical channel/pitch
preset while retaining the default sampling and detector gains.
"""
if not isinstance(layout, MEALayout):
raise TypeError("layout must be a MEALayout")
presets: Dict[MEALayout, Dict[str, Any]] = {
MEALayout.MEA_60: dict(num_channels=60, electrode_pitch_um=200.0),
MEALayout.MEA_120: dict(num_channels=120, electrode_pitch_um=100.0),
MEALayout.MEA_256: dict(num_channels=256, electrode_pitch_um=100.0),
MEALayout.MEA_4096: dict(num_channels=4096, electrode_pitch_um=17.5),
MEALayout.CUSTOM: dict(num_channels=60, electrode_pitch_um=200.0),
}
return cls(layout=layout, **presets[layout])
|
MEALayout
Bases: Enum
Standard MEA electrode layouts.
Source code in src/sc_neurocore/bioware/bioware_contracts.py
| Python |
|---|
| class MEALayout(Enum):
"""Standard MEA electrode layouts."""
MEA_60 = "60ch"
MEA_120 = "120ch"
MEA_256 = "256ch"
MEA_4096 = "4096ch"
CUSTOM = "custom"
|
OptogeneticPulse
dataclass
One optical stimulation pulse.
Source code in src/sc_neurocore/bioware/bioware_contracts.py
| Python |
|---|
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179 | @dataclass
class OptogeneticPulse:
"""One optical stimulation pulse."""
channel: int
onset_ms: float
duration_ms: float
intensity_mw_mm2: float = 1.0
wavelength_nm: int = 470 # blue (ChR2)
illuminated_area_mm2: float = 1.0
def __post_init__(self) -> None:
"""Validate timing, irradiance, wavelength, and illuminated area."""
require_nonnegative_int(self.channel, "channel")
require_nonnegative(self.onset_ms, "onset_ms")
require_positive(self.duration_ms, "duration_ms")
require_nonnegative(self.intensity_mw_mm2, "intensity_mw_mm2")
require_positive_int(self.wavelength_nm, "wavelength_nm")
require_positive(self.illuminated_area_mm2, "illuminated_area_mm2")
@property
def power_mw(self) -> float:
"""Return optical power as irradiance multiplied by illuminated area."""
return self.intensity_mw_mm2 * self.illuminated_area_mm2
|
power_mw
property
Return optical power as irradiance multiplied by illuminated area.
__post_init__()
Validate timing, irradiance, wavelength, and illuminated area.
Source code in src/sc_neurocore/bioware/bioware_contracts.py
| Python |
|---|
167
168
169
170
171
172
173
174 | def __post_init__(self) -> None:
"""Validate timing, irradiance, wavelength, and illuminated area."""
require_nonnegative_int(self.channel, "channel")
require_nonnegative(self.onset_ms, "onset_ms")
require_positive(self.duration_ms, "duration_ms")
require_nonnegative(self.intensity_mw_mm2, "intensity_mw_mm2")
require_positive_int(self.wavelength_nm, "wavelength_nm")
require_positive(self.illuminated_area_mm2, "illuminated_area_mm2")
|
StimProtocol
Bases: Enum
Optogenetic stimulation protocols.
Source code in src/sc_neurocore/bioware/bioware_contracts.py
| Python |
|---|
147
148
149
150
151
152
153 | class StimProtocol(Enum):
"""Optogenetic stimulation protocols."""
CONSTANT = "constant"
PULSED = "pulsed"
GRADED = "graded"
PATTERN = "pattern"
|
AERToSCConverter
dataclass
Converts AER event streams to SC bitstreams.
Uses a time-windowed rate code: count events per neuron per window,
then LFSR-encode the resulting firing probabilities.
Source code in src/sc_neurocore/bioware/bioware_encoding.py
| Python |
|---|
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152 | @dataclass
class AERToSCConverter:
"""Converts AER event streams to SC bitstreams.
Uses a time-windowed rate code: count events per neuron per window,
then LFSR-encode the resulting firing probabilities.
"""
window_ticks: int = 0x10000
bitstream_length: int = 256
num_neurons: int = 128
lfsr_seed: int = 0xACE1
def __post_init__(self) -> None:
"""Validate window, bitstream, neuron, and LFSR boundaries."""
require_positive_int(self.window_ticks, "window_ticks")
require_positive_int(self.bitstream_length, "bitstream_length")
require_positive_int(self.num_neurons, "num_neurons")
require_nonnegative_int(self.lfsr_seed, "lfsr_seed")
if self.lfsr_seed > 0xFFFF:
raise ValueError("lfsr_seed must fit 16 bits")
def convert(self, events: List[AEREvent]) -> Dict[int, np.ndarray[Any, Any]]:
"""Convert AER events to per-neuron SC bitstreams."""
# Count events per neuron in the window
counts: Dict[int, int] = {}
for e in events:
if e.valid:
if e.neuron_id >= self.num_neurons:
raise ValueError(
f"AER neuron_id {e.neuron_id} is outside num_neurons={self.num_neurons}"
)
if e.timestamp >= self.window_ticks:
raise ValueError(
f"AER timestamp {e.timestamp} is outside window_ticks={self.window_ticks}"
)
counts[e.neuron_id] = counts.get(e.neuron_id, 0) + 1
max_count = max(counts.values()) if counts else 1
bitstreams = {}
for nid, count in counts.items():
prob = count / max_count
bitstreams[nid] = self._lfsr_encode(prob, nid)
return bitstreams
def _lfsr_encode(self, probability: float, neuron_id: int) -> np.ndarray[Any, Any]:
"""LFSR-16 encoding (bit-compatible with core_engine)."""
require_finite(probability, "probability")
if not 0.0 <= probability <= 1.0:
raise ValueError("probability must be in [0, 1]")
require_nonnegative_int(neuron_id, "neuron_id")
if neuron_id >= self.num_neurons:
raise ValueError("neuron_id must be smaller than num_neurons")
threshold = int(np.clip(probability, 0.0, 1.0) * 65535)
seed = (self.lfsr_seed + neuron_id * 7919) & 0xFFFF
if seed == 0:
seed = 1
reg = seed
bits = np.zeros(self.bitstream_length, dtype=np.uint8)
for i in range(self.bitstream_length):
bits[i] = 1 if reg < threshold else 0
feedback = ((reg >> 15) ^ (reg >> 13) ^ (reg >> 12) ^ (reg >> 10)) & 1
reg = ((reg << 1) | feedback) & 0xFFFF
return bits
|
__post_init__()
Validate window, bitstream, neuron, and LFSR boundaries.
Source code in src/sc_neurocore/bioware/bioware_encoding.py
| Python |
|---|
102
103
104
105
106
107
108
109 | def __post_init__(self) -> None:
"""Validate window, bitstream, neuron, and LFSR boundaries."""
require_positive_int(self.window_ticks, "window_ticks")
require_positive_int(self.bitstream_length, "bitstream_length")
require_positive_int(self.num_neurons, "num_neurons")
require_nonnegative_int(self.lfsr_seed, "lfsr_seed")
if self.lfsr_seed > 0xFFFF:
raise ValueError("lfsr_seed must fit 16 bits")
|
convert(events)
Convert AER events to per-neuron SC bitstreams.
Source code in src/sc_neurocore/bioware/bioware_encoding.py
| Python |
|---|
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132 | def convert(self, events: List[AEREvent]) -> Dict[int, np.ndarray[Any, Any]]:
"""Convert AER events to per-neuron SC bitstreams."""
# Count events per neuron in the window
counts: Dict[int, int] = {}
for e in events:
if e.valid:
if e.neuron_id >= self.num_neurons:
raise ValueError(
f"AER neuron_id {e.neuron_id} is outside num_neurons={self.num_neurons}"
)
if e.timestamp >= self.window_ticks:
raise ValueError(
f"AER timestamp {e.timestamp} is outside window_ticks={self.window_ticks}"
)
counts[e.neuron_id] = counts.get(e.neuron_id, 0) + 1
max_count = max(counts.values()) if counts else 1
bitstreams = {}
for nid, count in counts.items():
prob = count / max_count
bitstreams[nid] = self._lfsr_encode(prob, nid)
return bitstreams
|
MEAToAERTranscoder
dataclass
Converts MEA spike events to AER events for hardware.
Maps biological electrode channels to AER neuron IDs,
converting real-time timestamps to hardware clock ticks.
Source code in src/sc_neurocore/bioware/bioware_encoding.py
| Python |
|---|
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86 | @dataclass
class MEAToAERTranscoder:
"""Converts MEA spike events to AER events for hardware.
Maps biological electrode channels to AER neuron IDs,
converting real-time timestamps to hardware clock ticks.
"""
hw_clock_hz: float = 1e6 # 1 MHz default AER clock
channel_map: Optional[Dict[int, int]] = None # electrode → neuron_id
def __post_init__(self) -> None:
"""Validate the hardware clock and optional channel mapping."""
require_positive(self.hw_clock_hz, "hw_clock_hz")
if self.channel_map is None:
return
for channel, neuron_id in self.channel_map.items():
require_nonnegative_int(channel, "channel_map channel")
require_nonnegative_int(neuron_id, "channel_map neuron_id")
def transcode(
self,
spikes: List[DetectedSpike],
t_start_s: float = 0.0,
) -> List[AEREvent]:
"""Convert spikes in one 16-bit hardware-clock epoch to AER events.
``DetectedSpike.timestamp_s`` and ``t_start_s`` must use the same time
origin. The method rejects events outside the representable epoch;
callers must split longer recordings instead of accepting timestamp
wraparound and the resulting loss of temporal ordering.
"""
require_nonnegative(t_start_s, "t_start_s")
events = []
for spike in spikes:
neuron_id = self._map_channel(spike.channel)
relative_s = spike.timestamp_s - t_start_s
if relative_s < 0.0:
raise ValueError("spike timestamp precedes t_start_s")
ticks = relative_s * self.hw_clock_hz
if not np.isfinite(ticks) or ticks > 0xFFFF:
raise ValueError("spike timestamp does not fit the 16-bit AER window")
ts_hw = int(ticks)
events.append(
AEREvent(
neuron_id=neuron_id,
timestamp=ts_hw,
valid=True,
)
)
# Sort by timestamp (AER is time-ordered)
events.sort(key=lambda e: e.timestamp)
return events
def _map_channel(self, channel: int) -> int:
if self.channel_map is not None:
return self.channel_map.get(channel, channel)
return channel
|
__post_init__()
Validate the hardware clock and optional channel mapping.
Source code in src/sc_neurocore/bioware/bioware_encoding.py
| Python |
|---|
| def __post_init__(self) -> None:
"""Validate the hardware clock and optional channel mapping."""
require_positive(self.hw_clock_hz, "hw_clock_hz")
if self.channel_map is None:
return
for channel, neuron_id in self.channel_map.items():
require_nonnegative_int(channel, "channel_map channel")
require_nonnegative_int(neuron_id, "channel_map neuron_id")
|
transcode(spikes, t_start_s=0.0)
Convert spikes in one 16-bit hardware-clock epoch to AER events.
DetectedSpike.timestamp_s and t_start_s must use the same time
origin. The method rejects events outside the representable epoch;
callers must split longer recordings instead of accepting timestamp
wraparound and the resulting loss of temporal ordering.
Source code in src/sc_neurocore/bioware/bioware_encoding.py
| Python |
|---|
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81 | def transcode(
self,
spikes: List[DetectedSpike],
t_start_s: float = 0.0,
) -> List[AEREvent]:
"""Convert spikes in one 16-bit hardware-clock epoch to AER events.
``DetectedSpike.timestamp_s`` and ``t_start_s`` must use the same time
origin. The method rejects events outside the representable epoch;
callers must split longer recordings instead of accepting timestamp
wraparound and the resulting loss of temporal ordering.
"""
require_nonnegative(t_start_s, "t_start_s")
events = []
for spike in spikes:
neuron_id = self._map_channel(spike.channel)
relative_s = spike.timestamp_s - t_start_s
if relative_s < 0.0:
raise ValueError("spike timestamp precedes t_start_s")
ticks = relative_s * self.hw_clock_hz
if not np.isfinite(ticks) or ticks > 0xFFFF:
raise ValueError("spike timestamp does not fit the 16-bit AER window")
ts_hw = int(ticks)
events.append(
AEREvent(
neuron_id=neuron_id,
timestamp=ts_hw,
valid=True,
)
)
# Sort by timestamp (AER is time-ordered)
events.sort(key=lambda e: e.timestamp)
return events
|
SCToOptoEncoder
dataclass
Encodes SC bitstream output as optogenetic pulse sequences.
Maps SC bitstream density to optical stimulation intensity,
enabling closed-loop feedback from in-silico → biological.
Enforces total power budget for tissue safety.
Source code in src/sc_neurocore/bioware/bioware_encoding.py
| Python |
|---|
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219 | @dataclass
class SCToOptoEncoder:
"""Encodes SC bitstream output as optogenetic pulse sequences.
Maps SC bitstream density to optical stimulation intensity,
enabling closed-loop feedback from in-silico → biological.
Enforces total power budget for tissue safety.
"""
max_intensity_mw_mm2: float = 5.0
min_pulse_ms: float = 1.0
max_pulse_ms: float = 50.0
wavelength_nm: int = 470
clock_period_ms: float = 0.001 # 1 MHz
max_total_power_mw: float = 50.0
illuminated_area_mm2: float = 1.0
def __post_init__(self) -> None:
"""Validate optical timing, irradiance, area, and power limits."""
require_nonnegative(self.max_intensity_mw_mm2, "max_intensity_mw_mm2")
require_positive(self.min_pulse_ms, "min_pulse_ms")
require_positive(self.max_pulse_ms, "max_pulse_ms")
if self.max_pulse_ms < self.min_pulse_ms:
raise ValueError("max_pulse_ms must be >= min_pulse_ms")
require_positive_int(self.wavelength_nm, "wavelength_nm")
require_nonnegative(self.clock_period_ms, "clock_period_ms")
require_positive(self.max_total_power_mw, "max_total_power_mw")
require_positive(self.illuminated_area_mm2, "illuminated_area_mm2")
def encode(
self,
bitstreams: Dict[int, np.ndarray[Any, Any]],
t_start_ms: float = 0.0,
) -> List[OptogeneticPulse]:
"""Convert SC bitstreams to optogenetic pulses."""
require_nonnegative(t_start_ms, "t_start_ms")
pulses = []
total_power = 0.0
for nid, bs in sorted(bitstreams.items()):
require_nonnegative_int(nid, "bitstream neuron_id")
validate_binary_bitstream(bs, name=f"bitstreams[{nid}]", allow_empty=True)
density = float(np.sum(bs)) / len(bs) if len(bs) > 0 else 0.0
if density < 0.01:
continue
intensity = density * self.max_intensity_mw_mm2
power_mw = intensity * self.illuminated_area_mm2
if total_power + power_mw > self.max_total_power_mw:
continue
total_power += power_mw
duration = self.min_pulse_ms + density * (self.max_pulse_ms - self.min_pulse_ms)
onset = t_start_ms + nid * self.clock_period_ms
pulses.append(
OptogeneticPulse(
channel=nid,
onset_ms=onset,
duration_ms=duration,
intensity_mw_mm2=intensity,
wavelength_nm=self.wavelength_nm,
illuminated_area_mm2=self.illuminated_area_mm2,
)
)
return pulses
|
__post_init__()
Validate optical timing, irradiance, area, and power limits.
Source code in src/sc_neurocore/bioware/bioware_encoding.py
| Python |
|---|
172
173
174
175
176
177
178
179
180
181
182 | def __post_init__(self) -> None:
"""Validate optical timing, irradiance, area, and power limits."""
require_nonnegative(self.max_intensity_mw_mm2, "max_intensity_mw_mm2")
require_positive(self.min_pulse_ms, "min_pulse_ms")
require_positive(self.max_pulse_ms, "max_pulse_ms")
if self.max_pulse_ms < self.min_pulse_ms:
raise ValueError("max_pulse_ms must be >= min_pulse_ms")
require_positive_int(self.wavelength_nm, "wavelength_nm")
require_nonnegative(self.clock_period_ms, "clock_period_ms")
require_positive(self.max_total_power_mw, "max_total_power_mw")
require_positive(self.illuminated_area_mm2, "illuminated_area_mm2")
|
encode(bitstreams, t_start_ms=0.0)
Convert SC bitstreams to optogenetic pulses.
Source code in src/sc_neurocore/bioware/bioware_encoding.py
| Python |
|---|
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219 | def encode(
self,
bitstreams: Dict[int, np.ndarray[Any, Any]],
t_start_ms: float = 0.0,
) -> List[OptogeneticPulse]:
"""Convert SC bitstreams to optogenetic pulses."""
require_nonnegative(t_start_ms, "t_start_ms")
pulses = []
total_power = 0.0
for nid, bs in sorted(bitstreams.items()):
require_nonnegative_int(nid, "bitstream neuron_id")
validate_binary_bitstream(bs, name=f"bitstreams[{nid}]", allow_empty=True)
density = float(np.sum(bs)) / len(bs) if len(bs) > 0 else 0.0
if density < 0.01:
continue
intensity = density * self.max_intensity_mw_mm2
power_mw = intensity * self.illuminated_area_mm2
if total_power + power_mw > self.max_total_power_mw:
continue
total_power += power_mw
duration = self.min_pulse_ms + density * (self.max_pulse_ms - self.min_pulse_ms)
onset = t_start_ms + nid * self.clock_period_ms
pulses.append(
OptogeneticPulse(
channel=nid,
onset_ms=onset,
duration_ms=duration,
intensity_mw_mm2=intensity,
wavelength_nm=self.wavelength_nm,
illuminated_area_mm2=self.illuminated_area_mm2,
)
)
return pulses
|
MultiWellPlate
dataclass
Multi-well plate (e.g., 6/24/48/96-well MEA plate).
Source code in src/sc_neurocore/bioware/bioware_experiment.py
| Python |
|---|
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296 | @dataclass
class MultiWellPlate:
"""Multi-well plate (e.g., 6/24/48/96-well MEA plate)."""
wells: List[WellConfig] = field(default_factory=list)
def __post_init__(self) -> None:
"""Validate well types and unique well identifiers."""
seen: set[str] = set()
for well in self.wells:
if not isinstance(well, WellConfig):
raise TypeError("wells must contain WellConfig instances")
if well.well_id in seen:
raise ValueError(f"duplicate well_id: {well.well_id}")
seen.add(well.well_id)
def add_well(self, well: WellConfig) -> None:
"""Append a well configuration to the plate.
Parameters
----------
well:
Well metadata and MEA configuration to append.
"""
if not isinstance(well, WellConfig):
raise TypeError("well must be a WellConfig")
if self.get_well(well.well_id) is not None:
raise ValueError(f"duplicate well_id: {well.well_id}")
self.wells.append(well)
@classmethod
def standard_6_well(cls, layout: MEALayout = MEALayout.MEA_60) -> MultiWellPlate:
"""Construct a six-well plate with uniform MEA layout presets.
Parameters
----------
layout:
MEA layout preset used for each generated well.
Returns
-------
MultiWellPlate
Plate containing wells ``W1`` through ``W6``.
"""
if not isinstance(layout, MEALayout):
raise TypeError("layout must be a MEALayout")
plate = cls()
for i in range(6):
plate.add_well(
WellConfig(
well_id=f"W{i + 1}",
mea_config=MEAConfig.from_layout(layout),
)
)
return plate
@property
def num_wells(self) -> int:
"""Return the number of configured wells.
Returns
-------
int
Length of the plate's well list.
"""
return len(self.wells)
def get_well(self, well_id: str) -> Optional[WellConfig]:
"""Return a well by identifier.
Parameters
----------
well_id:
Identifier such as ``"W1"``.
Returns
-------
WellConfig | None
Matching well configuration, or ``None`` when the plate does not
contain ``well_id``.
"""
if not well_id or not well_id.strip():
raise ValueError("well_id must not be empty")
return next((w for w in self.wells if w.well_id == well_id), None)
|
num_wells
property
Return the number of configured wells.
Returns
int
Length of the plate's well list.
__post_init__()
Validate well types and unique well identifiers.
Source code in src/sc_neurocore/bioware/bioware_experiment.py
| Python |
|---|
219
220
221
222
223
224
225
226
227 | def __post_init__(self) -> None:
"""Validate well types and unique well identifiers."""
seen: set[str] = set()
for well in self.wells:
if not isinstance(well, WellConfig):
raise TypeError("wells must contain WellConfig instances")
if well.well_id in seen:
raise ValueError(f"duplicate well_id: {well.well_id}")
seen.add(well.well_id)
|
add_well(well)
Append a well configuration to the plate.
Parameters
well:
Well metadata and MEA configuration to append.
Source code in src/sc_neurocore/bioware/bioware_experiment.py
| Python |
|---|
229
230
231
232
233
234
235
236
237
238
239
240
241 | def add_well(self, well: WellConfig) -> None:
"""Append a well configuration to the plate.
Parameters
----------
well:
Well metadata and MEA configuration to append.
"""
if not isinstance(well, WellConfig):
raise TypeError("well must be a WellConfig")
if self.get_well(well.well_id) is not None:
raise ValueError(f"duplicate well_id: {well.well_id}")
self.wells.append(well)
|
standard_6_well(layout=MEALayout.MEA_60)
classmethod
Construct a six-well plate with uniform MEA layout presets.
Parameters
layout:
MEA layout preset used for each generated well.
Returns
MultiWellPlate
Plate containing wells W1 through W6.
Source code in src/sc_neurocore/bioware/bioware_experiment.py
| Python |
|---|
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267 | @classmethod
def standard_6_well(cls, layout: MEALayout = MEALayout.MEA_60) -> MultiWellPlate:
"""Construct a six-well plate with uniform MEA layout presets.
Parameters
----------
layout:
MEA layout preset used for each generated well.
Returns
-------
MultiWellPlate
Plate containing wells ``W1`` through ``W6``.
"""
if not isinstance(layout, MEALayout):
raise TypeError("layout must be a MEALayout")
plate = cls()
for i in range(6):
plate.add_well(
WellConfig(
well_id=f"W{i + 1}",
mea_config=MEAConfig.from_layout(layout),
)
)
return plate
|
get_well(well_id)
Return a well by identifier.
Parameters
well_id:
Identifier such as "W1".
Returns
WellConfig | None
Matching well configuration, or None when the plate does not
contain well_id.
Source code in src/sc_neurocore/bioware/bioware_experiment.py
| Python |
|---|
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296 | def get_well(self, well_id: str) -> Optional[WellConfig]:
"""Return a well by identifier.
Parameters
----------
well_id:
Identifier such as ``"W1"``.
Returns
-------
WellConfig | None
Matching well configuration, or ``None`` when the plate does not
contain ``well_id``.
"""
if not well_id or not well_id.strip():
raise ValueError("well_id must not be empty")
return next((w for w in self.wells if w.well_id == well_id), None)
|
PharmModel
dataclass
Simulates effect of pharmacological agents on spike rate.
Models excitatory (e.g., bicuculline) or inhibitory (e.g., TTX) agents
as gain factors on firing rate.
Source code in src/sc_neurocore/bioware/bioware_experiment.py
| Python |
|---|
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159 | @dataclass
class PharmModel:
"""Simulates effect of pharmacological agents on spike rate.
Models excitatory (e.g., bicuculline) or inhibitory (e.g., TTX) agents
as gain factors on firing rate.
"""
agent_name: str = "none"
gain: float = 1.0 # >1 = excitatory, <1 = inhibitory, 0 = silencing
onset_delay_s: float = 30.0
wash_time_s: float = 120.0
_applied_at: float = -1.0
def __post_init__(self) -> None:
"""Validate the pharmacological gain and experiment-time constants."""
if not self.agent_name or not self.agent_name.strip():
raise ValueError("agent_name must not be empty")
require_nonnegative(self.gain, "gain")
require_nonnegative(self.onset_delay_s, "onset_delay_s")
require_positive(self.wash_time_s, "wash_time_s")
require_finite(self._applied_at, "_applied_at")
if self._applied_at < -1.0:
raise ValueError("_applied_at must be -1 or a non-negative timestamp")
def apply(self, t_current_s: float) -> None:
"""Mark the pharmacological agent as applied at the current time.
Parameters
----------
t_current_s:
Experiment time in seconds used as the onset reference for
subsequent gain interpolation.
"""
require_nonnegative(t_current_s, "t_current_s")
self._applied_at = t_current_s
def effective_gain(self, t_current_s: float) -> float:
"""Return the active firing-rate gain at an experiment timestamp.
Parameters
----------
t_current_s:
Experiment time in seconds.
Returns
-------
float
``1.0`` before application, a linearly interpolated onset gain
during ``onset_delay_s``, or the configured steady-state gain after
onset.
"""
require_nonnegative(t_current_s, "t_current_s")
if self._applied_at < 0:
return 1.0
elapsed = t_current_s - self._applied_at
if elapsed < 0.0:
raise ValueError("t_current_s must not precede the application time")
if elapsed < self.onset_delay_s:
frac = elapsed / self.onset_delay_s
return 1.0 + frac * (self.gain - 1.0)
return self.gain
def modulate_spikes(
self, spike_counts: np.ndarray[Any, Any], t_current_s: float
) -> np.ndarray[Any, Any]:
"""Modulate spike counts by pharmacological gain."""
if not isinstance(spike_counts, np.ndarray):
raise TypeError("spike_counts must be a NumPy array")
if spike_counts.ndim != 1:
raise ValueError("spike_counts must be one-dimensional")
if not np.issubdtype(spike_counts.dtype, np.number):
raise TypeError("spike_counts must have a numeric dtype")
if not np.all(np.isfinite(spike_counts)) or np.any(spike_counts < 0):
raise ValueError("spike_counts must contain finite non-negative values")
g = self.effective_gain(t_current_s)
return np.round(spike_counts * g).astype(int)
def modulate_spike_events(
self,
spikes: List[DetectedSpike],
t_current_s: float,
) -> List[DetectedSpike]:
"""Apply pharmacological rate gain to spike events.
Inhibitory gains deterministically thin events across the observed
response span instead of truncating the head of the frame. Excitatory
gains preserve observed events and insert synthetic events inside the
observed temporal support, using nearest observed spikes as channel,
unit, amplitude, and waveform templates.
"""
require_nonnegative(t_current_s, "t_current_s")
if not spikes:
return []
gain = self.effective_gain(t_current_s)
if not math.isfinite(gain) or gain < 0.0:
raise ValueError("pharmacological gain must be finite and >= 0")
ordered = sorted(spikes, key=lambda s: (s.timestamp_s, s.channel, s.unit_id))
target_count = int(round(len(ordered) * gain))
if target_count <= 0:
return []
if target_count == len(ordered):
return list(ordered)
if target_count < len(ordered):
indices = _quantile_indices(len(ordered), target_count)
return [ordered[i] for i in indices]
extra = target_count - len(ordered)
timestamps = np.array([s.timestamp_s for s in ordered], dtype=float)
if not np.all(np.isfinite(timestamps)):
raise ValueError("detected spike timestamps must be finite")
if len(ordered) == 1 or timestamps[-1] <= timestamps[0]:
synthetic = [
_clone_spike(ordered[0], timestamp_s=float(timestamps[0])) for _ in range(extra)
]
else:
insert_times = np.linspace(timestamps[0], timestamps[-1], extra + 2)[1:-1]
synthetic = []
for t in insert_times:
# insert_times are strictly interior to (timestamps[0],
# timestamps[-1]) — the linspace endpoints are dropped — so for
# t < timestamps[-1] a left-side searchsorted always yields
# idx <= len(ordered) - 1; no upper clamp is reachable.
idx = int(np.searchsorted(timestamps, t, side="left"))
if idx > 0 and abs(timestamps[idx - 1] - t) <= abs(timestamps[idx] - t):
idx -= 1
synthetic.append(_clone_spike(ordered[idx], timestamp_s=float(t)))
return sorted([*ordered, *synthetic], key=lambda s: (s.timestamp_s, s.channel, s.unit_id))
|
__post_init__()
Validate the pharmacological gain and experiment-time constants.
Source code in src/sc_neurocore/bioware/bioware_experiment.py
| Python |
|---|
42
43
44
45
46
47
48
49
50
51 | def __post_init__(self) -> None:
"""Validate the pharmacological gain and experiment-time constants."""
if not self.agent_name or not self.agent_name.strip():
raise ValueError("agent_name must not be empty")
require_nonnegative(self.gain, "gain")
require_nonnegative(self.onset_delay_s, "onset_delay_s")
require_positive(self.wash_time_s, "wash_time_s")
require_finite(self._applied_at, "_applied_at")
if self._applied_at < -1.0:
raise ValueError("_applied_at must be -1 or a non-negative timestamp")
|
apply(t_current_s)
Mark the pharmacological agent as applied at the current time.
Parameters
t_current_s:
Experiment time in seconds used as the onset reference for
subsequent gain interpolation.
Source code in src/sc_neurocore/bioware/bioware_experiment.py
| Python |
|---|
53
54
55
56
57
58
59
60
61
62
63 | def apply(self, t_current_s: float) -> None:
"""Mark the pharmacological agent as applied at the current time.
Parameters
----------
t_current_s:
Experiment time in seconds used as the onset reference for
subsequent gain interpolation.
"""
require_nonnegative(t_current_s, "t_current_s")
self._applied_at = t_current_s
|
effective_gain(t_current_s)
Return the active firing-rate gain at an experiment timestamp.
Parameters
t_current_s:
Experiment time in seconds.
Returns
float
1.0 before application, a linearly interpolated onset gain
during onset_delay_s, or the configured steady-state gain after
onset.
Source code in src/sc_neurocore/bioware/bioware_experiment.py
| Python |
|---|
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89 | def effective_gain(self, t_current_s: float) -> float:
"""Return the active firing-rate gain at an experiment timestamp.
Parameters
----------
t_current_s:
Experiment time in seconds.
Returns
-------
float
``1.0`` before application, a linearly interpolated onset gain
during ``onset_delay_s``, or the configured steady-state gain after
onset.
"""
require_nonnegative(t_current_s, "t_current_s")
if self._applied_at < 0:
return 1.0
elapsed = t_current_s - self._applied_at
if elapsed < 0.0:
raise ValueError("t_current_s must not precede the application time")
if elapsed < self.onset_delay_s:
frac = elapsed / self.onset_delay_s
return 1.0 + frac * (self.gain - 1.0)
return self.gain
|
modulate_spikes(spike_counts, t_current_s)
Modulate spike counts by pharmacological gain.
Source code in src/sc_neurocore/bioware/bioware_experiment.py
| Python |
|---|
91
92
93
94
95
96
97
98
99
100
101
102
103
104 | def modulate_spikes(
self, spike_counts: np.ndarray[Any, Any], t_current_s: float
) -> np.ndarray[Any, Any]:
"""Modulate spike counts by pharmacological gain."""
if not isinstance(spike_counts, np.ndarray):
raise TypeError("spike_counts must be a NumPy array")
if spike_counts.ndim != 1:
raise ValueError("spike_counts must be one-dimensional")
if not np.issubdtype(spike_counts.dtype, np.number):
raise TypeError("spike_counts must have a numeric dtype")
if not np.all(np.isfinite(spike_counts)) or np.any(spike_counts < 0):
raise ValueError("spike_counts must contain finite non-negative values")
g = self.effective_gain(t_current_s)
return np.round(spike_counts * g).astype(int)
|
modulate_spike_events(spikes, t_current_s)
Apply pharmacological rate gain to spike events.
Inhibitory gains deterministically thin events across the observed
response span instead of truncating the head of the frame. Excitatory
gains preserve observed events and insert synthetic events inside the
observed temporal support, using nearest observed spikes as channel,
unit, amplitude, and waveform templates.
Source code in src/sc_neurocore/bioware/bioware_experiment.py
| Python |
|---|
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159 | def modulate_spike_events(
self,
spikes: List[DetectedSpike],
t_current_s: float,
) -> List[DetectedSpike]:
"""Apply pharmacological rate gain to spike events.
Inhibitory gains deterministically thin events across the observed
response span instead of truncating the head of the frame. Excitatory
gains preserve observed events and insert synthetic events inside the
observed temporal support, using nearest observed spikes as channel,
unit, amplitude, and waveform templates.
"""
require_nonnegative(t_current_s, "t_current_s")
if not spikes:
return []
gain = self.effective_gain(t_current_s)
if not math.isfinite(gain) or gain < 0.0:
raise ValueError("pharmacological gain must be finite and >= 0")
ordered = sorted(spikes, key=lambda s: (s.timestamp_s, s.channel, s.unit_id))
target_count = int(round(len(ordered) * gain))
if target_count <= 0:
return []
if target_count == len(ordered):
return list(ordered)
if target_count < len(ordered):
indices = _quantile_indices(len(ordered), target_count)
return [ordered[i] for i in indices]
extra = target_count - len(ordered)
timestamps = np.array([s.timestamp_s for s in ordered], dtype=float)
if not np.all(np.isfinite(timestamps)):
raise ValueError("detected spike timestamps must be finite")
if len(ordered) == 1 or timestamps[-1] <= timestamps[0]:
synthetic = [
_clone_spike(ordered[0], timestamp_s=float(timestamps[0])) for _ in range(extra)
]
else:
insert_times = np.linspace(timestamps[0], timestamps[-1], extra + 2)[1:-1]
synthetic = []
for t in insert_times:
# insert_times are strictly interior to (timestamps[0],
# timestamps[-1]) — the linspace endpoints are dropped — so for
# t < timestamps[-1] a left-side searchsorted always yields
# idx <= len(ordered) - 1; no upper clamp is reachable.
idx = int(np.searchsorted(timestamps, t, side="left"))
if idx > 0 and abs(timestamps[idx - 1] - t) <= abs(timestamps[idx] - t):
idx -= 1
synthetic.append(_clone_spike(ordered[idx], timestamp_s=float(t)))
return sorted([*ordered, *synthetic], key=lambda s: (s.timestamp_s, s.channel, s.unit_id))
|
WellConfig
dataclass
One well in a multi-well MEA plate.
Source code in src/sc_neurocore/bioware/bioware_experiment.py
| Python |
|---|
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210 | @dataclass
class WellConfig:
"""One well in a multi-well MEA plate."""
well_id: str
mea_config: MEAConfig
culture_type: str = "cortical"
passage_number: int = 0
def __post_init__(self) -> None:
"""Validate well identity, culture label, passage, and MEA config."""
if not self.well_id or not self.well_id.strip():
raise ValueError("well_id must not be empty")
if not isinstance(self.mea_config, MEAConfig):
raise TypeError("mea_config must be an MEAConfig")
if not self.culture_type or not self.culture_type.strip():
raise ValueError("culture_type must not be empty")
require_nonnegative_int(self.passage_number, "passage_number")
@property
def label(self) -> str:
"""Return the stable plate label for this well.
Returns
-------
str
Identifier combining well ID, culture type, and passage number.
"""
return f"{self.well_id}_{self.culture_type}_P{self.passage_number}"
|
label
property
Return the stable plate label for this well.
Returns
str
Identifier combining well ID, culture type, and passage number.
__post_init__()
Validate well identity, culture label, passage, and MEA config.
Source code in src/sc_neurocore/bioware/bioware_experiment.py
| Python |
|---|
191
192
193
194
195
196
197
198
199 | def __post_init__(self) -> None:
"""Validate well identity, culture label, passage, and MEA config."""
if not self.well_id or not self.well_id.strip():
raise ValueError("well_id must not be empty")
if not isinstance(self.mea_config, MEAConfig):
raise TypeError("mea_config must be an MEAConfig")
if not self.culture_type or not self.culture_type.strip():
raise ValueError("culture_type must not be empty")
require_nonnegative_int(self.passage_number, "passage_number")
|
BCMPlasticity
dataclass
Bienenstock-Cooper-Munro plasticity adapter.
Implements sliding-threshold BCM rule where the modification
threshold θ tracks the postsynaptic firing rate. Converts
biological firing rates to Q8.8 weight deltas.
Source code in src/sc_neurocore/bioware/bioware_plasticity.py
| Python |
|---|
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140 | @dataclass
class BCMPlasticity:
"""Bienenstock-Cooper-Munro plasticity adapter.
Implements sliding-threshold BCM rule where the modification
threshold θ tracks the postsynaptic firing rate. Converts
biological firing rates to Q8.8 weight deltas.
"""
tau_theta_ms: float = 1000.0 # threshold adaptation time constant
learning_rate: float = 0.001
theta: float = 0.0 # sliding threshold (internal state)
w_max_q88: int = 512
w_min_q88: int = 0
def __post_init__(self) -> None:
"""Validate BCM dynamics and Q8.8 weight bounds."""
require_positive(self.tau_theta_ms, "tau_theta_ms")
require_nonnegative(self.learning_rate, "learning_rate")
require_nonnegative(self.theta, "theta")
require_nonnegative_int(self.w_min_q88, "w_min_q88")
require_nonnegative_int(self.w_max_q88, "w_max_q88")
if self.w_max_q88 < self.w_min_q88:
raise ValueError("w_max_q88 must be >= w_min_q88")
def update_theta(self, post_rate_hz: float, dt_ms: float) -> float:
"""Update the sliding threshold from postsynaptic activity."""
require_nonnegative(post_rate_hz, "post_rate_hz")
require_nonnegative(dt_ms, "dt_ms")
alpha = dt_ms / self.tau_theta_ms
target = post_rate_hz**2
self.theta += alpha * (target - self.theta)
return self.theta
def compute_dw(self, pre_rate_hz: float, post_rate_hz: float) -> float:
"""BCM weight change: ΔW = η * x * y * (y - θ)."""
require_nonnegative(pre_rate_hz, "pre_rate_hz")
require_nonnegative(post_rate_hz, "post_rate_hz")
require_nonnegative(self.theta, "theta")
return self.learning_rate * pre_rate_hz * post_rate_hz * (post_rate_hz - self.theta)
def update_weight(self, current_q88: int, pre_rate: float, post_rate: float) -> int:
"""Apply the BCM update to a saturated Q8.8 synaptic weight.
Parameters
----------
current_q88:
Current synaptic weight encoded as Q8.8.
pre_rate:
Presynaptic firing rate in hertz.
post_rate:
Postsynaptic firing rate in hertz.
Returns
-------
int
Updated Q8.8 weight clamped to ``[w_min_q88, w_max_q88]``.
"""
require_nonnegative_int(current_q88, "current_q88")
if not self.w_min_q88 <= current_q88 <= self.w_max_q88:
raise ValueError("current_q88 must be inside configured weight bounds")
dw = self.compute_dw(pre_rate, post_rate)
dw_q88 = int(dw * 256)
new_w = current_q88 + dw_q88
return max(self.w_min_q88, min(self.w_max_q88, new_w))
|
__post_init__()
Validate BCM dynamics and Q8.8 weight bounds.
Source code in src/sc_neurocore/bioware/bioware_plasticity.py
| Python |
|---|
91
92
93
94
95
96
97
98
99 | def __post_init__(self) -> None:
"""Validate BCM dynamics and Q8.8 weight bounds."""
require_positive(self.tau_theta_ms, "tau_theta_ms")
require_nonnegative(self.learning_rate, "learning_rate")
require_nonnegative(self.theta, "theta")
require_nonnegative_int(self.w_min_q88, "w_min_q88")
require_nonnegative_int(self.w_max_q88, "w_max_q88")
if self.w_max_q88 < self.w_min_q88:
raise ValueError("w_max_q88 must be >= w_min_q88")
|
update_theta(post_rate_hz, dt_ms)
Update the sliding threshold from postsynaptic activity.
Source code in src/sc_neurocore/bioware/bioware_plasticity.py
| Python |
|---|
101
102
103
104
105
106
107
108 | def update_theta(self, post_rate_hz: float, dt_ms: float) -> float:
"""Update the sliding threshold from postsynaptic activity."""
require_nonnegative(post_rate_hz, "post_rate_hz")
require_nonnegative(dt_ms, "dt_ms")
alpha = dt_ms / self.tau_theta_ms
target = post_rate_hz**2
self.theta += alpha * (target - self.theta)
return self.theta
|
compute_dw(pre_rate_hz, post_rate_hz)
BCM weight change: ΔW = η * x * y * (y - θ).
Source code in src/sc_neurocore/bioware/bioware_plasticity.py
| Python |
|---|
| def compute_dw(self, pre_rate_hz: float, post_rate_hz: float) -> float:
"""BCM weight change: ΔW = η * x * y * (y - θ)."""
require_nonnegative(pre_rate_hz, "pre_rate_hz")
require_nonnegative(post_rate_hz, "post_rate_hz")
require_nonnegative(self.theta, "theta")
return self.learning_rate * pre_rate_hz * post_rate_hz * (post_rate_hz - self.theta)
|
update_weight(current_q88, pre_rate, post_rate)
Apply the BCM update to a saturated Q8.8 synaptic weight.
Parameters
current_q88:
Current synaptic weight encoded as Q8.8.
pre_rate:
Presynaptic firing rate in hertz.
post_rate:
Postsynaptic firing rate in hertz.
Returns
int
Updated Q8.8 weight clamped to [w_min_q88, w_max_q88].
Source code in src/sc_neurocore/bioware/bioware_plasticity.py
| Python |
|---|
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140 | def update_weight(self, current_q88: int, pre_rate: float, post_rate: float) -> int:
"""Apply the BCM update to a saturated Q8.8 synaptic weight.
Parameters
----------
current_q88:
Current synaptic weight encoded as Q8.8.
pre_rate:
Presynaptic firing rate in hertz.
post_rate:
Postsynaptic firing rate in hertz.
Returns
-------
int
Updated Q8.8 weight clamped to ``[w_min_q88, w_max_q88]``.
"""
require_nonnegative_int(current_q88, "current_q88")
if not self.w_min_q88 <= current_q88 <= self.w_max_q88:
raise ValueError("current_q88 must be inside configured weight bounds")
dw = self.compute_dw(pre_rate, post_rate)
dw_q88 = int(dw * 256)
new_w = current_q88 + dw_q88
return max(self.w_min_q88, min(self.w_max_q88, new_w))
|
BiologicalSTDP
dataclass
Spike-Timing-Dependent Plasticity adapter for bio-hybrid loops.
Bridges biological STDP time constants (∼20 ms) to SC clock
rates (MHz) via a time-scaling factor. Computes ΔW from
pre/post spike timing in biological time, then converts to
Q8.8 weight updates for the SC domain.
Source code in src/sc_neurocore/bioware/bioware_plasticity.py
| Python |
|---|
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73 | @dataclass
class BiologicalSTDP:
"""Spike-Timing-Dependent Plasticity adapter for bio-hybrid loops.
Bridges biological STDP time constants (∼20 ms) to SC clock
rates (MHz) via a time-scaling factor. Computes ΔW from
pre/post spike timing in biological time, then converts to
Q8.8 weight updates for the SC domain.
"""
tau_plus_ms: float = 20.0 # potentiation time constant
tau_minus_ms: float = 20.0 # depression time constant
a_plus: float = 0.01 # potentiation amplitude
a_minus: float = 0.012 # depression amplitude (slightly > a_plus)
w_max_q88: int = 512 # Q8.8 = 2.0
w_min_q88: int = 0
def __post_init__(self) -> None:
"""Validate time constants, amplitudes, and Q8.8 bounds."""
require_positive(self.tau_plus_ms, "tau_plus_ms")
require_positive(self.tau_minus_ms, "tau_minus_ms")
require_nonnegative(self.a_plus, "a_plus")
require_nonnegative(self.a_minus, "a_minus")
require_nonnegative_int(self.w_min_q88, "w_min_q88")
require_nonnegative_int(self.w_max_q88, "w_max_q88")
if self.w_max_q88 < self.w_min_q88:
raise ValueError("w_max_q88 must be >= w_min_q88")
def compute_dw(self, dt_ms: float) -> float:
"""Compute weight change from spike timing difference.
dt_ms = t_post - t_pre (positive = potentiation, negative = depression)
"""
require_finite(dt_ms, "dt_ms")
if dt_ms > 0:
return float(self.a_plus * np.exp(-dt_ms / self.tau_plus_ms))
elif dt_ms < 0:
return float(-self.a_minus * np.exp(dt_ms / self.tau_minus_ms))
return 0.0
def update_weight(self, current_q88: int, dt_ms: float) -> int:
"""Update Q8.8 weight from spike timing."""
require_nonnegative_int(current_q88, "current_q88")
if not self.w_min_q88 <= current_q88 <= self.w_max_q88:
raise ValueError("current_q88 must be inside configured weight bounds")
dw = self.compute_dw(dt_ms)
dw_q88 = int(dw * 256) # Convert to Q8.8
new_w = current_q88 + dw_q88
return max(self.w_min_q88, min(self.w_max_q88, new_w))
|
__post_init__()
Validate time constants, amplitudes, and Q8.8 bounds.
Source code in src/sc_neurocore/bioware/bioware_plasticity.py
| Python |
|---|
42
43
44
45
46
47
48
49
50
51 | def __post_init__(self) -> None:
"""Validate time constants, amplitudes, and Q8.8 bounds."""
require_positive(self.tau_plus_ms, "tau_plus_ms")
require_positive(self.tau_minus_ms, "tau_minus_ms")
require_nonnegative(self.a_plus, "a_plus")
require_nonnegative(self.a_minus, "a_minus")
require_nonnegative_int(self.w_min_q88, "w_min_q88")
require_nonnegative_int(self.w_max_q88, "w_max_q88")
if self.w_max_q88 < self.w_min_q88:
raise ValueError("w_max_q88 must be >= w_min_q88")
|
compute_dw(dt_ms)
Compute weight change from spike timing difference.
dt_ms = t_post - t_pre (positive = potentiation, negative = depression)
Source code in src/sc_neurocore/bioware/bioware_plasticity.py
| Python |
|---|
53
54
55
56
57
58
59
60
61
62
63 | def compute_dw(self, dt_ms: float) -> float:
"""Compute weight change from spike timing difference.
dt_ms = t_post - t_pre (positive = potentiation, negative = depression)
"""
require_finite(dt_ms, "dt_ms")
if dt_ms > 0:
return float(self.a_plus * np.exp(-dt_ms / self.tau_plus_ms))
elif dt_ms < 0:
return float(-self.a_minus * np.exp(dt_ms / self.tau_minus_ms))
return 0.0
|
update_weight(current_q88, dt_ms)
Update Q8.8 weight from spike timing.
Source code in src/sc_neurocore/bioware/bioware_plasticity.py
| Python |
|---|
65
66
67
68
69
70
71
72
73 | def update_weight(self, current_q88: int, dt_ms: float) -> int:
"""Update Q8.8 weight from spike timing."""
require_nonnegative_int(current_q88, "current_q88")
if not self.w_min_q88 <= current_q88 <= self.w_max_q88:
raise ValueError("current_q88 must be inside configured weight bounds")
dw = self.compute_dw(dt_ms)
dw_q88 = int(dw * 256) # Convert to Q8.8
new_w = current_q88 + dw_q88
return max(self.w_min_q88, min(self.w_max_q88, new_w))
|
HomeostaticPlasticity
dataclass
Intrinsic excitability scaling to maintain target firing rate.
Implements homeostatic plasticity: if a neuron fires too fast,
reduce its excitability (threshold up); too slow, increase it.
Operates on Q8.8 threshold values.
Source code in src/sc_neurocore/bioware/bioware_plasticity.py
| Python |
|---|
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191 | @dataclass
class HomeostaticPlasticity:
"""Intrinsic excitability scaling to maintain target firing rate.
Implements homeostatic plasticity: if a neuron fires too fast,
reduce its excitability (threshold up); too slow, increase it.
Operates on Q8.8 threshold values.
"""
target_rate_hz: float = 10.0
tau_homeo_ms: float = 10000.0 # slow timescale (seconds)
max_threshold_q88: int = 512 # Q8.8 = 2.0
min_threshold_q88: int = 64 # Q8.8 = 0.25
def __post_init__(self) -> None:
"""Validate target dynamics and Q8.8 threshold bounds."""
require_nonnegative(self.target_rate_hz, "target_rate_hz")
require_positive(self.tau_homeo_ms, "tau_homeo_ms")
require_nonnegative_int(self.min_threshold_q88, "min_threshold_q88")
require_nonnegative_int(self.max_threshold_q88, "max_threshold_q88")
if self.max_threshold_q88 < self.min_threshold_q88:
raise ValueError("max_threshold_q88 must be >= min_threshold_q88")
def update_threshold(
self,
current_q88: int,
observed_rate_hz: float,
dt_ms: float,
) -> int:
"""Adjust threshold to drive firing rate toward target.
Proportional homeostatic controller on a Q8.8 fixed-point
threshold. ``alpha = dt_ms / tau_homeo_ms`` is the integration
weight over the time step; the rate error (``observed − target``)
is scaled by ``alpha·256`` so that a 1 Hz error integrated over
one full time-constant shifts the threshold by 1.0 Q8.8 unit
(i.e. by ``256`` in integer representation). Result clamped to
``[min_threshold_q88, max_threshold_q88]``.
"""
require_nonnegative_int(current_q88, "current_q88")
if not self.min_threshold_q88 <= current_q88 <= self.max_threshold_q88:
raise ValueError("current_q88 must be inside configured threshold bounds")
require_nonnegative(observed_rate_hz, "observed_rate_hz")
require_nonnegative(dt_ms, "dt_ms")
error = observed_rate_hz - self.target_rate_hz
alpha = dt_ms / self.tau_homeo_ms
delta_q88 = int(alpha * error * 256.0)
new_q88 = current_q88 + delta_q88
return max(self.min_threshold_q88, min(self.max_threshold_q88, new_q88))
|
__post_init__()
Validate target dynamics and Q8.8 threshold bounds.
Source code in src/sc_neurocore/bioware/bioware_plasticity.py
| Python |
|---|
157
158
159
160
161
162
163
164 | def __post_init__(self) -> None:
"""Validate target dynamics and Q8.8 threshold bounds."""
require_nonnegative(self.target_rate_hz, "target_rate_hz")
require_positive(self.tau_homeo_ms, "tau_homeo_ms")
require_nonnegative_int(self.min_threshold_q88, "min_threshold_q88")
require_nonnegative_int(self.max_threshold_q88, "max_threshold_q88")
if self.max_threshold_q88 < self.min_threshold_q88:
raise ValueError("max_threshold_q88 must be >= min_threshold_q88")
|
update_threshold(current_q88, observed_rate_hz, dt_ms)
Adjust threshold to drive firing rate toward target.
Proportional homeostatic controller on a Q8.8 fixed-point
threshold. alpha = dt_ms / tau_homeo_ms is the integration
weight over the time step; the rate error (observed − target)
is scaled by alpha·256 so that a 1 Hz error integrated over
one full time-constant shifts the threshold by 1.0 Q8.8 unit
(i.e. by 256 in integer representation). Result clamped to
[min_threshold_q88, max_threshold_q88].
Source code in src/sc_neurocore/bioware/bioware_plasticity.py
| Python |
|---|
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191 | def update_threshold(
self,
current_q88: int,
observed_rate_hz: float,
dt_ms: float,
) -> int:
"""Adjust threshold to drive firing rate toward target.
Proportional homeostatic controller on a Q8.8 fixed-point
threshold. ``alpha = dt_ms / tau_homeo_ms`` is the integration
weight over the time step; the rate error (``observed − target``)
is scaled by ``alpha·256`` so that a 1 Hz error integrated over
one full time-constant shifts the threshold by 1.0 Q8.8 unit
(i.e. by ``256`` in integer representation). Result clamped to
``[min_threshold_q88, max_threshold_q88]``.
"""
require_nonnegative_int(current_q88, "current_q88")
if not self.min_threshold_q88 <= current_q88 <= self.max_threshold_q88:
raise ValueError("current_q88 must be inside configured threshold bounds")
require_nonnegative(observed_rate_hz, "observed_rate_hz")
require_nonnegative(dt_ms, "dt_ms")
error = observed_rate_hz - self.target_rate_hz
alpha = dt_ms / self.tau_homeo_ms
delta_q88 = int(alpha * error * 256.0)
new_q88 = current_q88 + delta_q88
return max(self.min_threshold_q88, min(self.max_threshold_q88, new_q88))
|
BioHybridSession
dataclass
Manages a complete bio-hybrid experiment session.
Orchestrates MEA recording, spike detection, AER transcoding, stochastic
conversion, optogenetic feedback, and culture-health assessment. The
stdp and homeostatic policies are retained for caller-managed
updates; process_frame does not mutate plasticity state implicitly.
Source code in src/sc_neurocore/bioware/bioware_session.py
| Python |
|---|
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192 | @dataclass
class BioHybridSession:
"""Manages a complete bio-hybrid experiment session.
Orchestrates MEA recording, spike detection, AER transcoding, stochastic
conversion, optogenetic feedback, and culture-health assessment. The
``stdp`` and ``homeostatic`` policies are retained for caller-managed
updates; ``process_frame`` does not mutate plasticity state implicitly.
"""
mea_config: MEAConfig
detector: SpikeDetector
transcoder: MEAToAERTranscoder
sc_converter: AERToSCConverter
opto_encoder: SCToOptoEncoder
stdp: BiologicalSTDP = field(default_factory=BiologicalSTDP)
health_monitor: CultureHealth = field(default_factory=CultureHealth)
artifact_rejector: Optional["ArtifactRejector"] = None
pharm_model: Optional["PharmModel"] = None
latency_budget: Optional["LatencyBudget"] = None
homeostatic: Optional["HomeostaticPlasticity"] = None
sorter: Optional["SpikeSorter"] = None
zenith_core: Optional["ArcaneZenithCognitiveCore"] = None
round_count: int = 0
def __post_init__(self) -> None:
"""Validate component compatibility before processing live data."""
if not isinstance(self.mea_config, MEAConfig):
raise TypeError("mea_config must be an MEAConfig")
if not isinstance(self.detector, SpikeDetector):
raise TypeError("detector must be a SpikeDetector")
if self.detector.config != self.mea_config:
raise ValueError("detector.config must match mea_config")
if not isinstance(self.transcoder, MEAToAERTranscoder):
raise TypeError("transcoder must be a MEAToAERTranscoder")
if not isinstance(self.sc_converter, AERToSCConverter):
raise TypeError("sc_converter must be an AERToSCConverter")
if not isinstance(self.opto_encoder, SCToOptoEncoder):
raise TypeError("opto_encoder must be an SCToOptoEncoder")
if not isinstance(self.stdp, BiologicalSTDP):
raise TypeError("stdp must be a BiologicalSTDP")
if not isinstance(self.health_monitor, CultureHealth):
raise TypeError("health_monitor must be a CultureHealth")
if self.artifact_rejector is not None and not isinstance(
self.artifact_rejector, ArtifactRejector
):
raise TypeError("artifact_rejector must be an ArtifactRejector or None")
if self.pharm_model is not None and not isinstance(self.pharm_model, PharmModel):
raise TypeError("pharm_model must be a PharmModel or None")
if self.latency_budget is not None and not isinstance(self.latency_budget, LatencyBudget):
raise TypeError("latency_budget must be a LatencyBudget or None")
if self.homeostatic is not None and not isinstance(self.homeostatic, HomeostaticPlasticity):
raise TypeError("homeostatic must be a HomeostaticPlasticity or None")
if self.sorter is not None and not isinstance(self.sorter, SpikeSorter):
raise TypeError("sorter must be a SpikeSorter or None")
if self.zenith_core is not None and not callable(
getattr(self.zenith_core, "step_from_bio_rates", None)
):
raise TypeError("zenith_core must provide step_from_bio_rates or be None")
if self.sc_converter.num_neurons < self.mea_config.num_channels:
raise ValueError("sc_converter.num_neurons must cover every MEA channel")
if self.transcoder.channel_map is not None:
for neuron_id in self.transcoder.channel_map.values():
if neuron_id >= self.sc_converter.num_neurons:
raise ValueError("channel_map targets must fit sc_converter.num_neurons")
require_nonnegative_int(self.round_count, "round_count")
def process_frame(
self,
voltage_data: np.ndarray[Any, Any],
t_start_s: float = 0.0,
stim_times_s: Optional[List[float]] = None,
) -> BioHybridFrameResult:
"""Process one frame whose timestamps fit one AER counter epoch.
Detector timestamps are frame-relative. ``t_start_s`` is the
non-negative experiment time used by optional experiment models; it
does not change the frame-local AER timestamp origin.
"""
validate_voltage_matrix(
voltage_data,
expected_channels=self.mea_config.num_channels,
)
require_nonnegative(t_start_s, "t_start_s")
max_frame_tick = int(
((voltage_data.shape[0] - 1) / self.mea_config.sample_rate_hz)
* self.transcoder.hw_clock_hz
)
if max_frame_tick > 0xFFFF:
raise ValueError("voltage frame exceeds the 16-bit AER timestamp epoch")
if max_frame_tick >= self.sc_converter.window_ticks:
raise ValueError("voltage frame exceeds sc_converter.window_ticks")
t0 = time.perf_counter_ns()
next_round = self.round_count + 1
if self.artifact_rejector is not None and stim_times_s is not None:
voltage_data = self.artifact_rejector.blank(
voltage_data, stim_times_s, self.mea_config.sample_rate_hz
)
# 1. Detect spikes
spikes = self.detector.detect(voltage_data)
# 1.5 Core primitive wiring
if self.sorter is not None:
spikes = self.sorter.assign(spikes)
if self.pharm_model is not None:
spikes = self.pharm_model.modulate_spike_events(spikes, t_start_s)
# 2. Transcode to AER
aer_events = self.transcoder.transcode(spikes)
# 3. Convert to SC bitstreams
bitstreams = self.sc_converter.convert(aer_events)
# 3.5 Zenith integration!
if self.zenith_core is not None:
rates = decode_bitstream_rate(bitstreams)
self.zenith_core.step_from_bio_rates(rates)
# 4. Generate optogenetic pulses
opto_pulses = self.opto_encoder.encode(bitstreams)
# 5. Health assessment
n_channels = voltage_data.shape[1]
spike_counts = np.zeros(n_channels)
for s in spikes:
if s.channel < n_channels:
spike_counts[s.channel] += 1
duration = voltage_data.shape[0] / self.mea_config.sample_rate_hz
health = self.health_monitor.assess(spike_counts, duration_s=duration)
latency_us = (time.perf_counter_ns() - t0) / 1000.0
result = BioHybridFrameResult(
round=next_round,
num_spikes=len(spikes),
num_aer_events=len(aer_events),
num_bitstreams=len(bitstreams),
num_opto_pulses=len(opto_pulses),
latency_us=latency_us,
health=health,
spikes=spikes,
aer_events=aer_events,
bitstreams=bitstreams,
opto_pulses=opto_pulses,
)
if self.latency_budget is not None:
self.latency_budget.record(latency_us)
self.round_count = next_round
return result
|
__post_init__()
Validate component compatibility before processing live data.
Source code in src/sc_neurocore/bioware/bioware_session.py
| Python |
|---|
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105 | def __post_init__(self) -> None:
"""Validate component compatibility before processing live data."""
if not isinstance(self.mea_config, MEAConfig):
raise TypeError("mea_config must be an MEAConfig")
if not isinstance(self.detector, SpikeDetector):
raise TypeError("detector must be a SpikeDetector")
if self.detector.config != self.mea_config:
raise ValueError("detector.config must match mea_config")
if not isinstance(self.transcoder, MEAToAERTranscoder):
raise TypeError("transcoder must be a MEAToAERTranscoder")
if not isinstance(self.sc_converter, AERToSCConverter):
raise TypeError("sc_converter must be an AERToSCConverter")
if not isinstance(self.opto_encoder, SCToOptoEncoder):
raise TypeError("opto_encoder must be an SCToOptoEncoder")
if not isinstance(self.stdp, BiologicalSTDP):
raise TypeError("stdp must be a BiologicalSTDP")
if not isinstance(self.health_monitor, CultureHealth):
raise TypeError("health_monitor must be a CultureHealth")
if self.artifact_rejector is not None and not isinstance(
self.artifact_rejector, ArtifactRejector
):
raise TypeError("artifact_rejector must be an ArtifactRejector or None")
if self.pharm_model is not None and not isinstance(self.pharm_model, PharmModel):
raise TypeError("pharm_model must be a PharmModel or None")
if self.latency_budget is not None and not isinstance(self.latency_budget, LatencyBudget):
raise TypeError("latency_budget must be a LatencyBudget or None")
if self.homeostatic is not None and not isinstance(self.homeostatic, HomeostaticPlasticity):
raise TypeError("homeostatic must be a HomeostaticPlasticity or None")
if self.sorter is not None and not isinstance(self.sorter, SpikeSorter):
raise TypeError("sorter must be a SpikeSorter or None")
if self.zenith_core is not None and not callable(
getattr(self.zenith_core, "step_from_bio_rates", None)
):
raise TypeError("zenith_core must provide step_from_bio_rates or be None")
if self.sc_converter.num_neurons < self.mea_config.num_channels:
raise ValueError("sc_converter.num_neurons must cover every MEA channel")
if self.transcoder.channel_map is not None:
for neuron_id in self.transcoder.channel_map.values():
if neuron_id >= self.sc_converter.num_neurons:
raise ValueError("channel_map targets must fit sc_converter.num_neurons")
require_nonnegative_int(self.round_count, "round_count")
|
process_frame(voltage_data, t_start_s=0.0, stim_times_s=None)
Process one frame whose timestamps fit one AER counter epoch.
Detector timestamps are frame-relative. t_start_s is the
non-negative experiment time used by optional experiment models; it
does not change the frame-local AER timestamp origin.
Source code in src/sc_neurocore/bioware/bioware_session.py
| Python |
|---|
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192 | def process_frame(
self,
voltage_data: np.ndarray[Any, Any],
t_start_s: float = 0.0,
stim_times_s: Optional[List[float]] = None,
) -> BioHybridFrameResult:
"""Process one frame whose timestamps fit one AER counter epoch.
Detector timestamps are frame-relative. ``t_start_s`` is the
non-negative experiment time used by optional experiment models; it
does not change the frame-local AER timestamp origin.
"""
validate_voltage_matrix(
voltage_data,
expected_channels=self.mea_config.num_channels,
)
require_nonnegative(t_start_s, "t_start_s")
max_frame_tick = int(
((voltage_data.shape[0] - 1) / self.mea_config.sample_rate_hz)
* self.transcoder.hw_clock_hz
)
if max_frame_tick > 0xFFFF:
raise ValueError("voltage frame exceeds the 16-bit AER timestamp epoch")
if max_frame_tick >= self.sc_converter.window_ticks:
raise ValueError("voltage frame exceeds sc_converter.window_ticks")
t0 = time.perf_counter_ns()
next_round = self.round_count + 1
if self.artifact_rejector is not None and stim_times_s is not None:
voltage_data = self.artifact_rejector.blank(
voltage_data, stim_times_s, self.mea_config.sample_rate_hz
)
# 1. Detect spikes
spikes = self.detector.detect(voltage_data)
# 1.5 Core primitive wiring
if self.sorter is not None:
spikes = self.sorter.assign(spikes)
if self.pharm_model is not None:
spikes = self.pharm_model.modulate_spike_events(spikes, t_start_s)
# 2. Transcode to AER
aer_events = self.transcoder.transcode(spikes)
# 3. Convert to SC bitstreams
bitstreams = self.sc_converter.convert(aer_events)
# 3.5 Zenith integration!
if self.zenith_core is not None:
rates = decode_bitstream_rate(bitstreams)
self.zenith_core.step_from_bio_rates(rates)
# 4. Generate optogenetic pulses
opto_pulses = self.opto_encoder.encode(bitstreams)
# 5. Health assessment
n_channels = voltage_data.shape[1]
spike_counts = np.zeros(n_channels)
for s in spikes:
if s.channel < n_channels:
spike_counts[s.channel] += 1
duration = voltage_data.shape[0] / self.mea_config.sample_rate_hz
health = self.health_monitor.assess(spike_counts, duration_s=duration)
latency_us = (time.perf_counter_ns() - t0) / 1000.0
result = BioHybridFrameResult(
round=next_round,
num_spikes=len(spikes),
num_aer_events=len(aer_events),
num_bitstreams=len(bitstreams),
num_opto_pulses=len(opto_pulses),
latency_us=latency_us,
health=health,
spikes=spikes,
aer_events=aer_events,
bitstreams=bitstreams,
opto_pulses=opto_pulses,
)
if self.latency_budget is not None:
self.latency_budget.record(latency_us)
self.round_count = next_round
return result
|
detect_network_bursts(spikes, bin_width_s=0.01, threshold_sigma=3.0, min_channels=3)
Detect network-wide synchronised bursts.
Bins spikes in time, detects bins with activity > threshold_sigma
above the mean, and requires participation from ≥ min_channels.
Source code in src/sc_neurocore/bioware/bioware_analysis.py
| Python |
|---|
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273 | def detect_network_bursts(
spikes: List[DetectedSpike],
bin_width_s: float = 0.01,
threshold_sigma: float = 3.0,
min_channels: int = 3,
) -> List[NetworkBurst]:
"""Detect network-wide synchronised bursts.
Bins spikes in time, detects bins with activity > threshold_sigma
above the mean, and requires participation from ≥ min_channels.
"""
require_positive(bin_width_s, "bin_width_s")
require_nonnegative(threshold_sigma, "threshold_sigma")
require_positive_int(min_channels, "min_channels")
if not spikes:
return []
timestamps = np.array([s.timestamp_s for s in spikes])
t_start, t_end = timestamps.min(), timestamps.max()
if t_end <= t_start:
return []
n_bins = max(1, int((t_end - t_start) / bin_width_s) + 1)
bin_counts = np.zeros(n_bins)
bin_channels: List[set[int]] = [set() for _ in range(n_bins)]
for s in spikes:
idx = min(int((s.timestamp_s - t_start) / bin_width_s), n_bins - 1)
bin_counts[idx] += 1
bin_channels[idx].add(s.channel)
mean_count = np.mean(bin_counts)
std_count = np.std(bin_counts)
if std_count == 0:
return []
threshold = mean_count + threshold_sigma * std_count
bursts = []
for i in range(n_bins):
if bin_counts[i] >= threshold and len(bin_channels[i]) >= min_channels:
bursts.append(
NetworkBurst(
onset_s=t_start + i * bin_width_s,
duration_s=bin_width_s,
participating_channels=len(bin_channels[i]),
total_spikes=int(bin_counts[i]),
)
)
return bursts
|
Extract per-channel power in each LFP band.
Uses FFT-based power spectral density estimation.
Returns dict of band_name → per-channel power array.
Source code in src/sc_neurocore/bioware/bioware_analysis.py
| Python |
|---|
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141 | def extract_lfp_power(
voltage_data: np.ndarray[Any, Any],
sample_rate_hz: float,
bands: Optional[List[LFPBand]] = None,
) -> Dict[str, np.ndarray[Any, Any]]:
"""Extract per-channel power in each LFP band.
Uses FFT-based power spectral density estimation.
Returns dict of band_name → per-channel power array.
"""
if bands is None:
bands = DEFAULT_LFP_BANDS
validate_voltage_matrix(voltage_data)
require_positive(sample_rate_hz, "sample_rate_hz")
if not bands:
raise ValueError("bands must not be empty")
names: set[str] = set()
for band in bands:
if not isinstance(band, LFPBand):
raise TypeError("bands must contain LFPBand instances")
if band.name in names:
raise ValueError(f"duplicate LFP band name: {band.name}")
names.add(band.name)
n_samples, n_channels = voltage_data.shape
freqs = np.fft.rfftfreq(n_samples, d=1.0 / sample_rate_hz)
fft_mag = np.abs(np.fft.rfft(voltage_data, axis=0)) ** 2
result = {}
for band in bands:
mask = (freqs >= band.low_hz) & (freqs < band.high_hz)
power = np.sum(fft_mag[mask, :], axis=0) if mask.any() else np.zeros(n_channels)
result[band.name] = power
return result
|
decode_bitstream_rate(bitstreams, sc_clock_hz=1000000.0)
Decode SC bitstreams back to biological firing rates (Hz).
Interprets popcount/length as probability, scales by SC clock
to get equivalent biological firing rate.
Source code in src/sc_neurocore/bioware/bioware_encoding.py
| Python |
|---|
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241 | def decode_bitstream_rate(
bitstreams: Dict[int, np.ndarray[Any, Any]],
sc_clock_hz: float = 1e6,
) -> Dict[int, float]:
"""Decode SC bitstreams back to biological firing rates (Hz).
Interprets popcount/length as probability, scales by SC clock
to get equivalent biological firing rate.
"""
require_positive(sc_clock_hz, "sc_clock_hz")
rates = {}
for nid, bs in bitstreams.items():
require_nonnegative_int(nid, "bitstream neuron_id")
validate_binary_bitstream(bs, name=f"bitstreams[{nid}]", allow_empty=True)
if len(bs) == 0:
rates[nid] = 0.0
continue
prob = float(np.sum(bs)) / len(bs)
rates[nid] = prob * sc_clock_hz
return rates
|
mea_fitness_hook(detected_spikes, target_rate=10.0, *, duration_s=None, stimulus_time_s=None, measured_latency_ms=None)
Organism fitness metrics derived from MEA response dynamics.
Designed to plug into the evo_substrate
ReplicationEngine(metrics_fn=mea_fitness_hook) — returns the
{"accuracy", "energy_mw", "latency_ms"} triple the engine scores.
Accuracy is a bounded distance to the target mean per-channel firing
rate when duration_s is supplied, or to the legacy per-channel
spike count when it is omitted. The legacy energy_mw key is a
dimensionless optimisation proxy equal to 0.5 * spike_count; it is
not a measured power or energy quantity. latency_ms is either a
caller-supplied closed-loop measurement, the first response latency after
stimulus_time_s, or the first spike timestamp relative to frame start.
Source code in src/sc_neurocore/bioware/bioware_fitness.py
| Python |
|---|
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85 | def mea_fitness_hook(
detected_spikes: List[DetectedSpike],
target_rate: float = 10.0,
*,
duration_s: Optional[float] = None,
stimulus_time_s: Optional[float] = None,
measured_latency_ms: Optional[float] = None,
) -> Dict[str, float]:
"""Organism fitness metrics derived from MEA response dynamics.
Designed to plug into the evo_substrate
``ReplicationEngine(metrics_fn=mea_fitness_hook)`` — returns the
``{"accuracy", "energy_mw", "latency_ms"}`` triple the engine scores.
Accuracy is a bounded distance to the target mean per-channel firing
rate when ``duration_s`` is supplied, or to the legacy per-channel
spike count when it is omitted. The legacy ``energy_mw`` key is a
dimensionless optimisation proxy equal to ``0.5 * spike_count``; it is
not a measured power or energy quantity. ``latency_ms`` is either a
caller-supplied closed-loop measurement, the first response latency after
``stimulus_time_s``, or the first spike timestamp relative to frame start.
"""
require_nonnegative(target_rate, "target_rate")
if duration_s is not None and (not math.isfinite(duration_s) or duration_s <= 0.0):
raise ValueError("duration_s must be finite and > 0 when provided")
if stimulus_time_s is not None and not math.isfinite(stimulus_time_s):
raise ValueError("stimulus_time_s must be finite when provided")
if measured_latency_ms is not None:
if not math.isfinite(measured_latency_ms) or measured_latency_ms < 0.0:
raise ValueError("measured_latency_ms must be finite and >= 0 when provided")
if not detected_spikes:
latency_ms = _mea_response_latency_ms(
detected_spikes,
stimulus_time_s=stimulus_time_s,
measured_latency_ms=measured_latency_ms,
)
return {"accuracy": 0.1, "energy_mw": 0.0, "latency_ms": latency_ms}
counts: Dict[int, float] = {}
for s in detected_spikes:
counts[s.channel] = counts.get(s.channel, 0.0) + 1.0
per_channel_activity = np.array(list(counts.values()), dtype=float)
if duration_s is not None:
per_channel_activity = per_channel_activity / duration_s
mean_rate = float(np.mean(per_channel_activity)) if per_channel_activity.size else 0.0
# Normalised distance to target rate → accuracy ∈ [0.1, 0.99].
if target_rate > 0.0:
accuracy = 1.0 - min(1.0, abs(mean_rate - target_rate) / target_rate)
else:
accuracy = 0.1
latency_ms = _mea_response_latency_ms(
detected_spikes,
stimulus_time_s=stimulus_time_s,
measured_latency_ms=measured_latency_ms,
)
return {
"accuracy": float(np.clip(accuracy, 0.1, 0.99)),
"energy_mw": float(len(detected_spikes) * 0.5),
"latency_ms": latency_ms,
}
|