"""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)