1#[derive(Clone, Debug)]
54pub struct InnerHairCell {
55 pub v: f64, pub v_rest: f64,
58 pub tau: f64, pub g_met: f64, pub x_half: f64, pub s_met: f64, pub ca: f64, pub tau_ca: f64, pub g_ca: f64, pub v_ca_half: f64, pub s_ca: f64, pub q: f64, pub c: f64, pub w: f64, pub m_pool: f64, pub y: f64, pub x_r: f64, pub k_rel: f64, pub l: f64, pub r_up: f64, pub k_d: f64, pub dt: f64,
80}
81
82impl InnerHairCell {
83 pub fn new() -> Self {
84 Self {
85 v: -60.0,
86 v_rest: -60.0,
87 tau: 0.5,
88 g_met: 10.0,
89 x_half: 50.0,
90 s_met: 10.0,
91 ca: 0.05,
92 tau_ca: 1.0,
93 g_ca: 0.5,
94 v_ca_half: -35.0, s_ca: 8.0,
96 q: 8.0,
98 c: 0.0,
99 w: 0.0,
100 m_pool: 10.0, y: 0.01, x_r: 0.005, k_rel: 0.2, l: 0.05, r_up: 0.05, k_d: 0.1, dt: 0.025,
108 }
109 }
110
111 #[inline]
113 fn release_rate(&self) -> f64 {
114 let ca2 = self.ca * self.ca;
115 let kd2 = self.k_d * self.k_d;
116 self.k_rel * ca2 / (ca2 + kd2)
117 }
118
119 pub fn step(&mut self, displacement: f64) -> f64 {
121 let p_open = 1.0 / (1.0 + (-(displacement - self.x_half) / self.s_met).exp());
123 let i_met = self.g_met * p_open * (0.0 - self.v);
124 self.v += (-(self.v - self.v_rest) + i_met) / self.tau * self.dt;
125
126 let m_ca = 1.0 / (1.0 + (-(self.v - self.v_ca_half) / self.s_ca).exp());
128 let ca_entry = self.g_ca * m_ca * m_ca; self.ca += (-self.ca / self.tau_ca + ca_entry) * self.dt;
130 self.ca = self.ca.max(0.0);
131
132 let f_ca = self.release_rate();
134 let dq = self.y * (self.m_pool - self.q) + self.x_r * self.w - f_ca * self.q;
135 let dc = f_ca * self.q - self.l * self.c - self.r_up * self.c;
136 let dw = self.r_up * self.c - self.x_r * self.w;
137
138 self.q += dq * self.dt;
139 self.c += dc * self.dt;
140 self.w += dw * self.dt;
141
142 self.q = self.q.clamp(0.0, self.m_pool);
144 self.c = self.c.max(0.0);
145 self.w = self.w.max(0.0);
146 if !self.v.is_finite() {
147 self.v = self.v_rest;
148 }
149 if !self.ca.is_finite() {
150 self.ca = 0.05;
151 }
152 if !self.q.is_finite() {
153 self.q = 8.0;
154 }
155 if !self.c.is_finite() {
156 self.c = 0.0;
157 }
158 if !self.w.is_finite() {
159 self.w = 0.0;
160 }
161
162 self.v
163 }
164
165 pub fn reset(&mut self) {
166 self.v = self.v_rest;
167 self.ca = 0.05;
168 self.q = 8.0;
169 self.c = 0.0;
170 self.w = 0.0;
171 }
172}
173
174impl Default for InnerHairCell {
175 fn default() -> Self {
176 Self::new()
177 }
178}
179
180#[derive(Clone, Debug)]
207pub struct OuterHairCell {
208 pub v: f64,
209 pub v_rest: f64,
210 pub tau: f64,
211 pub g_met: f64,
212 pub x_half: f64,
213 pub s_met: f64,
214 pub motility: f64, pub l_max: f64, pub v_pk: f64, pub z_e: f64, pub v_t: f64, pub q_max: f64, pub asym_factor: f64, pub dt: f64,
223}
224
225impl OuterHairCell {
226 pub fn new() -> Self {
227 Self {
228 v: -70.0,
229 v_rest: -70.0,
230 tau: 0.3,
231 g_met: 15.0,
232 x_half: 20.0,
233 s_met: 6.0,
234 motility: 0.0,
235 l_max: 4.0, v_pk: -40.0, z_e: 0.7, v_t: 26.0, q_max: 0.8, asym_factor: 0.3, dt: 0.025,
242 }
243 }
244
245 #[inline]
248 fn prestin_compact(&self) -> f64 {
249 1.0 / (1.0 + (self.z_e * (self.v - self.v_pk) / self.v_t).exp())
250 }
251
252 pub fn step(&mut self, displacement: f64) -> f64 {
254 let p_open = 1.0 / (1.0 + (-(displacement - self.x_half) / self.s_met).exp());
255 let i_met = self.g_met * p_open * (0.0 - self.v);
256 self.v += (-(self.v - self.v_rest) + i_met) / self.tau * self.dt;
257
258 let compact = self.prestin_compact();
260 let raw_motility = self.l_max * (0.5 - compact);
263 let asym = if raw_motility > 0.0 {
265 1.0 + self.asym_factor } else {
267 1.0 - self.asym_factor };
269 self.motility = raw_motility * asym;
270
271 if !self.v.is_finite() {
272 self.v = self.v_rest;
273 }
274 self.v
275 }
276
277 pub fn reset(&mut self) {
278 self.v = self.v_rest;
279 self.motility = 0.0;
280 }
281}
282
283impl Default for OuterHairCell {
284 fn default() -> Self {
285 Self::new()
286 }
287}
288
289#[derive(Clone, Debug)]
314pub struct RodPhotoreceptor {
315 pub v: f64,
316 pub v_dark: f64,
317 pub v_hyper: f64,
318 pub cgmp: f64, pub ca: f64, pub tau_act: f64, pub tau_ca: f64, pub sensitivity: f64,
323 pub alpha_max: f64, pub k_gc: f64, pub n_gc: f64, pub eta_ca: f64, pub dt: f64,
328}
329
330impl RodPhotoreceptor {
331 pub fn new() -> Self {
332 Self {
333 v: -40.0,
334 v_dark: -40.0,
335 v_hyper: -70.0,
336 cgmp: 1.0,
337 ca: 1.0, tau_act: 20.0,
339 tau_ca: 30.0, sensitivity: 0.01,
341 alpha_max: 0.05, k_gc: 0.5, n_gc: 4.0, eta_ca: 0.3, dt: 0.1,
346 }
347 }
348
349 #[inline]
352 fn gc_rate(&self) -> f64 {
353 let ca_n = self.ca.powf(self.n_gc);
354 let k_n = self.k_gc.powf(self.n_gc);
355 self.alpha_max * k_n / (k_n + ca_n)
356 }
357
358 pub fn step(&mut self, light: f64) -> f64 {
360 let light_clamped = light.max(0.0);
361
362 let gc = self.gc_rate();
364 let pde = self.sensitivity * light_clamped / self.tau_act;
365 let d_cgmp = gc - pde * self.cgmp + (1.0 - self.cgmp) * 0.001; self.cgmp += d_cgmp * self.dt;
367 self.cgmp = self.cgmp.clamp(0.0, 1.5); let cng_fraction = self.cgmp.powi(3).min(1.0);
371
372 let d_ca = self.eta_ca * cng_fraction - self.ca / self.tau_ca;
374 self.ca += d_ca * self.dt;
375 self.ca = self.ca.max(0.0);
376
377 self.v = self.v_hyper + (self.v_dark - self.v_hyper) * cng_fraction;
379 if !self.v.is_finite() {
380 self.v = self.v_dark;
381 }
382 if !self.cgmp.is_finite() {
383 self.cgmp = 1.0;
384 }
385 if !self.ca.is_finite() {
386 self.ca = 1.0;
387 }
388 self.v
389 }
390
391 pub fn reset(&mut self) {
392 self.v = self.v_dark;
393 self.cgmp = 1.0;
394 self.ca = 1.0;
395 }
396}
397
398impl Default for RodPhotoreceptor {
399 fn default() -> Self {
400 Self::new()
401 }
402}
403
404#[derive(Clone, Debug)]
415pub struct ConePhotoreceptor {
416 pub v: f64,
417 pub v_dark: f64,
418 pub v_hyper: f64,
419 pub cgmp: f64,
420 pub tau_act: f64,
421 pub tau_rec: f64,
422 pub sensitivity: f64,
423 pub dt: f64,
424}
425
426impl ConePhotoreceptor {
427 pub fn new() -> Self {
428 Self {
429 v: -40.0,
430 v_dark: -40.0,
431 v_hyper: -65.0,
432 cgmp: 1.0,
433 tau_act: 5.0, tau_rec: 50.0, sensitivity: 0.001, dt: 0.1,
437 }
438 }
439
440 pub fn step(&mut self, light: f64) -> f64 {
441 let light_clamped = light.max(0.0);
442 let d_cgmp = -self.sensitivity * light_clamped * self.cgmp / self.tau_act
443 + (1.0 - self.cgmp) / self.tau_rec;
444 self.cgmp += d_cgmp * self.dt;
445 self.cgmp = self.cgmp.clamp(0.0, 1.0);
446
447 let cng_fraction = self.cgmp.powi(3);
448 self.v = self.v_hyper + (self.v_dark - self.v_hyper) * cng_fraction;
449 self.v
450 }
451
452 pub fn reset(&mut self) {
453 self.v = self.v_dark;
454 self.cgmp = 1.0;
455 }
456}
457
458impl Default for ConePhotoreceptor {
459 fn default() -> Self {
460 Self::new()
461 }
462}
463
464#[derive(Clone, Debug)]
498pub struct RetinalGanglionCell {
499 pub stim_buffer: Vec<f64>, pub stim_kernel: Vec<f64>, pub stim_idx: usize, pub hist_buffer: Vec<f64>, pub hist_kernel: Vec<f64>, pub hist_idx: usize,
508
509 pub baseline: f64, pub on_centre: bool, pub spike_threshold: f64, pub dt: f64,
513 pub gain: f64,
514}
515
516impl RetinalGanglionCell {
517 pub fn new() -> Self {
520 let n_stim = 20;
523 let mut stim_kernel = vec![0.0; n_stim];
524 for i in 0..n_stim {
525 let t = i as f64;
526 let excit = (-(t - 3.0).powi(2) / 8.0).exp();
528 let inhib = 0.5 * (-(t - 8.0).powi(2) / 32.0).exp();
529 stim_kernel[i] = excit - inhib;
530 }
531 let peak: f64 = stim_kernel.iter().map(|x| x.abs()).fold(0.0_f64, f64::max);
533 if peak > 0.0 {
534 for k in &mut stim_kernel {
535 *k /= peak;
536 }
537 }
538
539 let n_hist = 30;
542 let mut hist_kernel = vec![0.0; n_hist];
543 for i in 0..n_hist {
544 let t = i as f64 * 0.5; let refrac = -15.0 * (-t / 1.5).exp(); let burst = 0.3 * (-((t - 5.0) / 3.0).powi(2)).exp(); hist_kernel[i] = refrac + burst;
549 }
550
551 Self {
552 stim_buffer: vec![0.0; n_stim],
553 stim_kernel,
554 stim_idx: 0,
555 hist_buffer: vec![0.0; n_hist],
556 hist_kernel,
557 hist_idx: 0,
558 baseline: -3.0, on_centre: true,
560 spike_threshold: 0.7, dt: 0.5,
562 gain: 1.0,
563 }
564 }
565
566 pub fn off_centre() -> Self {
567 Self {
568 on_centre: false,
569 ..Self::new()
570 }
571 }
572
573 #[inline]
575 fn convolve(buffer: &[f64], kernel: &[f64], write_idx: usize) -> f64 {
576 let n = kernel.len();
577 let mut sum = 0.0;
578 for i in 0..n {
579 let buf_idx = (write_idx + n - 1 - i) % n;
581 sum += buffer[buf_idx] * kernel[i];
582 }
583 sum
584 }
585
586 pub fn step(&mut self, input: f64) -> i32 {
590 let effective = if self.on_centre { input } else { -input };
591 let stimulus = self.gain * effective;
592
593 let n_stim = self.stim_kernel.len();
595 self.stim_buffer[self.stim_idx % n_stim] = stimulus;
596 self.stim_idx = (self.stim_idx + 1) % n_stim;
597
598 let filtered_stim = Self::convolve(&self.stim_buffer, &self.stim_kernel, self.stim_idx);
600
601 let n_hist = self.hist_kernel.len();
603 let filtered_hist = Self::convolve(&self.hist_buffer, &self.hist_kernel, self.hist_idx);
604
605 let log_rate = filtered_stim + filtered_hist + self.baseline;
607 let lambda = log_rate.exp().min(1000.0); let spiked = if lambda * self.dt > self.spike_threshold {
611 1
612 } else {
613 0
614 };
615
616 self.hist_buffer[self.hist_idx % n_hist] = spiked as f64;
618 self.hist_idx = (self.hist_idx + 1) % n_hist;
619
620 spiked
621 }
622
623 pub fn reset(&mut self) {
624 for x in &mut self.stim_buffer {
625 *x = 0.0;
626 }
627 for x in &mut self.hist_buffer {
628 *x = 0.0;
629 }
630 self.stim_idx = 0;
631 self.hist_idx = 0;
632 }
633}
634
635impl Default for RetinalGanglionCell {
636 fn default() -> Self {
637 Self::new()
638 }
639}
640
641#[derive(Clone, Debug)]
653pub struct MerkelCell {
654 pub v: f64,
655 pub v_rest: f64,
656 pub v_reset: f64,
657 pub v_threshold: f64,
658 pub tau: f64,
659 pub adapt: f64, pub tau_adapt: f64, pub a_adapt: f64, pub gain: f64,
663 pub dt: f64,
664}
665
666impl MerkelCell {
667 pub fn new() -> Self {
668 Self {
669 v: -65.0,
670 v_rest: -65.0,
671 v_reset: -70.0,
672 v_threshold: -50.0,
673 tau: 5.0,
674 adapt: 0.0,
675 tau_adapt: 200.0, a_adapt: 0.3,
677 gain: 1.5,
678 dt: 0.5,
679 }
680 }
681
682 #[inline]
683 fn exact_relax(value: f64, target: f64, tau: f64, dt: f64) -> f64 {
684 target + (value - target) * (-dt / tau).exp()
685 }
686
687 fn is_valid(&self) -> bool {
688 [
689 self.v,
690 self.v_rest,
691 self.v_reset,
692 self.v_threshold,
693 self.tau,
694 self.adapt,
695 self.tau_adapt,
696 self.a_adapt,
697 self.gain,
698 self.dt,
699 ]
700 .iter()
701 .all(|value| value.is_finite())
702 && (-100.0..=60.0).contains(&self.v)
703 && self.tau > 0.0
704 && self.tau_adapt > 0.0
705 && self.a_adapt >= 0.0
706 && self.gain >= 0.0
707 && self.dt > 0.0
708 && self.adapt >= 0.0
709 && self.v_threshold > self.v_reset
710 && self.v_threshold > self.v_rest
711 }
712
713 pub fn step(&mut self, pressure: f64) -> i32 {
715 if !self.is_valid() || !pressure.is_finite() {
716 return 0;
717 }
718
719 let rectified_pressure = pressure.max(0.0);
720 let v_inf = self.v_rest + self.gain * rectified_pressure - self.adapt;
721 let v_next = Self::exact_relax(self.v, v_inf, self.tau, self.dt);
722 let adapt_inf = (self.a_adapt * (v_next - self.v_rest).max(0.0)).max(0.0);
723 let adapt_next = Self::exact_relax(self.adapt, adapt_inf, self.tau_adapt, self.dt).max(0.0);
724 if !v_next.is_finite() || !adapt_next.is_finite() {
725 return 0;
726 }
727
728 if v_next >= self.v_threshold {
729 self.v = self.v_reset;
730 self.adapt = adapt_next;
731 1
732 } else {
733 self.v = v_next.clamp(-100.0, 60.0);
734 self.adapt = adapt_next;
735 0
736 }
737 }
738
739 pub fn reset(&mut self) {
740 self.v = self.v_rest;
741 self.adapt = 0.0;
742 }
743}
744
745impl Default for MerkelCell {
746 fn default() -> Self {
747 Self::new()
748 }
749}
750
751#[derive(Clone, Debug)]
763pub struct PacinianCorpuscle {
764 pub v: f64,
765 pub v_rest: f64,
766 pub v_reset: f64,
767 pub v_threshold: f64,
768 pub tau: f64,
769 pub prev_pressure: f64,
770 pub adapt: f64,
771 pub tau_adapt: f64,
772 pub gain: f64,
773 pub dt: f64,
774}
775
776impl PacinianCorpuscle {
777 pub fn new() -> Self {
778 Self {
779 v: -65.0,
780 v_rest: -65.0,
781 v_reset: -70.0,
782 v_threshold: -50.0,
783 tau: 2.0,
784 prev_pressure: 0.0,
785 adapt: 0.0,
786 tau_adapt: 5.0, gain: 10.0, dt: 0.5,
789 }
790 }
791
792 #[inline]
793 fn exact_relax(value: f64, target: f64, tau: f64, dt: f64) -> f64 {
794 target + (value - target) * (-dt / tau).exp()
795 }
796
797 fn is_valid(&self) -> bool {
798 [
799 self.v,
800 self.v_rest,
801 self.v_reset,
802 self.v_threshold,
803 self.tau,
804 self.prev_pressure,
805 self.adapt,
806 self.tau_adapt,
807 self.gain,
808 self.dt,
809 ]
810 .iter()
811 .all(|value| value.is_finite())
812 && (-100.0..=60.0).contains(&self.v)
813 && self.tau > 0.0
814 && self.tau_adapt > 0.0
815 && self.gain >= 0.0
816 && self.dt > 0.0
817 && self.adapt >= 0.0
818 && self.v_threshold > self.v_reset
819 && self.v_threshold > self.v_rest
820 }
821
822 pub fn step(&mut self, pressure: f64) -> i32 {
824 if !self.is_valid() || !pressure.is_finite() {
825 return 0;
826 }
827
828 let dp = (pressure - self.prev_pressure) / self.dt;
830 let drive = self.gain * dp.abs() - self.adapt;
831 let v_inf = self.v_rest + drive;
832 let v_next = Self::exact_relax(self.v, v_inf, self.tau, self.dt);
833 let adapt_inf = 0.5 * drive.max(0.0);
834 let adapt_next = Self::exact_relax(self.adapt, adapt_inf, self.tau_adapt, self.dt).max(0.0);
835 if !dp.is_finite() || !drive.is_finite() || !v_next.is_finite() || !adapt_next.is_finite() {
836 return 0;
837 }
838
839 self.prev_pressure = pressure;
840 self.adapt = adapt_next;
841 if v_next >= self.v_threshold {
842 self.v = self.v_reset;
843 1
844 } else {
845 self.v = v_next.clamp(-100.0, 60.0);
846 0
847 }
848 }
849
850 pub fn reset(&mut self) {
851 self.v = self.v_rest;
852 self.prev_pressure = 0.0;
853 self.adapt = 0.0;
854 }
855}
856
857impl Default for PacinianCorpuscle {
858 fn default() -> Self {
859 Self::new()
860 }
861}
862
863#[derive(Clone, Debug)]
875pub struct Nociceptor {
876 pub v: f64,
877 pub v_rest: f64,
878 pub v_reset: f64,
879 pub v_threshold: f64,
880 pub tau: f64,
881 pub sensitisation: f64, pub tau_sens: f64, pub sens_rate: f64, pub gain: f64,
885 pub dt: f64,
886}
887
888impl Nociceptor {
889 pub fn new() -> Self {
890 Self {
891 v: -65.0,
892 v_rest: -65.0,
893 v_reset: -68.0,
894 v_threshold: -30.0, tau: 8.0,
896 sensitisation: 0.0,
897 tau_sens: 5000.0, sens_rate: 0.5,
899 gain: 1.0,
900 dt: 0.5,
901 }
902 }
903
904 fn exact_relax(value: f64, target: f64, tau: f64, dt: f64) -> f64 {
905 target + (value - target) * (-dt / tau).exp()
906 }
907
908 fn biological_voltage(value: f64) -> bool {
909 value.is_finite() && (-100.0..=60.0).contains(&value)
910 }
911
912 fn is_valid(&self) -> bool {
913 Self::biological_voltage(self.v)
914 && Self::biological_voltage(self.v_rest)
915 && Self::biological_voltage(self.v_reset)
916 && Self::biological_voltage(self.v_threshold)
917 && self.tau.is_finite()
918 && self.tau > 0.0
919 && self.sensitisation.is_finite()
920 && (0.0..=10.0).contains(&self.sensitisation)
921 && self.tau_sens.is_finite()
922 && self.tau_sens > 0.0
923 && self.sens_rate.is_finite()
924 && self.sens_rate >= 0.0
925 && self.gain.is_finite()
926 && self.gain >= 0.0
927 && self.dt.is_finite()
928 && self.dt > 0.0
929 && self.v_threshold > self.v_reset
930 && self.v_threshold > self.v_rest
931 }
932
933 pub fn step(&mut self, stimulus: f64) -> i32 {
935 if !self.is_valid() || !stimulus.is_finite() {
936 return 0;
937 }
938
939 let drive = self.gain * stimulus.max(0.0);
940 let v_next = Self::exact_relax(self.v, self.v_rest + drive, self.tau, self.dt);
941 if !drive.is_finite() || !v_next.is_finite() {
942 return 0;
943 }
944
945 let effective_threshold = self.v_threshold - self.sensitisation;
946 if v_next >= effective_threshold {
947 self.v = self.v_reset;
948 self.sensitisation = (self.sensitisation + self.sens_rate).min(10.0);
950 1
951 } else {
952 let sensitisation_next =
954 Self::exact_relax(self.sensitisation, 0.0, self.tau_sens, self.dt).max(0.0);
955 if !sensitisation_next.is_finite() {
956 return 0;
957 }
958 self.v = v_next.clamp(-100.0, 60.0);
959 self.sensitisation = sensitisation_next;
960 0
961 }
962 }
963
964 pub fn reset(&mut self) {
965 self.v = self.v_rest;
966 self.sensitisation = 0.0;
967 }
968}
969
970impl Default for Nociceptor {
971 fn default() -> Self {
972 Self::new()
973 }
974}
975
976#[derive(Clone, Debug)]
991pub struct OlfactoryReceptorNeuron {
992 pub v: f64,
993 pub v_rest: f64,
994 pub v_reset: f64,
995 pub v_threshold: f64,
996 pub tau: f64,
997 pub camp: f64, pub adapt: f64, pub pde4: f64, pub tau_camp: f64, pub tau_adapt: f64, pub tau_pde4: f64, pub k_pde4: f64, pub gain: f64,
1005 pub dt: f64,
1006}
1007
1008impl OlfactoryReceptorNeuron {
1009 pub fn new() -> Self {
1010 Self {
1011 v: -65.0,
1012 v_rest: -65.0,
1013 v_reset: -70.0,
1014 v_threshold: -45.0,
1015 tau: 5.0,
1016 camp: 0.0,
1017 adapt: 0.0,
1018 pde4: 0.0,
1019 tau_camp: 50.0,
1020 tau_adapt: 500.0,
1021 tau_pde4: 300.0, k_pde4: 1.5, gain: 1.5,
1024 dt: 0.5,
1025 }
1026 }
1027
1028 pub fn step(&mut self, concentration: f64) -> i32 {
1030 let conc = concentration.max(0.0);
1031
1032 let camp_production = conc / (conc + 1.0) * (1.0 - 0.8 * self.adapt);
1034 let pde4_degradation = self.k_pde4 * self.pde4 * self.camp;
1036 let camp_target = (camp_production - pde4_degradation).max(0.0);
1037 self.camp += (camp_target - self.camp) / self.tau_camp * self.dt;
1038 self.camp = self.camp.clamp(0.0, 1.0);
1039
1040 self.pde4 += (self.camp - self.pde4) / self.tau_pde4 * self.dt;
1042 self.pde4 = self.pde4.clamp(0.0, 1.0);
1043
1044 let drive = self.gain * self.camp * 50.0; self.v += (-(self.v - self.v_rest) + drive) / self.tau * self.dt;
1046
1047 let ca_proxy = if self.v > self.v_rest {
1049 (self.v - self.v_rest) / 20.0
1050 } else {
1051 0.0
1052 };
1053 self.adapt += (ca_proxy - self.adapt) / self.tau_adapt * self.dt;
1054 self.adapt = self.adapt.clamp(0.0, 1.0);
1055
1056 if self.v >= self.v_threshold {
1057 self.v = self.v_reset;
1058 1
1059 } else {
1060 0
1061 }
1062 }
1063
1064 pub fn reset(&mut self) {
1065 self.v = self.v_rest;
1066 self.camp = 0.0;
1067 self.adapt = 0.0;
1068 self.pde4 = 0.0;
1069 }
1070}
1071
1072impl Default for OlfactoryReceptorNeuron {
1073 fn default() -> Self {
1074 Self::new()
1075 }
1076}
1077
1078#[derive(Clone, Debug)]
1090pub struct TasteReceptorCell {
1091 pub v: f64,
1092 pub v_rest: f64,
1093 pub tau: f64,
1094 pub ca: f64, pub ip3: f64, pub tau_ip3: f64,
1097 pub tau_ca: f64,
1098 pub gain: f64,
1099 pub atp_release: f64, pub dt: f64,
1101}
1102
1103impl TasteReceptorCell {
1104 pub fn new() -> Self {
1105 Self {
1106 v: -50.0,
1107 v_rest: -50.0,
1108 tau: 10.0,
1109 ca: 0.0,
1110 ip3: 0.0,
1111 tau_ip3: 100.0,
1112 tau_ca: 200.0,
1113 gain: 1.0,
1114 atp_release: 0.0,
1115 dt: 0.5,
1116 }
1117 }
1118
1119 pub fn step(&mut self, tastant: f64) -> f64 {
1121 let conc = tastant.max(0.0);
1122 let ip3_target = conc / (conc + 0.5);
1124 self.ip3 += (ip3_target - self.ip3) / self.tau_ip3 * self.dt;
1125 self.ip3 = self.ip3.clamp(0.0, 1.0);
1126
1127 let ca_release = self.ip3.powi(2) * (1.0 - self.ca);
1129 self.ca += (ca_release - self.ca / self.tau_ca) * self.dt;
1130 self.ca = self.ca.clamp(0.0, 1.0);
1131
1132 let i_trpm5 = self.gain * self.ca * 20.0;
1134 self.v += (-(self.v - self.v_rest) + i_trpm5) / self.tau * self.dt;
1135
1136 self.atp_release = self.ca;
1138
1139 self.v
1140 }
1141
1142 pub fn reset(&mut self) {
1143 self.v = self.v_rest;
1144 self.ca = 0.0;
1145 self.ip3 = 0.0;
1146 self.atp_release = 0.0;
1147 }
1148}
1149
1150impl Default for TasteReceptorCell {
1151 fn default() -> Self {
1152 Self::new()
1153 }
1154}
1155
1156#[cfg(test)]
1161mod tests {
1162 use super::*;
1163
1164 #[test]
1167 fn ihc_depolarises_with_displacement() {
1168 let mut c = InnerHairCell::new();
1169 let v_rest = c.v;
1170 for _ in 0..200 {
1171 c.step(50.0);
1172 }
1173 assert!(c.v > v_rest, "IHC should depolarise: v={}", c.v);
1174 }
1175
1176 #[test]
1177 fn ihc_no_change_at_zero() {
1178 let mut c = InnerHairCell::new();
1179 for _ in 0..200 {
1180 c.step(0.0);
1181 }
1182 assert!(
1183 (c.v - c.v_rest).abs() < 5.0,
1184 "IHC should stay near rest with no displacement"
1185 );
1186 }
1187
1188 #[test]
1189 fn ihc_ca_increases_with_depolarisation() {
1190 let mut c = InnerHairCell::new();
1191 for _ in 0..200 {
1192 c.step(60.0);
1193 }
1194 assert!(c.ca > 0.0, "Ca2+ should increase during depolarisation");
1195 }
1196
1197 #[test]
1198 fn ihc_reset_roundtrip() {
1199 let mut c = InnerHairCell::new();
1200 for _ in 0..100 {
1201 c.step(50.0);
1202 }
1203 c.reset();
1204 assert_eq!(c.v, c.v_rest);
1205 assert_eq!(c.ca, 0.05);
1206 assert_eq!(c.q, 8.0);
1207 assert_eq!(c.c, 0.0);
1208 assert_eq!(c.w, 0.0);
1209 }
1210
1211 #[test]
1212 fn ihc_bounded() {
1213 let mut c = InnerHairCell::new();
1214 for _ in 0..10000 {
1215 c.step(100.0);
1216 }
1217 assert!(c.v.is_finite());
1218 assert!(c.ca.is_finite());
1219 }
1220
1221 #[test]
1222 fn ihc_vesicle_pool_depletes() {
1223 let mut c = InnerHairCell::new();
1225 let q0 = c.q;
1226 for _ in 0..5000 {
1227 c.step(80.0);
1228 }
1229 assert!(
1230 c.q < q0,
1231 "Vesicle pool should deplete: q0={q0}, q_now={}",
1232 c.q
1233 );
1234 }
1235
1236 #[test]
1237 fn ihc_cleft_transmitter_rises() {
1238 let mut c = InnerHairCell::new();
1240 for _ in 0..2000 {
1241 c.step(80.0);
1242 }
1243 assert!(
1244 c.c > 0.0,
1245 "Cleft transmitter should rise with stimulation: c={}",
1246 c.c
1247 );
1248 }
1249
1250 #[test]
1251 fn ihc_reprocessing_store_fills() {
1252 let mut c = InnerHairCell::new();
1254 for _ in 0..5000 {
1255 c.step(80.0);
1256 }
1257 assert!(
1258 c.w > 0.0,
1259 "Reprocessing store should fill via reuptake: w={}",
1260 c.w
1261 );
1262 }
1263
1264 #[test]
1265 fn ihc_pool_mass_conserved() {
1266 let mut c = InnerHairCell::new();
1268 for _ in 0..10000 {
1269 c.step(80.0);
1270 }
1271 let total = c.q + c.c + c.w;
1272 assert!(
1273 total <= c.m_pool * 1.5,
1274 "Total transmitter should be bounded: q+c+w={total:.2}, m={}",
1275 c.m_pool
1276 );
1277 }
1278
1279 #[test]
1280 fn ihc_performance() {
1281 let mut c = InnerHairCell::new();
1282 let start = std::time::Instant::now();
1283 for _ in 0..100_000 {
1284 c.step(50.0);
1285 }
1286 assert!(start.elapsed().as_millis() < 50);
1287 }
1288
1289 #[test]
1292 fn ohc_depolarises_and_motility() {
1293 let mut c = OuterHairCell::new();
1294 for _ in 0..200 {
1295 c.step(40.0);
1296 }
1297 assert!(c.v > c.v_rest);
1298 assert!(c.motility.abs() > 0.01, "OHC should show motility");
1299 }
1300
1301 #[test]
1302 fn ohc_prestin_bidirectional() {
1303 let mut dep = OuterHairCell::new();
1306 dep.v = -20.0; dep.step(0.0); let mot_dep = dep.motility;
1309
1310 let mut hyp = OuterHairCell::new();
1311 hyp.v = -80.0; hyp.step(0.0);
1313 let mot_hyp = hyp.motility;
1314
1315 assert!(
1316 mot_dep > 0.0,
1317 "Depolarisation should contract: motility={mot_dep:.3}"
1318 );
1319 assert!(
1320 mot_hyp < 0.0,
1321 "Hyperpolarisation should elongate: motility={mot_hyp:.3}"
1322 );
1323 }
1324
1325 #[test]
1326 fn ohc_prestin_asymmetric() {
1327 let mut dep = OuterHairCell::new();
1330 for _ in 0..2000 {
1331 dep.step(80.0);
1332 } let contraction = dep.motility;
1334
1335 let mut hyp = OuterHairCell::new();
1337 for _ in 0..2000 {
1338 hyp.step(0.0);
1339 } let elongation = hyp.motility;
1341
1342 assert!(
1346 contraction.abs() > elongation.abs() * 0.5,
1347 "Asymmetric prestin: contraction={contraction:.3}, elongation={elongation:.3}"
1348 );
1349 }
1350
1351 #[test]
1352 fn ohc_reset() {
1353 let mut c = OuterHairCell::new();
1354 for _ in 0..100 {
1355 c.step(40.0);
1356 }
1357 c.reset();
1358 assert_eq!(c.motility, 0.0);
1359 }
1360
1361 #[test]
1362 fn ohc_bounded() {
1363 let mut c = OuterHairCell::new();
1364 for _ in 0..10000 {
1365 c.step(100.0);
1366 }
1367 assert!(c.v.is_finite());
1368 }
1369
1370 #[test]
1373 fn rod_hyperpolarises_with_light() {
1374 let mut r = RodPhotoreceptor::new();
1375 let v_dark = r.v;
1376 for _ in 0..1000 {
1377 r.step(100.0);
1378 }
1379 assert!(r.v < v_dark, "rod should hyperpolarise: v={}", r.v);
1380 }
1381
1382 #[test]
1383 fn rod_stays_dark_without_light() {
1384 let mut r = RodPhotoreceptor::new();
1385 for _ in 0..500 {
1386 r.step(0.0);
1387 }
1388 assert!((r.v - r.v_dark).abs() < 1.0);
1389 }
1390
1391 #[test]
1392 fn rod_slow_recovery() {
1393 let mut r = RodPhotoreceptor::new();
1394 for _ in 0..500 {
1396 r.step(200.0);
1397 }
1398 let v_after_flash = r.v;
1399 for _ in 0..1000 {
1401 r.step(0.0);
1402 }
1403 assert!(r.v > v_after_flash, "rod should recover in dark");
1404 assert!(r.v < r.v_dark, "rod should not fully recover in 1000 steps");
1405 }
1406
1407 #[test]
1408 fn rod_cgmp_bounded() {
1409 let mut r = RodPhotoreceptor::new();
1410 for _ in 0..10000 {
1411 r.step(1000.0);
1412 }
1413 assert!(
1414 r.cgmp >= 0.0 && r.cgmp <= 1.5,
1415 "cGMP should be bounded: {}",
1416 r.cgmp
1417 );
1418 r.reset();
1419 for _ in 0..10000 {
1420 r.step(-10.0);
1421 } assert!(
1424 r.cgmp >= 0.0 && r.cgmp <= 1.5,
1425 "cGMP should be bounded: {}",
1426 r.cgmp
1427 );
1428 }
1429
1430 #[test]
1431 fn rod_ca_feedback_adaptation() {
1432 let mut r = RodPhotoreceptor::new();
1435 for _ in 0..5000 {
1437 r.step(100.0);
1438 }
1439 let v_adapted = r.v;
1440 let ca_adapted = r.ca;
1441 assert!(
1443 ca_adapted < 1.0,
1444 "Ca²⁺ should drop during light: ca={ca_adapted:.3}"
1445 );
1446 assert!(
1448 v_adapted > r.v_hyper + 1.0,
1449 "Adaptation should partially restore: v={v_adapted:.1}, v_hyper={}",
1450 r.v_hyper
1451 );
1452 }
1453
1454 #[test]
1455 fn rod_performance() {
1456 let mut r = RodPhotoreceptor::new();
1457 let start = std::time::Instant::now();
1458 for _ in 0..100_000 {
1459 r.step(50.0);
1460 }
1461 assert!(start.elapsed().as_millis() < 50);
1462 }
1463
1464 #[test]
1467 fn cone_hyperpolarises_with_light() {
1468 let mut c = ConePhotoreceptor::new();
1469 let v_dark = c.v;
1470 for _ in 0..500 {
1471 c.step(500.0);
1472 }
1473 assert!(c.v < v_dark);
1474 }
1475
1476 #[test]
1477 fn cone_faster_than_rod() {
1478 let mut rod = RodPhotoreceptor::new();
1479 let mut cone = ConePhotoreceptor::new();
1480 for _ in 0..500 {
1482 rod.step(100.0);
1483 cone.step(100.0);
1484 }
1485 for _ in 0..2000 {
1486 rod.step(0.0);
1487 cone.step(0.0);
1488 }
1489 let rod_recovery = rod.v - rod.v_hyper;
1491 let cone_recovery = cone.v - cone.v_hyper;
1492 assert!(
1493 cone_recovery > rod_recovery,
1494 "cone ({cone_recovery:.1}) should recover more than rod ({rod_recovery:.1})"
1495 );
1496 }
1497
1498 #[test]
1499 fn cone_reset() {
1500 let mut c = ConePhotoreceptor::new();
1501 for _ in 0..500 {
1502 c.step(500.0);
1503 }
1504 c.reset();
1505 assert_eq!(c.cgmp, 1.0);
1506 assert_eq!(c.v, c.v_dark);
1507 }
1508
1509 #[test]
1512 fn rgc_on_fires_with_positive_input() {
1513 let mut rgc = RetinalGanglionCell::new();
1514 let spikes: i32 = (0..500).map(|_| rgc.step(20.0)).sum();
1515 assert!(spikes > 0, "ON-RGC should fire with positive input via GLM");
1516 }
1517
1518 #[test]
1519 fn rgc_off_fires_with_negative_input() {
1520 let mut rgc = RetinalGanglionCell::off_centre();
1521 let spikes: i32 = (0..500).map(|_| rgc.step(-20.0)).sum();
1522 assert!(spikes > 0, "OFF-RGC should fire with negative input");
1523 }
1524
1525 #[test]
1526 fn rgc_on_no_fire_without_input() {
1527 let mut rgc = RetinalGanglionCell::new();
1528 let spikes: i32 = (0..500).map(|_| rgc.step(0.0)).sum();
1529 assert_eq!(
1530 spikes, 0,
1531 "GLM with baseline=-3 should be quiescent without input"
1532 );
1533 }
1534
1535 #[test]
1536 fn rgc_history_filter_produces_refractoriness() {
1537 let mut rgc = RetinalGanglionCell::new();
1540 let mut spikes = Vec::new();
1541 for _ in 0..200 {
1543 spikes.push(rgc.step(5.0));
1544 }
1545 for (i, &s) in spikes.iter().enumerate() {
1547 if s == 1 && i + 3 < spikes.len() {
1548 let next3: i32 = spikes[i + 1..i + 4].iter().sum();
1549 assert!(
1550 next3 < 3,
1551 "History filter should suppress some re-firing after spike at {}",
1552 i
1553 );
1554 break;
1555 }
1556 }
1557 }
1558
1559 #[test]
1560 fn rgc_stimulus_filter_is_temporal() {
1561 let mut rgc = RetinalGanglionCell::new();
1563 for _ in 0..5 {
1565 rgc.step(50.0);
1566 }
1567 let late_spikes: i32 = (0..50).map(|_| rgc.step(0.0)).sum();
1569 let has_memory = rgc.stim_buffer.iter().any(|&x| x != 0.0);
1571 assert!(
1572 has_memory || late_spikes >= 0,
1573 "Stimulus filter should retain memory"
1574 );
1575 }
1576
1577 #[test]
1578 fn rgc_glm_has_both_filters() {
1579 let rgc = RetinalGanglionCell::new();
1581 assert!(!rgc.stim_kernel.is_empty(), "Must have stimulus filter");
1582 assert!(!rgc.hist_kernel.is_empty(), "Must have history filter");
1583 assert!(rgc.stim_kernel.len() >= 10, "Stimulus filter too short");
1584 assert!(rgc.hist_kernel.len() >= 10, "History filter too short");
1585 }
1586
1587 #[test]
1588 fn rgc_reset_clears_buffers() {
1589 let mut rgc = RetinalGanglionCell::new();
1590 for _ in 0..100 {
1591 rgc.step(20.0);
1592 }
1593 rgc.reset();
1594 assert!(
1595 rgc.stim_buffer.iter().all(|&x| x == 0.0),
1596 "Stimulus buffer not cleared"
1597 );
1598 assert!(
1599 rgc.hist_buffer.iter().all(|&x| x == 0.0),
1600 "History buffer not cleared"
1601 );
1602 }
1603
1604 #[test]
1607 fn merkel_fires_with_sustained_pressure() {
1608 let mut m = MerkelCell::new();
1609 let spikes: i32 = (0..2000).map(|_| m.step(20.0)).sum();
1610 assert!(spikes > 0, "Merkel should fire with sustained pressure");
1611 }
1612
1613 #[test]
1614 fn merkel_slow_adaptation() {
1615 let mut m = MerkelCell::new();
1616 let first: i32 = (0..1000).map(|_| m.step(20.0)).sum();
1617 let second: i32 = (0..1000).map(|_| m.step(20.0)).sum();
1618 assert!(
1620 second > 0,
1621 "Merkel should still fire in second half (slow adapting)"
1622 );
1623 assert!(second <= first + 5, "Merkel should slowly adapt");
1624 }
1625
1626 #[test]
1627 fn merkel_no_fire_without_pressure() {
1628 let mut m = MerkelCell::new();
1629 let spikes: i32 = (0..1000).map(|_| m.step(0.0)).sum();
1630 assert_eq!(spikes, 0);
1631 }
1632
1633 #[test]
1634 fn merkel_closed_form_membrane_and_adaptation_relaxation() {
1635 let mut m = MerkelCell::new();
1636 m.v = -66.0;
1637 m.adapt = 0.2;
1638 m.gain = 0.0;
1639
1640 let v_inf = m.v_rest - m.adapt;
1641 let expected_v = exact_relax_merkel(m.v, v_inf, m.tau, m.dt);
1642 let adapt_inf = (m.a_adapt * (expected_v - m.v_rest).max(0.0)).max(0.0);
1643 let expected_adapt = exact_relax_merkel(m.adapt, adapt_inf, m.tau_adapt, m.dt).max(0.0);
1644
1645 assert_eq!(m.step(0.0), 0);
1646 assert_close_merkel(m.v, expected_v, 1e-12);
1647 assert_close_merkel(m.adapt, expected_adapt, 1e-12);
1648 }
1649
1650 #[test]
1651 fn merkel_invalid_input_preserves_state() {
1652 let mut m = MerkelCell::new();
1653 let before = m.clone();
1654 assert_eq!(m.step(f64::NAN), 0);
1655 assert_eq!(m.v, before.v);
1656 assert_eq!(m.adapt, before.adapt);
1657 }
1658
1659 #[test]
1660 fn merkel_corrupted_state_preserved_on_step() {
1661 let mut m = MerkelCell::new();
1662 m.adapt = f64::NAN;
1663 let before = m.clone();
1664 assert_eq!(m.step(20.0), 0);
1665 assert_eq!(m.v, before.v);
1666 assert!(m.adapt.is_nan());
1667 }
1668
1669 #[test]
1670 fn merkel_invalid_voltage_preserved_on_step() {
1671 let mut m = MerkelCell::new();
1672 m.v = 60.1;
1673 let before = m.clone();
1674 assert_eq!(m.step(20.0), 0);
1675 assert_eq!(m.v, before.v);
1676 assert_eq!(m.adapt, before.adapt);
1677 }
1678
1679 fn exact_relax_merkel(value: f64, target: f64, tau: f64, dt: f64) -> f64 {
1680 target + (value - target) * (-dt / tau).exp()
1681 }
1682
1683 fn assert_close_merkel(actual: f64, expected: f64, tolerance: f64) {
1684 assert!(
1685 (actual - expected).abs() <= tolerance,
1686 "actual={:.16e} expected={:.16e} tolerance={:.3e}",
1687 actual,
1688 expected,
1689 tolerance
1690 );
1691 }
1692
1693 #[test]
1694 fn merkel_reset() {
1695 let mut m = MerkelCell::new();
1696 for _ in 0..500 {
1697 m.step(20.0);
1698 }
1699 m.reset();
1700 assert_eq!(m.adapt, 0.0);
1701 }
1702
1703 #[test]
1706 fn pacinian_fires_on_pressure_onset() {
1707 let mut p = PacinianCorpuscle::new();
1708 let spikes: i32 = (0..100).map(|i| p.step(i as f64 * 2.0)).sum();
1710 assert!(spikes > 0, "Pacinian should fire on pressure onset");
1711 }
1712
1713 #[test]
1714 fn pacinian_adapts_to_sustained() {
1715 let mut p = PacinianCorpuscle::new();
1716 let onset: i32 = (0..10).map(|i| p.step(i as f64 * 10.0)).sum();
1718 let sustained: i32 = (0..500).map(|_| p.step(100.0)).sum();
1720 assert!(
1722 sustained <= onset + 5,
1723 "Pacinian should adapt to sustained: onset={onset}, sustained={sustained}"
1724 );
1725 }
1726
1727 #[test]
1728 fn pacinian_no_fire_at_rest() {
1729 let mut p = PacinianCorpuscle::new();
1730 let spikes: i32 = (0..500).map(|_| p.step(0.0)).sum();
1731 assert_eq!(spikes, 0);
1732 }
1733
1734 #[test]
1735 fn pacinian_closed_form_membrane_and_adaptation_relaxation() {
1736 let mut p = PacinianCorpuscle::new();
1737 p.v = -66.0;
1738 p.prev_pressure = 5.0;
1739 p.adapt = 0.2;
1740 p.gain = 0.0;
1741
1742 let pressure = 5.0;
1743 let dp = (pressure - p.prev_pressure) / p.dt;
1744 let drive = p.gain * dp.abs() - p.adapt;
1745 let expected_v = exact_relax_pacinian(p.v, p.v_rest + drive, p.tau, p.dt);
1746 let expected_adapt =
1747 exact_relax_pacinian(p.adapt, 0.5 * drive.max(0.0), p.tau_adapt, p.dt).max(0.0);
1748
1749 assert_eq!(p.step(pressure), 0);
1750 assert_eq!(p.prev_pressure, pressure);
1751 assert_close_pacinian(p.v, expected_v, 1e-12);
1752 assert_close_pacinian(p.adapt, expected_adapt, 1e-12);
1753 }
1754
1755 #[test]
1756 fn pacinian_invalid_input_preserves_state() {
1757 let mut p = PacinianCorpuscle::new();
1758 p.prev_pressure = 12.0;
1759 p.adapt = 0.4;
1760 let before = p.clone();
1761 assert_eq!(p.step(f64::NAN), 0);
1762 assert_eq!(p.v, before.v);
1763 assert_eq!(p.prev_pressure, before.prev_pressure);
1764 assert_eq!(p.adapt, before.adapt);
1765 }
1766
1767 #[test]
1768 fn pacinian_corrupted_state_preserved_on_step() {
1769 let mut p = PacinianCorpuscle::new();
1770 p.prev_pressure = f64::NAN;
1771 let before = p.clone();
1772 assert_eq!(p.step(10.0), 0);
1773 assert_eq!(p.v, before.v);
1774 assert!(p.prev_pressure.is_nan());
1775 assert_eq!(p.adapt, before.adapt);
1776 }
1777
1778 #[test]
1779 fn pacinian_invalid_voltage_preserved_on_step() {
1780 let mut p = PacinianCorpuscle::new();
1781 p.v = -100.1;
1782 let before = p.clone();
1783 assert_eq!(p.step(10.0), 0);
1784 assert_eq!(p.v, before.v);
1785 assert_eq!(p.prev_pressure, before.prev_pressure);
1786 assert_eq!(p.adapt, before.adapt);
1787 }
1788
1789 fn exact_relax_pacinian(value: f64, target: f64, tau: f64, dt: f64) -> f64 {
1790 target + (value - target) * (-dt / tau).exp()
1791 }
1792
1793 fn assert_close_pacinian(actual: f64, expected: f64, tolerance: f64) {
1794 assert!(
1795 (actual - expected).abs() <= tolerance,
1796 "actual={:.16e} expected={:.16e} tolerance={:.3e}",
1797 actual,
1798 expected,
1799 tolerance
1800 );
1801 }
1802
1803 #[test]
1804 fn pacinian_reset() {
1805 let mut p = PacinianCorpuscle::new();
1806 for i in 0..100 {
1807 p.step(i as f64);
1808 }
1809 p.reset();
1810 assert_eq!(p.prev_pressure, 0.0);
1811 assert_eq!(p.adapt, 0.0);
1812 }
1813
1814 #[test]
1817 fn nociceptor_high_threshold() {
1818 let mut n = Nociceptor::new();
1819 let low: i32 = (0..500).map(|_| n.step(5.0)).sum();
1821 assert_eq!(low, 0, "nociceptor should not fire at low stimulus");
1822 n.reset();
1824 let high: i32 = (0..500).map(|_| n.step(50.0)).sum();
1825 assert!(high > 0, "nociceptor should fire at high stimulus");
1826 }
1827
1828 #[test]
1829 fn nociceptor_closed_form_membrane_and_sensitisation_decay() {
1830 let mut n = Nociceptor::new();
1831 n.v = -60.0;
1832 n.sensitisation = 4.0;
1833 n.gain = 0.5;
1834
1835 let stimulus = 8.0;
1836 let drive = n.gain * stimulus;
1837 let expected_v = exact_relax_nociceptor(n.v, n.v_rest + drive, n.tau, n.dt);
1838 let expected_sensitisation =
1839 exact_relax_nociceptor(n.sensitisation, 0.0, n.tau_sens, n.dt).max(0.0);
1840
1841 assert_eq!(n.step(stimulus), 0);
1842 assert_close_nociceptor(n.v, expected_v, 1e-12);
1843 assert_close_nociceptor(n.sensitisation, expected_sensitisation, 1e-12);
1844 }
1845
1846 #[test]
1847 fn nociceptor_invalid_input_preserves_state() {
1848 let mut n = Nociceptor::new();
1849 n.v = -60.0;
1850 n.sensitisation = 2.0;
1851 let before = n.clone();
1852
1853 assert_eq!(n.step(f64::NAN), 0);
1854 assert_eq!(n.v, before.v);
1855 assert_eq!(n.sensitisation, before.sensitisation);
1856 }
1857
1858 #[test]
1859 fn nociceptor_corrupted_state_preserved_on_step() {
1860 let mut n = Nociceptor::new();
1861 n.sensitisation = f64::NAN;
1862 let before = n.clone();
1863
1864 assert_eq!(n.step(50.0), 0);
1865 assert_eq!(n.v, before.v);
1866 assert!(n.sensitisation.is_nan());
1867 }
1868
1869 #[test]
1870 fn nociceptor_invalid_voltage_preserved_on_step() {
1871 let mut n = Nociceptor::new();
1872 n.v = -100.1;
1873 let before = n.clone();
1874
1875 assert_eq!(n.step(50.0), 0);
1876 assert_eq!(n.v, before.v);
1877 assert_eq!(n.sensitisation, before.sensitisation);
1878 }
1879
1880 #[test]
1881 fn nociceptor_overflowing_drive_preserves_state() {
1882 let mut n = Nociceptor::new();
1883 n.gain = f64::MAX;
1884 let before = n.clone();
1885
1886 assert_eq!(n.step(2.0), 0);
1887 assert_eq!(n.v, before.v);
1888 assert_eq!(n.sensitisation, before.sensitisation);
1889 }
1890
1891 fn exact_relax_nociceptor(value: f64, target: f64, tau: f64, dt: f64) -> f64 {
1892 target + (value - target) * (-dt / tau).exp()
1893 }
1894
1895 fn assert_close_nociceptor(actual: f64, expected: f64, tolerance: f64) {
1896 assert!(
1897 (actual - expected).abs() <= tolerance,
1898 "actual={:.16e} expected={:.16e} tolerance={:.3e}",
1899 actual,
1900 expected,
1901 tolerance
1902 );
1903 }
1904
1905 #[test]
1906 fn nociceptor_sensitisation() {
1907 let mut n = Nociceptor::new();
1908 for _ in 0..1000 {
1910 n.step(50.0);
1911 }
1912 assert!(n.sensitisation > 0.0, "sensitisation should increase");
1913 let sens = n.sensitisation;
1914 for _ in 0..50000 {
1916 n.step(0.0);
1917 }
1918 assert!(
1919 n.sensitisation < sens,
1920 "sensitisation should decay: was {sens}, now {}",
1921 n.sensitisation
1922 );
1923 }
1924
1925 #[test]
1926 fn nociceptor_no_fire_without_stimulus() {
1927 let mut n = Nociceptor::new();
1928 let spikes: i32 = (0..1000).map(|_| n.step(0.0)).sum();
1929 assert_eq!(spikes, 0);
1930 }
1931
1932 #[test]
1933 fn nociceptor_reset() {
1934 let mut n = Nociceptor::new();
1935 for _ in 0..500 {
1936 n.step(50.0);
1937 }
1938 n.reset();
1939 assert_eq!(n.sensitisation, 0.0);
1940 }
1941
1942 #[test]
1945 fn olfactory_fires_with_odorant() {
1946 let mut o = OlfactoryReceptorNeuron::new();
1947 let spikes: i32 = (0..2000).map(|_| o.step(5.0)).sum();
1948 assert!(spikes > 0, "olfactory should fire with odorant");
1949 }
1950
1951 #[test]
1952 fn olfactory_adapts() {
1953 let mut o = OlfactoryReceptorNeuron::new();
1954 let first: i32 = (0..2000).map(|_| o.step(5.0)).sum();
1955 let second: i32 = (0..2000).map(|_| o.step(5.0)).sum();
1956 assert!(
1957 second <= first + 5,
1958 "olfactory should adapt: first={first}, second={second}"
1959 );
1960 }
1961
1962 #[test]
1963 fn olfactory_no_fire_without_odorant() {
1964 let mut o = OlfactoryReceptorNeuron::new();
1965 let spikes: i32 = (0..1000).map(|_| o.step(0.0)).sum();
1966 assert_eq!(spikes, 0);
1967 }
1968
1969 #[test]
1970 fn olfactory_reset() {
1971 let mut o = OlfactoryReceptorNeuron::new();
1972 for _ in 0..1000 {
1973 o.step(5.0);
1974 }
1975 o.reset();
1976 assert_eq!(o.camp, 0.0);
1977 assert_eq!(o.adapt, 0.0);
1978 assert_eq!(o.pde4, 0.0);
1979 }
1980
1981 #[test]
1982 fn olfactory_pde4_activates_with_odorant() {
1983 let mut o = OlfactoryReceptorNeuron::new();
1985 assert_eq!(o.pde4, 0.0);
1986 for _ in 0..5000 {
1987 o.step(5.0);
1988 }
1989 assert!(
1990 o.pde4 > 0.0,
1991 "PDE4 should activate with sustained odorant, got {}",
1992 o.pde4
1993 );
1994 }
1995
1996 #[test]
1997 fn olfactory_pde4_reduces_camp() {
1998 let mut with_pde4 = OlfactoryReceptorNeuron::new();
2000 let mut no_pde4 = OlfactoryReceptorNeuron::new();
2001 no_pde4.k_pde4 = 0.0; for _ in 0..10_000 {
2004 with_pde4.step(5.0);
2005 no_pde4.step(5.0);
2006 }
2007 assert!(
2008 with_pde4.camp < no_pde4.camp,
2009 "PDE4 should reduce cAMP: with={:.3} vs without={:.3}",
2010 with_pde4.camp,
2011 no_pde4.camp
2012 );
2013 }
2014
2015 #[test]
2016 fn olfactory_pde4_enhances_adaptation() {
2017 let mut with_pde4 = OlfactoryReceptorNeuron::new();
2019 let mut no_pde4 = OlfactoryReceptorNeuron::new();
2020 no_pde4.k_pde4 = 0.0;
2021
2022 for _ in 0..5000 {
2024 with_pde4.step(5.0);
2025 no_pde4.step(5.0);
2026 }
2027 let spikes_with: i32 = (0..5000).map(|_| with_pde4.step(5.0)).sum();
2029 let spikes_no: i32 = (0..5000).map(|_| no_pde4.step(5.0)).sum();
2030 assert!(
2031 spikes_with <= spikes_no,
2032 "PDE4 should enhance adaptation: with={spikes_with}, without={spikes_no}"
2033 );
2034 }
2035
2036 #[test]
2039 fn taste_depolarises_with_tastant() {
2040 let mut t = TasteReceptorCell::new();
2041 let v_rest = t.v;
2042 for _ in 0..500 {
2043 t.step(5.0);
2044 }
2045 assert!(t.v > v_rest, "taste cell should depolarise");
2046 }
2047
2048 #[test]
2049 fn taste_atp_release() {
2050 let mut t = TasteReceptorCell::new();
2051 for _ in 0..500 {
2052 t.step(5.0);
2053 }
2054 assert!(t.atp_release > 0.0, "ATP should be released");
2055 }
2056
2057 #[test]
2058 fn taste_no_response_without_tastant() {
2059 let mut t = TasteReceptorCell::new();
2060 for _ in 0..500 {
2061 t.step(0.0);
2062 }
2063 assert!((t.v - t.v_rest).abs() < 2.0);
2064 assert!(t.atp_release < 0.01);
2065 }
2066
2067 #[test]
2068 fn taste_ca_bounded() {
2069 let mut t = TasteReceptorCell::new();
2070 for _ in 0..10000 {
2071 t.step(100.0);
2072 }
2073 assert!(t.ca >= 0.0 && t.ca <= 1.0);
2074 assert!(t.ip3 >= 0.0 && t.ip3 <= 1.0);
2075 }
2076
2077 #[test]
2078 fn taste_reset() {
2079 let mut t = TasteReceptorCell::new();
2080 for _ in 0..500 {
2081 t.step(5.0);
2082 }
2083 t.reset();
2084 assert_eq!(t.ca, 0.0);
2085 assert_eq!(t.ip3, 0.0);
2086 assert_eq!(t.atp_release, 0.0);
2087 }
2088}
2089
2090#[derive(Clone, Debug)]
2102pub struct DirectionSelectiveRGC {
2103 pub v: f64,
2104 pub tau: f64,
2105 pub theta: f64,
2106 pub dt: f64,
2107 pub is_on_centre: bool,
2108 prev_intensity: f64,
2109 surround: f64,
2110 pub w_centre: f64,
2111 pub w_surround: f64,
2112 pub direction_pref: f64,
2113}
2114
2115impl DirectionSelectiveRGC {
2116 pub fn new_on() -> Self {
2117 Self {
2118 v: 0.0,
2119 tau: 10.0,
2120 theta: 0.5,
2121 dt: 1.0,
2122 is_on_centre: true,
2123 prev_intensity: 0.0,
2124 surround: 0.0,
2125 w_centre: 1.0,
2126 w_surround: 0.3,
2127 direction_pref: 0.0,
2128 }
2129 }
2130
2131 pub fn new_off() -> Self {
2132 let mut cell = Self::new_on();
2133 cell.is_on_centre = false;
2134 cell
2135 }
2136
2137 fn valid_runtime(&self) -> bool {
2138 [
2139 self.v,
2140 self.tau,
2141 self.theta,
2142 self.dt,
2143 self.prev_intensity,
2144 self.surround,
2145 self.w_centre,
2146 self.w_surround,
2147 self.direction_pref,
2148 ]
2149 .iter()
2150 .all(|x| x.is_finite())
2151 && self.tau > 0.0
2152 && self.theta > 0.0
2153 && self.dt > 0.0
2154 && self.prev_intensity >= 0.0
2155 && self.surround >= 0.0
2156 && self.w_centre >= 0.0
2157 && self.w_surround >= 0.0
2158 }
2159
2160 pub fn step_rf(&mut self, intensity: f64, surround_mean: f64) -> i32 {
2162 if !intensity.is_finite()
2163 || !surround_mean.is_finite()
2164 || intensity < 0.0
2165 || surround_mean < 0.0
2166 || !self.valid_runtime()
2167 {
2168 return 0;
2169 }
2170 let temporal_diff = intensity - self.prev_intensity;
2171 let centre_response = if self.is_on_centre {
2172 self.w_centre * temporal_diff
2173 } else {
2174 -self.w_centre * temporal_diff
2175 };
2176
2177 let next_surround = 0.9 * self.surround + 0.1 * surround_mean;
2178 let surround_inhib = self.w_surround * next_surround;
2179 let drive = centre_response - surround_inhib;
2180 let decay = (-self.dt / self.tau).exp();
2181 let next_v = drive + (self.v - drive) * decay;
2182 if !next_surround.is_finite()
2183 || !drive.is_finite()
2184 || !decay.is_finite()
2185 || !next_v.is_finite()
2186 || next_surround < 0.0
2187 {
2188 return 0;
2189 }
2190
2191 self.prev_intensity = intensity;
2192 self.surround = next_surround;
2193 if next_v >= self.theta {
2194 self.v = 0.0;
2195 1
2196 } else {
2197 self.v = next_v;
2198 0
2199 }
2200 }
2201
2202 pub fn step(&mut self, current: f64) -> i32 {
2204 self.step_rf(current, 0.0)
2205 }
2206
2207 pub fn reset(&mut self) {
2208 self.v = 0.0;
2209 self.prev_intensity = 0.0;
2210 self.surround = 0.0;
2211 }
2212}
2213
2214impl Default for DirectionSelectiveRGC {
2215 fn default() -> Self {
2216 Self::new_on()
2217 }
2218}
2219
2220#[derive(Clone, Debug)]
2231pub struct CochlearHairCell {
2232 pub v: f64,
2233 pub g_max: f64,
2234 pub e_met: f64,
2235 pub g_l: f64,
2236 pub e_l: f64,
2237 pub cap: f64,
2238 pub x0: f64,
2239 pub delta: f64,
2240 pub dt: f64,
2241 pub glutamate_release: f64,
2242}
2243
2244impl CochlearHairCell {
2245 pub fn new() -> Self {
2246 Self {
2247 v: -60.0,
2248 g_max: 10.0,
2249 e_met: 0.0,
2250 g_l: 1.0,
2251 e_l: -60.0,
2252 cap: 10.0,
2253 x0: 0.0,
2254 delta: 0.1,
2255 dt: 0.01,
2256 glutamate_release: 0.0,
2257 }
2258 }
2259
2260 fn p_open(&self, displacement: f64) -> f64 {
2262 let z = (displacement - self.x0) / self.delta;
2263 if z >= 0.0 {
2264 1.0 / (1.0 + (-z).exp())
2265 } else {
2266 let ez = z.exp();
2267 ez / (1.0 + ez)
2268 }
2269 }
2270
2271 fn valid_runtime(&self) -> bool {
2272 [
2273 self.v,
2274 self.g_max,
2275 self.e_met,
2276 self.g_l,
2277 self.e_l,
2278 self.cap,
2279 self.x0,
2280 self.delta,
2281 self.dt,
2282 self.glutamate_release,
2283 ]
2284 .iter()
2285 .all(|x| x.is_finite())
2286 && self.g_max >= 0.0
2287 && self.g_l > 0.0
2288 && self.cap > 0.0
2289 && self.delta > 0.0
2290 && self.dt > 0.0
2291 && self.glutamate_release >= 0.0
2292 }
2293
2294 pub fn step(&mut self, displacement: f64) -> i32 {
2296 if !self.valid_runtime() || !displacement.is_finite() {
2297 return 0;
2298 }
2299 let po = self.p_open(displacement);
2300 let g_met = self.g_max * po;
2301 let g_total = self.g_l + g_met;
2302 if !(g_total.is_finite() && g_total > 0.0) {
2303 return 0;
2304 }
2305 let v_inf = (self.g_l * self.e_l + g_met * self.e_met) / g_total;
2306 let candidate_v = v_inf + (self.v - v_inf) * (-(g_total / self.cap) * self.dt).exp();
2307 let candidate_release = (candidate_v + 60.0).max(0.0) / 40.0;
2308 if !(candidate_v.is_finite() && candidate_release.is_finite()) {
2309 return 0;
2310 }
2311 self.v = candidate_v;
2312
2313 self.glutamate_release = candidate_release;
2315 if self.glutamate_release > 0.5 {
2316 1
2317 } else {
2318 0
2319 }
2320 }
2321
2322 pub fn reset(&mut self) {
2323 self.v = self.e_l;
2324 self.glutamate_release = 0.0;
2325 }
2326}
2327
2328impl Default for CochlearHairCell {
2329 fn default() -> Self {
2330 Self::new()
2331 }
2332}
2333
2334#[cfg(test)]
2335mod gap_sensory_tests {
2336 use super::*;
2337
2338 #[test]
2339 fn rgc_on_responds_to_light_increase() {
2340 let mut cell = DirectionSelectiveRGC::new_on();
2341 for _ in 0..10 {
2343 cell.step_rf(0.0, 0.0);
2344 }
2345 let mut spikes = 0;
2346 for _ in 0..20 {
2347 spikes += cell.step_rf(6.0, 0.0);
2348 }
2349 assert!(spikes > 0, "On-centre must respond to light increase");
2350 }
2351
2352 #[test]
2353 fn rgc_off_responds_to_light_decrease() {
2354 let mut cell = DirectionSelectiveRGC::new_off();
2355 cell.theta = 0.1; let mut spikes = 0;
2358 for i in 0..400 {
2359 let intensity = if (i / 10) % 2 == 0 { 5.0 } else { 0.0 };
2360 spikes += cell.step_rf(intensity, 0.0);
2361 }
2362 assert!(spikes > 0, "Off-centre must respond to light transitions");
2363 }
2364
2365 #[test]
2366 fn rgc_surround_inhibition() {
2367 let mut no_surr = DirectionSelectiveRGC::new_on();
2368 let mut with_surr = DirectionSelectiveRGC::new_on();
2369 let mut spikes_no = 0;
2371 let mut spikes_surr = 0;
2372 for i in 0..200 {
2373 let intensity = if i % 10 == 0 { 3.0 } else { 0.0 };
2374 spikes_no += no_surr.step_rf(intensity, 0.0);
2375 spikes_surr += with_surr.step_rf(intensity, 2.0);
2376 }
2377 assert!(
2378 spikes_surr <= spikes_no,
2379 "Surround should inhibit: surr={spikes_surr} <= no={spikes_no}"
2380 );
2381 }
2382
2383 #[test]
2384 fn rgc_exact_membrane_relaxation() {
2385 let mut cell = DirectionSelectiveRGC::new_on();
2386 cell.tau = 7.0;
2387 cell.theta = 100.0;
2388 cell.dt = 1.25;
2389 cell.w_centre = 1.4;
2390 cell.w_surround = 0.2;
2391 cell.v = 0.35;
2392 let expected_surround = 0.9 * cell.surround + 0.1 * 0.5;
2393 let expected_drive =
2394 cell.w_centre * (2.0 - cell.prev_intensity) - cell.w_surround * expected_surround;
2395 let expected_v = expected_drive + (cell.v - expected_drive) * (-cell.dt / cell.tau).exp();
2396 assert_eq!(cell.step_rf(2.0, 0.5), 0);
2397 assert!((cell.v - expected_v).abs() < 1e-12);
2398 assert!((cell.surround - expected_surround).abs() < 1e-12);
2399 }
2400
2401 #[test]
2402 fn rgc_invalid_drive_preserves_state() {
2403 let mut cell = DirectionSelectiveRGC::new_on();
2404 let before = (cell.v, cell.prev_intensity, cell.surround);
2405 assert_eq!(cell.step_rf(f64::NAN, 0.0), 0);
2406 assert_eq!((cell.v, cell.prev_intensity, cell.surround), before);
2407 }
2408
2409 #[test]
2410 fn rgc_corrupt_runtime_state_preserves_state() {
2411 let mut cell = DirectionSelectiveRGC::new_on();
2412 cell.surround = f64::INFINITY;
2413 let before = (cell.v, cell.prev_intensity, cell.surround);
2414 assert_eq!(cell.step_rf(1.0, 0.0), 0);
2415 assert_eq!((cell.v, cell.prev_intensity, cell.surround), before);
2416 }
2417
2418 #[test]
2419 fn cochlear_displacement_depolarises() {
2420 let mut cell = CochlearHairCell::new();
2421 let v_rest = cell.v;
2422 for _ in 0..100 {
2423 cell.step(0.5);
2424 }
2425 assert!(
2426 cell.v > v_rest,
2427 "Positive displacement should depolarise: {:.2} > {:.2}",
2428 cell.v,
2429 v_rest
2430 );
2431 }
2432
2433 #[test]
2434 fn cochlear_matches_closed_form_membrane_relaxation() {
2435 let mut cell = CochlearHairCell::new();
2436 let po = 1.0 / (1.0 + (-(0.0 - cell.x0) / cell.delta).exp());
2437 let g_met = cell.g_max * po;
2438 let g_total = cell.g_l + g_met;
2439 let v_inf = (cell.g_l * cell.e_l + g_met * cell.e_met) / g_total;
2440 let expected = v_inf + (cell.v - v_inf) * (-(g_total / cell.cap) * cell.dt).exp();
2441 let spike = cell.step(0.0);
2442 assert!(spike == 0 || spike == 1);
2443 assert!((cell.v - expected).abs() < 1e-12);
2444 }
2445
2446 #[test]
2447 fn cochlear_invalid_runtime_preserves_state() {
2448 let mut cell = CochlearHairCell::new();
2449 cell.v = -55.0;
2450 cell.glutamate_release = 0.125;
2451 let before = (cell.v, cell.glutamate_release);
2452 cell.cap = -1.0;
2453 assert_eq!(cell.step(0.25), 0);
2454 assert_eq!((cell.v, cell.glutamate_release), before);
2455 }
2456
2457 #[test]
2458 fn cochlear_graded_release() {
2459 let mut cell = CochlearHairCell::new();
2460 for _ in 0..200 {
2461 cell.step(1.0);
2462 }
2463 assert!(
2464 cell.glutamate_release > 0.0,
2465 "Should release glutamate: {:.4}",
2466 cell.glutamate_release
2467 );
2468 }
2469
2470 #[test]
2471 fn cochlear_zero_displacement_rest() {
2472 let mut cell = CochlearHairCell::new();
2473 for _ in 0..100 {
2474 cell.step(0.0);
2475 }
2476 assert!(
2478 cell.v > -80.0 && cell.v < 0.0,
2479 "Zero displacement → physiological range: {:.2}",
2480 cell.v
2481 );
2482 }
2483
2484 #[test]
2485 fn cochlear_reset() {
2486 let mut cell = CochlearHairCell::new();
2487 for _ in 0..100 {
2488 cell.step(1.0);
2489 }
2490 cell.reset();
2491 assert_eq!(cell.v, cell.e_l);
2492 assert_eq!(cell.glutamate_release, 0.0);
2493 }
2494}