Source code for mercurial.core.sensory_transduction

"""Realistic sensory transduction models with empirical parameters."""

from typing import Optional

import numpy as np

from mercurial.params.empirical import HairCellParams, PhotoreceptorParams


[docs] class Photoreceptor: """ Photoreceptor model with Naka‑Rushton non‑linearity and light adaptation. Uses empirical parameters from literature. """ def __init__(self, params: Optional[PhotoreceptorParams] = None, **kwargs): if params is None: params = PhotoreceptorParams() self.tau_p = kwargs.get("tau_p", params.tau_p) self.R_max = kwargs.get("R_max", params.R_max) self.I50 = kwargs.get("I50", params.I50) self.n = kwargs.get("n", params.n) self.alpha = kwargs.get("alpha", params.alpha_adapt) self.sigma = kwargs.get("sigma", 0.01) self.V = 0.0 self.rng = np.random.default_rng()
[docs] def response(self, I: float) -> float: if I <= 0: return 0.0 return self.R_max * (I**self.n) / (I**self.n + self.I50**self.n)
[docs] def step(self, dt: float, I: float) -> float: # Adaptation: I50 follows mean intensity dI50 = self.alpha * (I - self.I50) self.I50 += dI50 * dt self.I50 = max(1.0, self.I50) # Photoreceptor potential dynamics target = self.response(I) / self.R_max dV = (-self.V + target) / self.tau_p noise = self.sigma * np.sqrt(dt) * self.rng.normal() self.V += dV * dt + noise self.V = np.clip(self.V, 0.0, 1.0) return self.V
[docs] class BipolarCell: """ Bipolar cell with ON and OFF pathways. """ def __init__(self, tau_b: float = 0.010, on_center: bool = True): self.tau_b = tau_b self.on_center = on_center self.B = 0.0 self.rng = np.random.default_rng()
[docs] def step(self, dt: float, V_photoreceptor: float) -> float: if self.on_center: input_signal = max(0.0, V_photoreceptor) else: input_signal = max(0.0, 1.0 - V_photoreceptor) dB = (-self.B + input_signal) / self.tau_b self.B += dB * dt self.B = np.clip(self.B, 0.0, 1.0) return self.B
[docs] class HairCell: """ Hair cell mechano‑transduction with fast and slow adaptation. Uses empirical parameters. """ def __init__(self, params: Optional[HairCellParams] = None, **kwargs): if params is None: params = HairCellParams() self.g_max = kwargs.get("g_max", params.g_max) self.z = kwargs.get("z", params.z) self.x0 = kwargs.get("x0", params.x0) self.tau_fast = kwargs.get("tau_fast", params.tau_fast) self.tau_slow = kwargs.get("tau_slow", params.tau_slow) self.k_fast = kwargs.get("k_fast", 1.0) self.k_slow = kwargs.get("k_slow", 1.0) self.sigma = kwargs.get("sigma", 0.01) self.a = 0.0 self.s = 0.0 self.rng = np.random.default_rng()
[docs] def open_probability(self, x: float) -> float: return 1.0 / (1.0 + np.exp(-self.z * (x - self.x0)))
[docs] def step(self, dt: float, displacement: float) -> float: P = self.open_probability(displacement) da = (-self.a + self.k_fast * P) / self.tau_fast ds = (-self.s + self.k_slow * P) / self.tau_slow self.a += da * dt self.s += ds * dt P_eff = max(0.0, P - self.a - self.s) current = self.g_max * P_eff noise = self.sigma * np.sqrt(dt) * self.rng.normal() current += noise return max(0.0, current)
[docs] class VisualTransductionPipeline: """ Complete visual transduction: photoreceptor + bipolar cells. """ def __init__( self, photoreceptor_params: Optional[PhotoreceptorParams] = None, bipolar_tau: float = 0.010, bipolar_on_center: bool = True, ): self.photoreceptor = Photoreceptor(photoreceptor_params) self.bipolar = BipolarCell(tau_b=bipolar_tau, on_center=bipolar_on_center)
[docs] def step(self, dt: float, intensity: float) -> float: V = self.photoreceptor.step(dt, intensity) B = self.bipolar.step(dt, V) return B
[docs] class AuditoryTransductionPipeline: """ Hair cell transduction pipeline. """ def __init__(self, hair_cell_params: Optional[HairCellParams] = None): self.hair_cell = HairCell(hair_cell_params)
[docs] def step(self, dt: float, displacement: float) -> float: return self.hair_cell.step(dt, displacement)