Spike Train Analysis -- 124 Functions (23 Modules)¶
SC-NeuroCore includes a complete spike train analysis toolkit covering every metric from Elephant, PySpike, SpikeInterface, and NeuroTools. 127 functions across 23 modules (22 spike_stats + 1 explainability). Pure NumPy, zero external dependencies.
Rust acceleration: All 22 spike_stats modules have native Rust implementations with 96 PyO3 bindings (597 tests). See Rust Analysis Engine for the Rust API reference.
Quick Start¶
from sc_neurocore.analysis import (
firing_rate, cv_isi, fano_factor, cross_correlation,
power_spectrum, burst_detection, victor_purpura_distance,
phase_locking_value, spade_detect, gpfa,
)
import numpy as np
# Generate a Poisson spike train
from sc_neurocore.analysis import homogeneous_poisson
train = homogeneous_poisson(rate_hz=50.0, duration_s=10.0)
# Basic statistics
print(f"Rate: {firing_rate(train):.1f} Hz")
print(f"CV(ISI): {cv_isi(train):.2f}")
print(f"Fano factor: {fano_factor(train):.2f}")
Module Reference¶
Basic (spike_stats.basic)¶
Spike extraction, ISI computation, rate, count, binning.
| Function | Description |
|---|---|
spike_times(train, dt) |
Extract spike times (seconds) from a binary 0/1 array |
isi(train, dt) |
Inter-spike intervals (seconds) |
firing_rate(train, dt) |
Mean firing rate (Hz) |
spike_count(train) |
Total spike count |
bin_spike_train(train, bin_size) |
Bin a binary spike train into spike counts per bin |
Variability (spike_stats.variability)¶
Spike train regularity, complexity, and fractal measures.
| Function | Description |
|---|---|
cv_isi(train, dt) |
Coefficient of variation of ISI (CV=1 for Poisson, <1 regular) |
cv2(train, dt) |
Local CV2, less sensitive to rate changes (Holt et al. 1996) |
local_variation(train, dt) |
Local variation LV (Shinomoto et al. 2003) |
lvr(train, dt, refractoriness_ms) |
Revised local variation LvR corrected for refractoriness (Shinomoto et al. 2009) |
fano_factor(train, window_ms, dt) |
Fano factor: variance/mean of windowed spike counts |
isi_entropy(train, dt, bins) |
Shannon entropy of ISI distribution (bits) |
lempel_ziv_complexity(train) |
Lempel-Ziv 1976 complexity, normalised by N/log2(N) |
approximate_entropy(train, m, r_factor) |
Approximate entropy ApEn (Pincus 1991) |
sample_entropy(train, m, r_factor) |
Sample entropy SampEn, lower bias (Richman & Moorman 2000) |
permutation_entropy(train, order, delay) |
Bandt-Pompe permutation entropy, normalised to [0,1] |
hurst_exponent(train, min_window) |
Hurst exponent via detrended fluctuation analysis (Peng et al. 1994) |
allan_factor(train, dt, n_scales) |
Allan factor vs window size for fractal clustering detection |
rescaled_range(train, min_window) |
Hurst exponent via R/S analysis (Hurst 1951) |
complexity_pdf(train, dt, bins) |
ISI probability density function (Abeles 1982) |
optimal_bin_width(train, dt) |
Shimazaki-Shinomoto 2007 optimal histogram bin width |
optimal_kernel_bandwidth(train, dt) |
Silverman's rule-of-thumb bandwidth for ISI KDE |
Rate Estimation (spike_stats.rate)¶
Instantaneous and population firing rate estimation.
| Function | Description |
|---|---|
instantaneous_rate(train, dt, kernel, sigma_ms) |
Kernel-smoothed instantaneous rate (gaussian/exponential/rectangular) |
population_rate(trains, dt, sigma_ms) |
Population-level instantaneous rate across multiple neurons |
psth(trials, bin_ms, dt) |
Peri-stimulus time histogram across trials |
Distance Metrics (spike_stats.distance)¶
Spike train distance and similarity measures.
| Function | Description |
|---|---|
van_rossum_distance(a, b, dt, tau_ms) |
Van Rossum 2001 exponential-kernel distance |
victor_purpura_distance(times_a, times_b, cost_per_s) |
Victor-Purpura 1996 edit distance |
isi_distance(a, b, dt) |
ISI-distance ratio comparison (Kreuz et al. 2007) |
spike_distance(times_a, times_b, t_start, t_end) |
SPIKE-distance (Kreuz et al. 2013) |
spike_sync(times_a, times_b, t_start, t_end) |
SPIKE-synchronization, normalised to [0,1] (Kreuz et al. 2015) |
spike_sync_profile(times_a, times_b, n_bins, ...) |
Binned SPIKE-synchronization profile |
spike_profile(times_a, times_b, n_bins, ...) |
Binned SPIKE-distance profile |
isi_profile(a, b, dt, n_bins) |
Binned ISI-distance profile |
adaptive_spike_distance(times_a, times_b, ...) |
Adaptive SPIKE-distance with cost interpolation (Kreuz et al. 2013) |
schreiber_similarity(a, b, dt, sigma_ms) |
Smoothed Pearson correlation similarity (Schreiber et al. 2003) |
hunter_milton_similarity(times_a, times_b, dt_max) |
Nearest-neighbour coincidence fraction (Hunter-Milton 2003) |
earth_movers_distance(times_a, times_b, ...) |
EMD between spike time distributions (Rubner et al. 1998) |
multi_neuron_victor_purpura(times_list, cost) |
All-pairs Victor-Purpura distance matrix |
generalized_victor_purpura(times_a, times_b, cost_func) |
Victor-Purpura with arbitrary cost function |
spike_distance_matrix(times_list, metric, ...) |
All-pairs distance matrix (spike_distance/spike_sync/victor_purpura) |
Correlation (spike_stats.correlation)¶
Cross-correlation, synchrony, and covariance measures.
| Function | Description |
|---|---|
cross_correlation(a, b, max_lag_ms, dt) |
Cross-correlogram between two binary trains |
pairwise_correlation(trains, dt) |
Pairwise Pearson correlation matrix across neurons |
event_synchronization(a, b, dt, tau_ms) |
Quian Quiroga et al. 2002 event synchronization |
spike_train_coherence(a, b, dt) |
Magnitude-squared coherence between spike trains |
spike_time_tiling_coefficient(a, b, dt, delta_ms) |
STTC, corrects for rate bias (Cutts & Eglen 2014) |
covariance_matrix(trains, bin_size) |
Spike count covariance matrix (de la Rocha et al. 2007) |
autocorrelation_time(train, dt, max_lag_ms) |
Autocorrelation time until first zero crossing |
noise_correlation(trains, bin_size) |
Trial-to-trial variability correlation (Averbeck & Lee 2006) |
signal_correlation(trains, bin_size) |
Tuning similarity via Pearson correlation of mean responses |
spike_count_covariance(trains, window) |
Windowed spike count covariance (Kohn & Smith 2005) |
joint_psth(a, b, bin_size) |
Joint PSTH matrix (Aertsen et al. 1989) |
coincidence_index(a, b, dt, delta_ms) |
Rate-corrected coincidence index kappa (Joris et al. 2006) |
Spectral (spike_stats.spectral)¶
Spectral analysis of spike trains.
| Function | Description |
|---|---|
power_spectrum(train, dt) |
Power spectral density of a binary spike train |
Temporal Patterns (spike_stats.temporal)¶
Burst detection, latency, onset, and change point analysis.
| Function | Description |
|---|---|
burst_detection(train, dt, max_isi_ms, min_spikes) |
Detect bursts as consecutive short-ISI spikes |
first_spike_latency(train, dt) |
Time to first spike (seconds) |
response_onset(train, baseline_steps, dt, threshold_sigma) |
Detect response onset exceeding baseline + threshold |
change_point_detection(train, bin_size, threshold) |
CUSUM-based firing rate change points (Page 1954) |
Stimulus (spike_stats.stimulus)¶
Spike-triggered analysis and receptive field estimation.
| Function | Description |
|---|---|
spike_triggered_average(stimulus, train, window_steps) |
STA: average stimulus preceding each spike |
spike_triggered_covariance(stimulus, train, window_steps) |
STC: covariance of pre-spike stimulus (Schwartz et al. 2006) |
spatial_information(train, positions, n_bins, dt) |
Spatial information in bits/spike (Skaggs et al. 1993) |
place_field_detection(train, positions, ...) |
Detect place fields (O'Keefe & Dostrovsky 1971) |
tuning_curve(train, stimulus_values, n_bins, dt) |
Firing rate vs stimulus value (Dayan & Abbott 2001) |
LFP Coupling (spike_stats.lfp)¶
Spike-LFP phase locking, coherence, and phase histograms.
| Function | Description |
|---|---|
phase_locking_value(train, lfp) |
PLV via Hilbert phase at spike times |
spike_field_coherence(train, lfp, dt) |
Spike-field coherence SFC in frequency domain |
spike_phase_histogram(train, lfp, n_bins) |
Histogram of LFP phase at spike times |
Surrogates (spike_stats.surrogates)¶
Surrogate spike train generation for significance testing.
| Function | Description |
|---|---|
surrogate_isi_shuffle(train, seed) |
ISI-shuffled surrogate preserving rate + ISI distribution |
surrogate_dither(train, dither_ms, dt, seed) |
Spike jittering surrogate (+/- dither window) |
surrogate_trial_shuffle(trains, seed) |
Trial-order shuffling destroying trial-to-trial correlation |
homogeneous_poisson(rate_hz, duration_s, dt, seed) |
Generate homogeneous Poisson spike train |
inhomogeneous_poisson(rate_func, duration_s, dt, seed) |
Time-varying Poisson via thinning (Lewis & Shedler 1979) |
gamma_process(rate_hz, shape, duration_s, dt, seed) |
Gamma-renewal process (shape=1 Poisson, >1 regular) |
compound_poisson_process(rate_hz, burst_mean, ...) |
Compound Poisson with burst events (Snyder & Miller 1991) |
surrogate_joint_isi(train, seed) |
Joint-ISI surrogate preserving serial correlations (Louis et al. 2010) |
surrogate_bin_shuffling(train, bin_size, seed) |
Within-bin spike shuffling (Hatsopoulos et al. 2003) |
surrogate_spike_train_shifting(train, max_shift, seed) |
Circular shifting surrogate |
Information Theory (spike_stats.information)¶
Information-theoretic measures for spike trains.
| Function | Description |
|---|---|
mutual_information(a, b, bin_size) |
Mutual information between binned spike trains (bits) |
transfer_entropy(source, target, bin_size, lag) |
Transfer entropy from source to target (bits) |
spike_train_entropy(train, bin_size, word_length) |
Binary word entropy (Strong et al. 1998) |
noise_entropy(train, n_trials, bin_size, word_length) |
Noise entropy via pseudo-trials (de Ruyter van Steveninck et al. 1997) |
stimulus_specific_information(counts, stim_ids) |
SSI in bits (Butts 2003) |
kozachenko_leonenko_mi(x, y, k) |
k-NN mutual information estimator in nats (Kraskov et al. 2004) |
time_rescaling_ks_test(times, rate_func, ...) |
KS goodness-of-fit for point processes (Brown et al. 2002) |
Causality (spike_stats.causality)¶
Granger causality and directed connectivity.
| Function | Description |
|---|---|
pairwise_granger_causality(source, target, ...) |
Pairwise Granger causality log-likelihood ratio (Granger 1969) |
conditional_granger_causality(source, target, cond, ...) |
Conditional GC controlling for a third signal (Geweke 1984) |
spectral_granger_causality(trains, bin_size, order, ...) |
Frequency-domain GC via VAR model (Geweke 1982) |
partial_directed_coherence(trains, ...) |
PDC: normalised directed connectivity (Baccala & Sameshima 2001) |
directed_transfer_function(trains, ...) |
DTF: normalised transfer function (Kaminski & Blinowska 1991) |
Dimensionality Reduction (spike_stats.dimensionality)¶
Low-dimensional projections of population activity.
| Function | Description |
|---|---|
spike_train_pca(trains, n_components, bin_size, backend) |
PCA on binned spike count matrix |
demixed_pca(trains_by_condition, n_components, ..., backend) |
Demixed PCA separating condition variance (Kobak et al. 2016) |
factor_analysis(trains, n_factors, bin_size, n_iter, backend) |
Factor analysis via EM (Rubin & Thayer 1982) |
Deterministic eigendecomposition. The covariance eigendecomposition returns
eigenvalues in descending order with sign-canonicalised eigenvectors (each
component's largest-magnitude entry is positive), so the projections are
reproducible. Factor analysis starts from a deterministic PCA initialisation
(replacing a random one — seed-independent, like the GPFA init) and solves its
symmetric positive-definite M and E[zzᵀ] systems by Cholesky rather than an
explicit inverse (the Rust backend previously used a hand-rolled Jacobi sweep and
a Gauss-Jordan inverse).
Polyglot chain. Each estimator accepts backend= over five parity-verified
backends (NumPy / Rust / Julia / Go / Mojo), agreeing to ~1e-13: LAPACK in NumPy
and Julia, the nalgebra symmetric solver in Rust, and an accurate cyclic-Jacobi
solver where no LAPACK is linked (Go / Mojo). Unlike the smaller structured
kernels, dense symmetric eigendecomposition is LAPACK's strength, so on the
reference workload (benchmarks/bench_dimensionality.py, i5-11600K, shielded
cores) the NumPy/LAPACK reference is the fastest path (Rust ~0.5×, Mojo ~0.12×,
Go ~0.07×, Julia interop-bound). backend="auto" therefore resolves to the
NumPy reference; the compiled backends are kept for cross-language parity and for
deployments that run without an optimised BLAS.
Decoding (spike_stats.decoding)¶
Neural population decoding algorithms.
| Function | Description |
|---|---|
population_vector_decode(trains, directions, window) |
Georgopoulos population vector decoding |
bayesian_decode(counts, tuning_rates, prior) |
Bayesian MAP decoder (Dayan & Abbott 2001) |
maximum_likelihood_decode(counts, tuning_rates) |
Maximum likelihood Poisson decoder |
linear_discriminant_decode(data, labels, test) |
Fisher LDA decoder (Fisher 1936) |
naive_bayes_decode(data, labels, test) |
Gaussian naive Bayes decoder |
Network (spike_stats.network)¶
Network-level analysis: connectivity, assemblies, synfire chains.
| Function | Description |
|---|---|
functional_connectivity(trains, max_lag_ms, dt) |
Infer connectivity from peak cross-correlation |
unitary_events(trains, bin_size, alpha) |
Detect significant synchronous bins (Gruen et al. 2002) |
cell_assembly_detection(trains, bin_size, threshold) |
PCA-based cell assembly detection (Lopes-dos-Santos et al. 2013) |
synfire_chain_detection(trains, dt, max_delay_ms, ...) |
Cross-correlation peak ordering (Abeles 1991) |
Point Process (spike_stats.point_process)¶
Point process models and hazard functions.
| Function | Description |
|---|---|
conditional_intensity(train, dt, window_ms) |
Moving-window MLE conditional intensity (Brown et al. 2004) |
isi_hazard_function(train, dt, bins) |
ISI hazard function h(t) = f(t)/S(t) (Tuckwell 1988) |
isi_survivor_function(train, dt, bins) |
ISI survivor function S(t) = P(ISI > t) |
renewal_density(train, dt, bins) |
Renewal density normalised by mean rate (Cox 1962) |
Sorting Quality (spike_stats.sorting_quality)¶
Spike sorting quality metrics.
| Function | Description |
|---|---|
isolation_distance(cluster, noise, backend) |
Mahalanobis isolation distance (Harris et al. 2001) |
l_ratio(cluster, noise, backend) |
L-ratio cluster quality (Schmitzer-Torbert et al. 2005) |
silhouette_score(features, labels) |
Mean silhouette score (Rousseeuw 1987) |
d_prime(cluster_a, cluster_b) |
Sensitivity index between two clusters (Green & Swets 1966) |
isi_violation_rate(train, dt, refractory_ms) |
Fraction of ISIs below refractory period (Hill et al. 2011) |
presence_ratio(train, n_bins) |
Fraction of time bins with at least one spike (IBL 2019) |
amplitude_cutoff(amplitudes, bins) |
Estimated missing spike fraction (Hill et al. 2011) |
snr(waveforms) |
Signal-to-noise ratio of spike waveforms (Suner et al. 2005) |
nn_hit_rate(cluster, noise, k) |
k-NN cluster purity (Chung et al. 2017) |
drift_metric(waveforms, timestamps, n_bins) |
Waveform amplitude drift over time (IBL 2019) |
Mahalanobis kernel. isolation_distance and l_ratio share one numerically
optimal kernel for the squared Mahalanobis distance
(x-μ)ᵀ Σ⁻¹ (x-μ). The regularised cluster covariance Σ = L Lᵀ is Cholesky-
factorised once and the quadratic form is obtained by a triangular solve
L z = x-μ followed by Σ z² — the covariance is never inverted explicitly,
which is both more accurate for ill-conditioned cluster covariances and cheaper
than forming Σ⁻¹ and multiplying.
Polyglot chain. isolation_distance(..., backend=...) and
l_ratio(..., backend=...) run the same Cholesky kernel across five backends
(NumPy / Rust / Julia / Go / Mojo), agreeing with the NumPy reference to
floating-point round-off (~1e-13). backend="auto" prefers the Rust engine (the
always-available compiled path) and otherwise falls back to the NumPy reference;
"python", "rust", "julia", "go" and "mojo" force a specific path. On
the reference workload (benchmarks/bench_sorting_quality.py, i5-11600K,
shielded cores 10–11; 64-point cluster, 256 noise points, 12 features) the
compiled backends beat NumPy — Mojo ~4.6×, Go ~3.8×, Rust ~1.9× — while the
Julia path is dominated by per-call interop overhead on this small workload
(~0.5×). Degenerate inputs (n_cluster < 2, fewer noise points than the cluster
size for the isolation distance, or empty noise for the L-ratio) return nan
before any backend runs.
Waveform (spike_stats.waveform)¶
Spike waveform shape analysis.
| Function | Description |
|---|---|
waveform_width(waveform, dt) |
Trough-to-peak width in seconds (Bartho et al. 2004) |
waveform_amplitude(waveform) |
Peak-to-trough amplitude |
waveform_repolarization_slope(waveform, dt) |
Max dV/dt after trough (Bean 2007) |
waveform_recovery_slope(waveform, dt) |
dV/dt during return to baseline (Bean 2007) |
waveform_halfwidth(waveform, dt) |
Duration at half-minimum amplitude (Bartho et al. 2004) |
waveform_pt_ratio(waveform) |
Peak-to-trough ratio (Bartho et al. 2004) |
Statistics (spike_stats.statistics)¶
Significance testing.
| Function | Description |
|---|---|
significance_bootstrap(func, a, b, n_surrogates, seed) |
Bootstrap permutation test for pairwise statistics |
Patterns (spike_stats.patterns)¶
Spike directionality, ordering, and higher-order patterns.
| Function | Description |
|---|---|
spike_directionality(times_a, times_b, ...) |
Asymmetry in [-1,1]: positive means A leads B (Kreuz et al. 2015) |
spike_train_order(times_list, ...) |
Pairwise directionality matrix (Kreuz et al. 2017) |
cubic_higher_order(train, dt, max_lag) |
Third-order cumulant C3(tau1, tau2) (Nikias & Petropulu 1993) |
SPADE (spike_stats.spade)¶
Spike Pattern Detection and Evaluation (Torre et al. 2013).
| Function | Description |
|---|---|
spade_detect(trains, bin_ms, dt, min_support, ...) |
Detect significant spatiotemporal spike patterns via frequent itemset mining + surrogate testing |
GPFA (spike_stats.gpfa)¶
Gaussian Process Factor Analysis (Yu et al. 2009). Deterministic PCA initialisation and a five-language EM chain (NumPy / Rust / Julia / Go / Mojo) that agree to floating-point round-off. See GPFA -- deterministic init and polyglot EM for the algorithm, the backend contract and the benchmark.
| Function | Description |
|---|---|
gpfa(trains, n_latents, bin_ms, dt, max_iter, ..., backend) |
Extract smooth latent trajectories via EM with SE-GP priors |
gpfa_transform(new_trains, params, bin_ms, dt) |
Project new data using learned GPFA parameters |
gpfa_pca_init(Y, n_latents, bin_ms) |
Deterministic PCA initialisation shared by every backend |
Explainability¶
sc_neurocore.analysis.explainability
¶
Explain spike-vector activity through a configured neuron-to-concept map.
SpikeToConceptMapper
¶
Map spike-vector activity to human-readable semantic concepts.
Source code in src/sc_neurocore/analysis/explainability.py
| Python | |
|---|---|
16 17 18 19 20 21 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 | |
explain(spikes)
¶
Describe the concepts implied by an active spike vector.
Parameters¶
spikes : numpy.ndarray of shape (n_neurons,) Spike activity; indices with a value greater than zero are treated as active.
Returns¶
str A human-readable sentence naming the active concepts.
Source code in src/sc_neurocore/analysis/explainability.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 | |
Integrated Information (Phi*)¶
phi_star(...) estimates geometric integrated information following
Barrett and Seth (2011). It compares Gaussian mutual information between
past and future whole-system states against contiguous bipartitions and
returns a non-negative reducibility estimate in nats. This is a tractable
analysis metric, not a claim about consciousness or intrinsic causal power.
Each Gaussian mutual information is a difference of covariance
log-determinants taken from Cholesky factors
(MI = 0.5 (log|Cov_X| + log|Cov_Y| - log|Cov_XY|)). Summing log-determinants is
the numerically stable form of the determinant ratio: the naive product of raw
determinants underflows for larger channel counts, where the log-determinant form
stays exact.
phi_from_spike_trains(...) bins binary spike trains into spike counts and
then applies phi_star(...) at the requested lag. The estimator is suitable
for small analysis windows and coarse-grained spike-train diagnostics; it is
not a full IIT minimum-information-partition search.
Polyglot chain. phi_star(..., backend=...) runs the same estimator across
five backends (NumPy / Rust / Julia / Go / Mojo), agreeing to floating-point
round-off (Rust, Julia, Go ~1e-15; Mojo ~1e-10). backend="auto" prefers the
Rust engine when present, otherwise the NumPy reference. On the reference
workload (benchmarks/bench_phi.py, i5-11600K, shielded cores) every compiled
backend beats NumPy: Rust ~7x, Julia ~4.4x, Mojo ~3.5x, Go ~3.0x.
The public contract is tested as follows:
- independent continuous channels produce near-zero Phi* within finite-sample tolerance;
- correlated channels produce positive Phi*;
- two-channel results are invariant to channel ordering;
- single-channel and too-short inputs return
0.0; - Phi* is always non-negative;
- correlated spike trains pass through the binning adapter;
- independent random spike trains remain low-Phi under the maintained bound;
- the Rust, Julia, Go and Mojo backends match the NumPy reference (gated parity tests).
sc_neurocore.analysis.phi_estimation.phi_star(data, tau=1, backend='auto')
¶
Geometric integrated information Phi* (Barrett & Seth 2011).
Phi* = MI(past; future) - min_partition Σ_k MI(past_k; future_k) under the
Gaussian assumption, with each mutual information a difference of covariance
log-determinants (Cholesky form).
Parameters¶
data : numpy.ndarray
Shape (n_channels, n_timesteps) — spike counts or continuous signals.
tau : int, optional
Time lag for the past→future mapping.
backend : str, optional
"auto" selects the fastest available backend (Rust engine when present,
otherwise the NumPy reference); "python", "rust", "julia",
"go" and "mojo" force a specific path.
Returns¶
float
Phi* estimate in nats. Non-negative; 0 means fully reducible.
Source code in src/sc_neurocore/analysis/phi_estimation.py
| Python | |
|---|---|
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 | |
sc_neurocore.analysis.phi_estimation.phi_from_spike_trains(spikes, bin_size=10, tau=1, backend='auto')
¶
Compute Phi* from binary spike trains by binning into spike counts.
Parameters¶
spikes : numpy.ndarray
Shape (n_neurons, n_timesteps), binary {0, 1}.
bin_size : int, optional
Number of timesteps per bin.
tau : int, optional
Time lag in bins.
backend : str, optional
Forwarded to :func:phi_star.
Returns¶
float Phi* in nats.
Source code in src/sc_neurocore/analysis/phi_estimation.py
| Python | |
|---|---|
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 | |