Skip to content

Commit e09f906

Browse files
authored
Add IV curve plot (#3)
* add iv-curve * add unit test * add calculate_rheobase function in tools + unit tests
1 parent a4a3494 commit e09f906

File tree

6 files changed

+305
-1
lines changed

6 files changed

+305
-1
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/modlib
22
**/x86_64
3+
**/arm64
34
/.tox
45
*.pyc
56
*.egg-info
@@ -26,3 +27,4 @@ nrnivmodl.log
2627
*.btr
2728
*.whl
2829
.ipynb_checkpoints
30+
cADpyr_L2TPC_dendrogram.png

bluecellulab/analysis/analysis.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""Module for analyzing cell simulation results."""
2+
try:
3+
import efel
4+
except ImportError:
5+
efel = None
6+
import numpy as np
7+
8+
from bluecellulab.stimulus import StimulusFactory
9+
from bluecellulab.tools import calculate_rheobase
10+
from bluecellulab.analysis.inject_sequence import run_stimulus
11+
from bluecellulab.analysis.plotting import plot_iv_curve
12+
13+
14+
def compute_plot_iv_curve(cell, stim_start=100.0, duration=500.0, post_delay=100.0, threshold_voltage=-30, nb_bins=11):
15+
"""Compute and plot the IV curve from a given cell by injecting a
16+
predefined range of currents.
17+
18+
Args:
19+
cell (bluecellulab.cell.Cell): The initialized cell model.
20+
stim_start (float): Start time for current injection (in ms). Default is 100.0 ms.
21+
duration (float): Duration of current injection (in ms). Default is 500.0 ms.
22+
post_delay (float): Delay after the stimulation ends (in ms). Default is 100.0 ms.
23+
nb_bins (int): Number of current injection levels. Default is 11.
24+
25+
Returns:
26+
tuple: A tuple containing:
27+
- list_amp (np.ndarray): The predefined injected step current levels (nA).
28+
- steady_states (np.ndarray): The corresponding steady-state voltages (mV).
29+
"""
30+
rheobase = calculate_rheobase(cell)
31+
32+
list_amp = np.linspace(rheobase - 2, rheobase - 0.1, nb_bins) # [nA]
33+
34+
steps = []
35+
times = []
36+
voltages = []
37+
# inject step current and record voltage response
38+
for amp in list_amp:
39+
stim_factory = StimulusFactory(dt=0.1)
40+
step_stimulus = stim_factory.step(pre_delay=stim_start, duration=duration, post_delay=post_delay, amplitude=amp)
41+
recording = run_stimulus(cell.template_params, step_stimulus, section="soma[0]", segment=0.5)
42+
steps.append(step_stimulus)
43+
times.append(recording.time)
44+
voltages.append(recording.voltage)
45+
46+
steady_states = []
47+
# compute steady state response
48+
efel.set_setting('Threshold', threshold_voltage)
49+
for voltage, t in zip(voltages, times):
50+
trace = {
51+
'T': t,
52+
'V': voltage,
53+
'stim_start': [stim_start],
54+
'stim_end': [stim_start + duration]
55+
}
56+
features_results = efel.get_feature_values([trace], ['steady_state_voltage_stimend'])
57+
steady_state = features_results[0]['steady_state_voltage_stimend']
58+
steady_states.append(steady_state)
59+
60+
# plot I-V curve
61+
plot_iv_curve(list_amp, steady_states)
62+
63+
return np.array(list_amp), np.array(steady_states)

bluecellulab/analysis/plotting.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""Module for plotting analysis results of cell simulations."""
2+
3+
import matplotlib.pyplot as plt
4+
5+
6+
def plot_iv_curve(currents, voltages):
7+
"""Plots the IV curve.
8+
9+
Args:
10+
currents (iterable): The injected current levels (nA).
11+
voltages (iterable): The corresponding steady-state voltages (mV).
12+
Raises:
13+
ValueError: If the lengths of currents and voltages do not match.
14+
"""
15+
if len(currents) != len(voltages):
16+
raise ValueError("currents and voltages must have the same length")
17+
18+
# Plotting
19+
plt.figure(figsize=(10, 6))
20+
plt.plot(voltages, currents, marker='o', linestyle='-', color='b')
21+
plt.title("I-V Curve")
22+
plt.ylabel("Injected current [nA]")
23+
plt.xlabel("Steady state voltage [mV]")
24+
plt.tight_layout()
25+
plt.show()

bluecellulab/tools.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import numpy as np
2424

2525
import bluecellulab
26+
from bluecellulab.cell import Cell
2627
from bluecellulab.circuit.circuit_access import EmodelProperties
2728
from bluecellulab.exceptions import UnsteadyCellError
2829
from bluecellulab.simulation.parallel import IsolatedProcess
@@ -351,3 +352,76 @@ def check_empty_topology() -> bool:
351352
neuron.h.topology()
352353

353354
return stdout == ['', '']
355+
356+
357+
def calculate_max_thresh_current(cell: Cell, threshold_voltage: float = -30.0) -> float:
358+
"""Calculate the upper bound threshold current.
359+
360+
Args:
361+
cell (bluecellulab.cell.Cell): The initialized cell model.
362+
threshold_voltage (float, optional): Voltage threshold for spike detection. Default is -30.0 mV.
363+
364+
Returns:
365+
float: The upper bound threshold current.
366+
"""
367+
# Calculate resting membrane potential (rmp)
368+
rmp = calculate_SS_voltage(
369+
template_path=cell.template_params.template_filepath,
370+
morphology_path=cell.template_params.morph_filepath,
371+
template_format=cell.template_params.template_format,
372+
emodel_properties=cell.template_params.emodel_properties,
373+
step_level=0.0,
374+
)
375+
376+
# Calculate input resistance (rin)
377+
rin = calculate_input_resistance(
378+
template_path=cell.template_params.template_filepath,
379+
morphology_path=cell.template_params.morph_filepath,
380+
template_format=cell.template_params.template_format,
381+
emodel_properties=cell.template_params.emodel_properties,
382+
)
383+
384+
# Calculate upperbound threshold current
385+
upperbound_threshold_current = (threshold_voltage - rmp) / rin
386+
upperbound_threshold_current = np.min([upperbound_threshold_current, 2.0])
387+
388+
return upperbound_threshold_current
389+
390+
391+
def calculate_rheobase(cell: Cell,
392+
threshold_voltage: float = -30.0,
393+
threshold_search_stim_start: float = 300.0,
394+
threshold_search_stim_stop: float = 1000.0) -> float:
395+
"""Calculate the rheobase by first computing the upper bound threshold
396+
current.
397+
398+
Args:
399+
cell (bluecellulab.cell.Cell): The initialized cell model.
400+
threshold_voltage (float, optional): Voltage threshold for spike detection. Default is -30.0 mV.
401+
threshold_search_stim_start (float, optional): Start time for threshold search stimulation (in ms). Default is 300.0 ms.
402+
threshold_search_stim_stop (float, optional): Stop time for threshold search stimulation (in ms). Default is 1000.0 ms.
403+
404+
Returns:
405+
float: The rheobase current.
406+
"""
407+
if cell.template_params.emodel_properties is None:
408+
raise ValueError("emodel_properties cannot be None")
409+
410+
# Calculate upper bound threshold current
411+
upperbound_threshold_current = calculate_max_thresh_current(cell, threshold_voltage)
412+
413+
# Compute rheobase
414+
rheobase = search_threshold_current(
415+
template_name=cell.template_params.template_filepath,
416+
morphology_path=cell.template_params.morph_filepath,
417+
template_format=cell.template_params.template_format,
418+
emodel_properties=cell.template_params.emodel_properties,
419+
hyp_level=cell.template_params.emodel_properties.holding_current,
420+
inj_start=threshold_search_stim_start,
421+
inj_stop=threshold_search_stim_stop,
422+
min_current=cell.template_params.emodel_properties.holding_current,
423+
max_current=upperbound_threshold_current,
424+
current_precision=0.005,
425+
)
426+
427+
return rheobase

tests/test_analysis/test_analysis.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"""Unit tests for the analysis module."""
2+
3+
from unittest.mock import MagicMock, patch
4+
import numpy as np
5+
import pytest
6+
from bluecellulab.analysis.analysis import compute_plot_iv_curve
7+
from pathlib import Path
8+
from bluecellulab.cell import Cell
9+
from bluecellulab.circuit.circuit_access import EmodelProperties
10+
11+
12+
parent_dir = Path(__file__).resolve().parent.parent
13+
14+
15+
class MockRecording:
16+
def __init__(self):
17+
self.time = [1, 2, 3]
18+
self.voltage = [-70, -55, -40]
19+
20+
21+
@pytest.fixture
22+
def mock_run_stimulus():
23+
return MagicMock(return_value=MockRecording())
24+
25+
26+
@pytest.fixture
27+
def mock_search_threshold_current():
28+
return MagicMock(return_value=0.1)
29+
30+
31+
@pytest.fixture
32+
def mock_steady_state_voltage_stimend():
33+
return MagicMock(return_value=-65)
34+
35+
36+
@pytest.fixture
37+
def mock_efel():
38+
efel_mock = MagicMock()
39+
efel_mock.getFeatureValues.return_value = [{'steady_state_voltage_stimend': [-65]}]
40+
return efel_mock
41+
42+
43+
@pytest.fixture
44+
def mock_cell():
45+
emodel_properties = EmodelProperties(
46+
threshold_current=1.1433533430099487,
47+
holding_current=1.4146618843078613,
48+
AIS_scaler=1.4561502933502197,
49+
soma_scaler=1.0
50+
)
51+
cell = Cell(
52+
f"{parent_dir}/examples/circuit_sonata_quick_scx/components/hoc/cADpyr_L2TPC.hoc",
53+
f"{parent_dir}/examples/circuit_sonata_quick_scx/components/morphologies/asc/rr110330_C3_idA.asc",
54+
template_format="v6",
55+
emodel_properties=emodel_properties
56+
)
57+
return cell
58+
59+
60+
def test_plot_iv_curve(mock_cell, mock_run_stimulus, mock_search_threshold_current, mock_efel):
61+
"""Test the plot_iv_curve function."""
62+
with patch('bluecellulab.cell.core', mock_cell), \
63+
patch('bluecellulab.analysis.analysis.run_stimulus', mock_run_stimulus), \
64+
patch('bluecellulab.tools.search_threshold_current', mock_search_threshold_current), \
65+
patch('bluecellulab.analysis.analysis.efel', mock_efel):
66+
67+
stim_start = 100.0
68+
duration = 500.0
69+
post_delay = 100.0
70+
threshold_voltage = -30
71+
nb_bins = 11
72+
73+
list_amp, steady_states = compute_plot_iv_curve(mock_cell, stim_start, duration, post_delay, threshold_voltage, nb_bins)
74+
75+
assert isinstance(list_amp, np.ndarray)
76+
assert isinstance(steady_states, np.ndarray)
77+
assert len(list_amp) == nb_bins
78+
assert len(steady_states) == nb_bins

tests/test_tools.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,52 @@
1717

1818
import numpy as np
1919
import pytest
20+
from unittest.mock import MagicMock, patch
2021

22+
from bluecellulab.cell import Cell
2123
from bluecellulab import CircuitSimulation
2224
from bluecellulab.cell.ballstick import create_ball_stick
2325
from bluecellulab.circuit.circuit_access import EmodelProperties
2426
from bluecellulab.circuit.node_id import create_cell_id
2527
from bluecellulab.exceptions import UnsteadyCellError
26-
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
28+
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
29+
2730

2831
script_dir = Path(__file__).parent
2932

3033

34+
@pytest.fixture
35+
def mock_cell():
36+
emodel_properties = EmodelProperties(
37+
threshold_current=1.1433533430099487,
38+
holding_current=1.4146618843078613,
39+
AIS_scaler=1.4561502933502197,
40+
soma_scaler=1.0
41+
)
42+
cell = Cell(
43+
f"{script_dir}/examples/circuit_sonata_quick_scx/components/hoc/cADpyr_L2TPC.hoc",
44+
f"{script_dir}/examples/circuit_sonata_quick_scx/components/morphologies/asc/rr110330_C3_idA.asc",
45+
template_format="v6",
46+
emodel_properties=emodel_properties
47+
)
48+
return cell
49+
50+
51+
@pytest.fixture
52+
def mock_calculate_SS_voltage():
53+
return MagicMock(return_value=-65.0)
54+
55+
56+
@pytest.fixture
57+
def mock_calculate_input_resistance():
58+
return MagicMock(return_value=100.0)
59+
60+
61+
@pytest.fixture
62+
def mock_search_threshold_current():
63+
return MagicMock(return_value=0.1)
64+
65+
3166
@pytest.mark.v5
3267
def test_calculate_SS_voltage_subprocess():
3368
"""Tools: Test calculate_SS_voltage"""
@@ -241,3 +276,30 @@ def test_check_empty_topology():
241276
assert check_empty_topology() is True
242277
cell = create_ball_stick()
243278
assert check_empty_topology() is False
279+
280+
281+
def test_calculate_max_thresh_current(mock_cell, mock_calculate_SS_voltage, mock_calculate_input_resistance):
282+
"""Test the calculate_max_thresh_current function."""
283+
with patch('bluecellulab.tools.calculate_SS_voltage', mock_calculate_SS_voltage), \
284+
patch('bluecellulab.tools.calculate_input_resistance', mock_calculate_input_resistance):
285+
286+
threshold_voltage = -30.0
287+
upperbound_threshold_current = calculate_max_thresh_current(mock_cell, threshold_voltage)
288+
289+
assert upperbound_threshold_current == (threshold_voltage - mock_calculate_SS_voltage.return_value) / mock_calculate_input_resistance.return_value
290+
assert upperbound_threshold_current == 0.35
291+
292+
293+
def test_calculate_rheobase(mock_cell, mock_calculate_SS_voltage, mock_calculate_input_resistance, mock_search_threshold_current):
294+
"""Test the calculate_rheobase function."""
295+
with patch('bluecellulab.tools.calculate_SS_voltage', mock_calculate_SS_voltage), \
296+
patch('bluecellulab.tools.calculate_input_resistance', mock_calculate_input_resistance), \
297+
patch('bluecellulab.tools.search_threshold_current', mock_search_threshold_current):
298+
299+
threshold_voltage = -30.0
300+
threshold_search_stim_start = 300.0
301+
threshold_search_stim_stop = 1000.0
302+
303+
rheobase = calculate_rheobase(mock_cell, threshold_voltage, threshold_search_stim_start, threshold_search_stim_stop)
304+
305+
assert rheobase == mock_search_threshold_current.return_value

0 commit comments

Comments
 (0)