Skip to content

Commit

Permalink
Add IV curve plot (#3)
Browse files Browse the repository at this point in the history
* add iv-curve

* add unit test

* add calculate_rheobase function in tools + unit tests
  • Loading branch information
ilkilic authored Jan 16, 2025
1 parent a4a3494 commit e09f906
Show file tree
Hide file tree
Showing 6 changed files with 305 additions and 1 deletion.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/modlib
**/x86_64
**/arm64
/.tox
*.pyc
*.egg-info
Expand All @@ -26,3 +27,4 @@ nrnivmodl.log
*.btr
*.whl
.ipynb_checkpoints
cADpyr_L2TPC_dendrogram.png
63 changes: 63 additions & 0 deletions bluecellulab/analysis/analysis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Module for analyzing cell simulation results."""
try:
import efel
except ImportError:
efel = None
import numpy as np

from bluecellulab.stimulus import StimulusFactory
from bluecellulab.tools import calculate_rheobase
from bluecellulab.analysis.inject_sequence import run_stimulus
from bluecellulab.analysis.plotting import plot_iv_curve


def compute_plot_iv_curve(cell, stim_start=100.0, duration=500.0, post_delay=100.0, threshold_voltage=-30, nb_bins=11):
"""Compute and plot the IV curve from a given cell by injecting a
predefined range of currents.
Args:
cell (bluecellulab.cell.Cell): The initialized cell model.
stim_start (float): Start time for current injection (in ms). Default is 100.0 ms.
duration (float): Duration of current injection (in ms). Default is 500.0 ms.
post_delay (float): Delay after the stimulation ends (in ms). Default is 100.0 ms.
nb_bins (int): Number of current injection levels. Default is 11.
Returns:
tuple: A tuple containing:
- list_amp (np.ndarray): The predefined injected step current levels (nA).
- steady_states (np.ndarray): The corresponding steady-state voltages (mV).
"""
rheobase = calculate_rheobase(cell)

list_amp = np.linspace(rheobase - 2, rheobase - 0.1, nb_bins) # [nA]

steps = []
times = []
voltages = []
# inject step current and record voltage response
for amp in list_amp:
stim_factory = StimulusFactory(dt=0.1)
step_stimulus = stim_factory.step(pre_delay=stim_start, duration=duration, post_delay=post_delay, amplitude=amp)
recording = run_stimulus(cell.template_params, step_stimulus, section="soma[0]", segment=0.5)
steps.append(step_stimulus)
times.append(recording.time)
voltages.append(recording.voltage)

steady_states = []
# compute steady state response
efel.set_setting('Threshold', threshold_voltage)
for voltage, t in zip(voltages, times):
trace = {
'T': t,
'V': voltage,
'stim_start': [stim_start],
'stim_end': [stim_start + duration]
}
features_results = efel.get_feature_values([trace], ['steady_state_voltage_stimend'])
steady_state = features_results[0]['steady_state_voltage_stimend']
steady_states.append(steady_state)

# plot I-V curve
plot_iv_curve(list_amp, steady_states)

return np.array(list_amp), np.array(steady_states)
25 changes: 25 additions & 0 deletions bluecellulab/analysis/plotting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""Module for plotting analysis results of cell simulations."""

import matplotlib.pyplot as plt


def plot_iv_curve(currents, voltages):
"""Plots the IV curve.
Args:
currents (iterable): The injected current levels (nA).
voltages (iterable): The corresponding steady-state voltages (mV).
Raises:
ValueError: If the lengths of currents and voltages do not match.
"""
if len(currents) != len(voltages):
raise ValueError("currents and voltages must have the same length")

# Plotting
plt.figure(figsize=(10, 6))
plt.plot(voltages, currents, marker='o', linestyle='-', color='b')
plt.title("I-V Curve")
plt.ylabel("Injected current [nA]")
plt.xlabel("Steady state voltage [mV]")
plt.tight_layout()
plt.show()
74 changes: 74 additions & 0 deletions bluecellulab/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import numpy as np

import bluecellulab
from bluecellulab.cell import Cell
from bluecellulab.circuit.circuit_access import EmodelProperties
from bluecellulab.exceptions import UnsteadyCellError
from bluecellulab.simulation.parallel import IsolatedProcess
Expand Down Expand Up @@ -351,3 +352,76 @@ def check_empty_topology() -> bool:
neuron.h.topology()

return stdout == ['', '']


def calculate_max_thresh_current(cell: Cell, threshold_voltage: float = -30.0) -> float:
"""Calculate the upper bound threshold current.
Args:
cell (bluecellulab.cell.Cell): The initialized cell model.
threshold_voltage (float, optional): Voltage threshold for spike detection. Default is -30.0 mV.
Returns:
float: The upper bound threshold current.
"""
# Calculate resting membrane potential (rmp)
rmp = calculate_SS_voltage(
template_path=cell.template_params.template_filepath,
morphology_path=cell.template_params.morph_filepath,
template_format=cell.template_params.template_format,
emodel_properties=cell.template_params.emodel_properties,
step_level=0.0,
)

# Calculate input resistance (rin)
rin = calculate_input_resistance(
template_path=cell.template_params.template_filepath,
morphology_path=cell.template_params.morph_filepath,
template_format=cell.template_params.template_format,
emodel_properties=cell.template_params.emodel_properties,
)

# Calculate upperbound threshold current
upperbound_threshold_current = (threshold_voltage - rmp) / rin
upperbound_threshold_current = np.min([upperbound_threshold_current, 2.0])

return upperbound_threshold_current


def calculate_rheobase(cell: Cell,
threshold_voltage: float = -30.0,
threshold_search_stim_start: float = 300.0,
threshold_search_stim_stop: float = 1000.0) -> float:
"""Calculate the rheobase by first computing the upper bound threshold
current.
Args:
cell (bluecellulab.cell.Cell): The initialized cell model.
threshold_voltage (float, optional): Voltage threshold for spike detection. Default is -30.0 mV.
threshold_search_stim_start (float, optional): Start time for threshold search stimulation (in ms). Default is 300.0 ms.
threshold_search_stim_stop (float, optional): Stop time for threshold search stimulation (in ms). Default is 1000.0 ms.
Returns:
float: The rheobase current.
"""
if cell.template_params.emodel_properties is None:
raise ValueError("emodel_properties cannot be None")

# Calculate upper bound threshold current
upperbound_threshold_current = calculate_max_thresh_current(cell, threshold_voltage)

# Compute rheobase
rheobase = search_threshold_current(
template_name=cell.template_params.template_filepath,
morphology_path=cell.template_params.morph_filepath,
template_format=cell.template_params.template_format,
emodel_properties=cell.template_params.emodel_properties,
hyp_level=cell.template_params.emodel_properties.holding_current,
inj_start=threshold_search_stim_start,
inj_stop=threshold_search_stim_stop,
min_current=cell.template_params.emodel_properties.holding_current,
max_current=upperbound_threshold_current,
current_precision=0.005,
)

return rheobase
78 changes: 78 additions & 0 deletions tests/test_analysis/test_analysis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""Unit tests for the analysis module."""

from unittest.mock import MagicMock, patch
import numpy as np
import pytest
from bluecellulab.analysis.analysis import compute_plot_iv_curve
from pathlib import Path
from bluecellulab.cell import Cell
from bluecellulab.circuit.circuit_access import EmodelProperties


parent_dir = Path(__file__).resolve().parent.parent


class MockRecording:
def __init__(self):
self.time = [1, 2, 3]
self.voltage = [-70, -55, -40]


@pytest.fixture
def mock_run_stimulus():
return MagicMock(return_value=MockRecording())


@pytest.fixture
def mock_search_threshold_current():
return MagicMock(return_value=0.1)


@pytest.fixture
def mock_steady_state_voltage_stimend():
return MagicMock(return_value=-65)


@pytest.fixture
def mock_efel():
efel_mock = MagicMock()
efel_mock.getFeatureValues.return_value = [{'steady_state_voltage_stimend': [-65]}]
return efel_mock


@pytest.fixture
def mock_cell():
emodel_properties = EmodelProperties(
threshold_current=1.1433533430099487,
holding_current=1.4146618843078613,
AIS_scaler=1.4561502933502197,
soma_scaler=1.0
)
cell = Cell(
f"{parent_dir}/examples/circuit_sonata_quick_scx/components/hoc/cADpyr_L2TPC.hoc",
f"{parent_dir}/examples/circuit_sonata_quick_scx/components/morphologies/asc/rr110330_C3_idA.asc",
template_format="v6",
emodel_properties=emodel_properties
)
return cell


def test_plot_iv_curve(mock_cell, mock_run_stimulus, mock_search_threshold_current, mock_efel):
"""Test the plot_iv_curve function."""
with patch('bluecellulab.cell.core', mock_cell), \
patch('bluecellulab.analysis.analysis.run_stimulus', mock_run_stimulus), \
patch('bluecellulab.tools.search_threshold_current', mock_search_threshold_current), \
patch('bluecellulab.analysis.analysis.efel', mock_efel):

stim_start = 100.0
duration = 500.0
post_delay = 100.0
threshold_voltage = -30
nb_bins = 11

list_amp, steady_states = compute_plot_iv_curve(mock_cell, stim_start, duration, post_delay, threshold_voltage, nb_bins)

assert isinstance(list_amp, np.ndarray)
assert isinstance(steady_states, np.ndarray)
assert len(list_amp) == nb_bins
assert len(steady_states) == nb_bins
64 changes: 63 additions & 1 deletion tests/test_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,52 @@

import numpy as np
import pytest
from unittest.mock import MagicMock, patch

from bluecellulab.cell import Cell
from bluecellulab import CircuitSimulation
from bluecellulab.cell.ballstick import create_ball_stick
from bluecellulab.circuit.circuit_access import EmodelProperties
from bluecellulab.circuit.node_id import create_cell_id
from bluecellulab.exceptions import UnsteadyCellError
from bluecellulab.tools import calculate_SS_voltage, calculate_SS_voltage_subprocess, calculate_input_resistance, detect_hyp_current, detect_spike, detect_spike_step, detect_spike_step_subprocess, holding_current, holding_current_subprocess, search_threshold_current, template_accepts_cvode, check_empty_topology
from bluecellulab.tools import calculate_SS_voltage, calculate_SS_voltage_subprocess, calculate_input_resistance, detect_hyp_current, detect_spike, detect_spike_step, detect_spike_step_subprocess, holding_current, holding_current_subprocess, search_threshold_current, template_accepts_cvode, check_empty_topology, calculate_max_thresh_current, calculate_rheobase


script_dir = Path(__file__).parent


@pytest.fixture
def mock_cell():
emodel_properties = EmodelProperties(
threshold_current=1.1433533430099487,
holding_current=1.4146618843078613,
AIS_scaler=1.4561502933502197,
soma_scaler=1.0
)
cell = Cell(
f"{script_dir}/examples/circuit_sonata_quick_scx/components/hoc/cADpyr_L2TPC.hoc",
f"{script_dir}/examples/circuit_sonata_quick_scx/components/morphologies/asc/rr110330_C3_idA.asc",
template_format="v6",
emodel_properties=emodel_properties
)
return cell


@pytest.fixture
def mock_calculate_SS_voltage():
return MagicMock(return_value=-65.0)


@pytest.fixture
def mock_calculate_input_resistance():
return MagicMock(return_value=100.0)


@pytest.fixture
def mock_search_threshold_current():
return MagicMock(return_value=0.1)


@pytest.mark.v5
def test_calculate_SS_voltage_subprocess():
"""Tools: Test calculate_SS_voltage"""
Expand Down Expand Up @@ -241,3 +276,30 @@ def test_check_empty_topology():
assert check_empty_topology() is True
cell = create_ball_stick()
assert check_empty_topology() is False


def test_calculate_max_thresh_current(mock_cell, mock_calculate_SS_voltage, mock_calculate_input_resistance):
"""Test the calculate_max_thresh_current function."""
with patch('bluecellulab.tools.calculate_SS_voltage', mock_calculate_SS_voltage), \
patch('bluecellulab.tools.calculate_input_resistance', mock_calculate_input_resistance):

threshold_voltage = -30.0
upperbound_threshold_current = calculate_max_thresh_current(mock_cell, threshold_voltage)

assert upperbound_threshold_current == (threshold_voltage - mock_calculate_SS_voltage.return_value) / mock_calculate_input_resistance.return_value
assert upperbound_threshold_current == 0.35


def test_calculate_rheobase(mock_cell, mock_calculate_SS_voltage, mock_calculate_input_resistance, mock_search_threshold_current):
"""Test the calculate_rheobase function."""
with patch('bluecellulab.tools.calculate_SS_voltage', mock_calculate_SS_voltage), \
patch('bluecellulab.tools.calculate_input_resistance', mock_calculate_input_resistance), \
patch('bluecellulab.tools.search_threshold_current', mock_search_threshold_current):

threshold_voltage = -30.0
threshold_search_stim_start = 300.0
threshold_search_stim_stop = 1000.0

rheobase = calculate_rheobase(mock_cell, threshold_voltage, threshold_search_stim_start, threshold_search_stim_stop)

assert rheobase == mock_search_threshold_current.return_value

0 comments on commit e09f906

Please sign in to comment.