Skip to main content

sc_neurocore_engine/analysis/
point_process.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Commercial license available
3// © Concepts 1996–2026 Miroslav Šotek. All rights reserved.
4// © Code 2020–2026 Miroslav Šotek. All rights reserved.
5// ORCID: 0009-0009-3560-0851
6// Contact: www.anulum.li | protoscience@anulum.li
7// SC-NeuroCore — Point process models and hazard functions
8
9use super::basic;
10
11/// Conditional intensity function estimate (Hz). Brown et al. 2004.
12///
13/// Moving-window MLE of the Poisson rate at each time step.
14pub fn conditional_intensity(binary_train: &[i32], dt: f64, window_ms: f64) -> Vec<f64> {
15    let n = binary_train.len();
16    if n == 0 {
17        return vec![];
18    }
19    let w = (window_ms / (dt * 1000.0)).round().max(1.0) as usize;
20    let kernel_sum_inv = 1.0 / (w as f64 * dt);
21
22    // "same" convolution: centre the kernel
23    let half = w / 2;
24    let mut result = vec![0.0f64; n];
25    for i in 0..n {
26        let lo = i.saturating_sub(half);
27        let hi = (i + w - half).min(n);
28        let mut sum = 0.0;
29        for j in lo..hi {
30            sum += binary_train[j] as f64;
31        }
32        result[i] = sum * kernel_sum_inv;
33    }
34    result
35}
36
37/// ISI hazard function h(t) = f(t) / S(t). Tuckwell 1988.
38///
39/// Returns `(hazard, bin_centres)`.
40pub fn isi_hazard_function(binary_train: &[i32], dt: f64, bins: usize) -> (Vec<f64>, Vec<f64>) {
41    let intervals = basic::isi(binary_train, dt);
42    if intervals.len() < 5 {
43        return (vec![], vec![]);
44    }
45    let (hist, edges) = histogram(&intervals, bins);
46    let bin_width = edges[1] - edges[0];
47    let n = intervals.len() as f64;
48    let centres: Vec<f64> = (0..bins).map(|k| (edges[k] + edges[k + 1]) / 2.0).collect();
49    let pdf: Vec<f64> = hist.iter().map(|&c| c as f64 / (n * bin_width)).collect();
50    let mut cum = 0.0;
51    let mut survivor = vec![0.0; bins];
52    for k in 0..bins {
53        cum += pdf[k] * bin_width;
54        survivor[k] = (1.0 - cum).max(1e-30);
55    }
56    let hazard: Vec<f64> = (0..bins).map(|k| pdf[k] / survivor[k]).collect();
57    (hazard, centres)
58}
59
60/// ISI survivor function S(t) = P(ISI > t). Tuckwell 1988.
61///
62/// Returns `(survivor, bin_centres)`.
63pub fn isi_survivor_function(binary_train: &[i32], dt: f64, bins: usize) -> (Vec<f64>, Vec<f64>) {
64    let intervals = basic::isi(binary_train, dt);
65    if intervals.len() < 2 {
66        return (vec![], vec![]);
67    }
68    let mut sorted = intervals.clone();
69    sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
70    let n = sorted.len() as f64;
71    let max_isi = sorted[sorted.len() - 1];
72    let edges: Vec<f64> = (0..=bins)
73        .map(|k| k as f64 * max_isi / bins as f64)
74        .collect();
75    let centres: Vec<f64> = (0..bins).map(|k| (edges[k] + edges[k + 1]) / 2.0).collect();
76    let survivor: Vec<f64> = centres
77        .iter()
78        .map(|&t| sorted.iter().filter(|&&v| v > t).count() as f64 / n)
79        .collect();
80    (survivor, centres)
81}
82
83/// Renewal density from ISI distribution. Cox 1962.
84///
85/// Returns `(density, bin_centres)`. Density normalised by mean rate.
86pub fn renewal_density(binary_train: &[i32], dt: f64, bins: usize) -> (Vec<f64>, Vec<f64>) {
87    let intervals = basic::isi(binary_train, dt);
88    if intervals.len() < 5 {
89        return (vec![], vec![]);
90    }
91    let (hist, edges) = histogram(&intervals, bins);
92    let bin_width = edges[1] - edges[0];
93    let n = intervals.len() as f64;
94    let centres: Vec<f64> = (0..bins).map(|k| (edges[k] + edges[k + 1]) / 2.0).collect();
95    let pdf: Vec<f64> = hist.iter().map(|&c| c as f64 / (n * bin_width)).collect();
96    let mean_isi: f64 = intervals.iter().sum::<f64>() / n;
97    let mean_rate = if mean_isi > 0.0 { 1.0 / mean_isi } else { 1.0 };
98    let density: Vec<f64> = pdf.iter().map(|&p| p / mean_rate).collect();
99    (density, centres)
100}
101
102/// Simple histogram: returns (counts, edges) with `bins` equal-width buckets.
103fn histogram(data: &[f64], bins: usize) -> (Vec<usize>, Vec<f64>) {
104    let min = data.iter().cloned().fold(f64::INFINITY, f64::min);
105    let max = data.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
106    let range = if (max - min).abs() < 1e-30 {
107        1.0
108    } else {
109        max - min
110    };
111    let edges: Vec<f64> = (0..=bins)
112        .map(|k| min + k as f64 * range / bins as f64)
113        .collect();
114    let mut counts = vec![0usize; bins];
115    for &v in data {
116        let mut k = ((v - min) / range * bins as f64) as usize;
117        if k >= bins {
118            k = bins - 1;
119        }
120        counts[k] += 1;
121    }
122    (counts, edges)
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    fn make_train() -> Vec<i32> {
130        // Regular spiking every 10 steps
131        let mut t = vec![0i32; 200];
132        for i in (0..200).step_by(10) {
133            t[i] = 1;
134        }
135        t
136    }
137
138    #[test]
139    fn test_conditional_intensity_shape() {
140        let train = make_train();
141        let ci = conditional_intensity(&train, 0.001, 50.0);
142        assert_eq!(ci.len(), 200);
143        // Should be roughly 100 Hz (1 spike per 10ms = 100 Hz)
144        let mid = ci[100];
145        assert!(
146            (mid - 100.0).abs() < 50.0,
147            "CI midpoint {mid} not near 100 Hz"
148        );
149    }
150
151    #[test]
152    fn test_conditional_intensity_empty() {
153        assert!(conditional_intensity(&[], 0.001, 10.0).is_empty());
154    }
155
156    #[test]
157    fn test_conditional_intensity_single() {
158        let ci = conditional_intensity(&[1], 0.001, 10.0);
159        assert_eq!(ci.len(), 1);
160    }
161
162    #[test]
163    fn test_isi_hazard_basic() {
164        let train = make_train();
165        let (hazard, centres) = isi_hazard_function(&train, 0.001, 20);
166        assert!(!hazard.is_empty());
167        assert_eq!(hazard.len(), centres.len());
168        // All hazard values should be non-negative
169        assert!(hazard.iter().all(|&h| h >= 0.0));
170    }
171
172    #[test]
173    fn test_isi_hazard_few_spikes() {
174        let train = vec![0, 1, 0, 0, 0, 1, 0]; // 1 ISI
175        let (h, c) = isi_hazard_function(&train, 0.001, 10);
176        assert!(h.is_empty());
177        assert!(c.is_empty());
178    }
179
180    #[test]
181    fn test_isi_survivor_basic() {
182        let train = make_train();
183        let (surv, centres) = isi_survivor_function(&train, 0.001, 20);
184        assert!(!surv.is_empty());
185        assert_eq!(surv.len(), centres.len());
186        // Survivor should start near 1 and decrease
187        assert!(surv[0] >= surv[surv.len() - 1]);
188    }
189
190    #[test]
191    fn test_isi_survivor_few_spikes() {
192        let train = vec![1, 0];
193        let (s, c) = isi_survivor_function(&train, 0.001, 10);
194        assert!(s.is_empty());
195        assert!(c.is_empty());
196    }
197
198    #[test]
199    fn test_renewal_density_basic() {
200        let train = make_train();
201        let (dens, centres) = renewal_density(&train, 0.001, 20);
202        assert!(!dens.is_empty());
203        assert_eq!(dens.len(), centres.len());
204        assert!(dens.iter().all(|&d| d >= 0.0));
205    }
206
207    #[test]
208    fn test_renewal_density_few_spikes() {
209        let (d, c) = renewal_density(&[0, 1, 0], 0.001, 10);
210        assert!(d.is_empty());
211        assert!(c.is_empty());
212    }
213
214    #[test]
215    fn test_histogram_helper() {
216        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
217        let (counts, edges) = histogram(&data, 4);
218        assert_eq!(counts.len(), 4);
219        assert_eq!(edges.len(), 5);
220        let total: usize = counts.iter().sum();
221        assert_eq!(total, 5);
222    }
223
224    #[test]
225    fn test_regular_spike_isi_uniformity() {
226        let train = make_train();
227        // All ISIs should be 0.01s for regular train
228        let intervals = basic::isi(&train, 0.001);
229        assert!(intervals.iter().all(|&i| (i - 0.01).abs() < 1e-12));
230    }
231}