Skip to content

Adding thumbnail generating function #19

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,6 @@ nrnivmodl.log
*.btr
*.whl
.ipynb_checkpoints
cADpyr_L2TPC_dendrogram.png
cADpyr_L2TPC_dendrogram.png
temp/
venv/
134 changes: 134 additions & 0 deletions bluecellulab/analysis/plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,140 @@

import matplotlib.pyplot as plt
import pathlib
import numpy as np
from typing import Optional, Tuple
from bluecellulab.tools import calculate_rheobase
from bluecellulab.circuit.circuit_access import EmodelProperties
from bluecellulab.cell import Cell
from bluecellulab.simulation.neuron_globals import NeuronGlobals
from bluecellulab import Simulation


def generate_cell_thumbnail(
template_path: str,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you reduce the number of function arguments by passing template_params instead?
See https://github.com/openbraininstitute/BlueCelluLab/blob/main/bluecellulab/cell/template.py#L48

morphology_path: Optional[str] = None,
threshold_current: Optional[float] = None,
holding_current: Optional[float] = None,
template_format: str = "v6",
save_figure: bool = True,
output_path: str = "cell_thumbnail.png",
show_figure: bool = False,
v_init: Optional[float] = -80.0,
celsius: Optional[float] = 34.0,
) -> Tuple[np.ndarray, np.ndarray]:
"""Generate a thumbnail image of a Cell object's step response.

Args:
template_path: Path to the hoc file.
morphology_path: Path to the morphology file. Default is None.
threshold_current: Threshold current for the cell. Default is None.
holding_current: Holding current for the cell. Default is None.
template_format: Template format. Default is "v6".
save_figure: Whether to save the figure. Default is True.
output_path: Path where to save the thumbnail image.
show_figure: Whether to display the figure. Default is False.
v_init: Initial membrane potential. Default is -80.0 mV.
celsius: Temperature in Celsius. Default is 34.0.

Returns:
Tuple containing (time, voltage) arrays of the simulation.
"""
# If the threshold and holding_current is not known,
# we use calculate_rheobase() to find the threshold_current.
# For this case, we set the holding_current to 0.0 nA.
# if threshold_current is None:
threshold_current = 1.0
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you overwriting this value?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The threshold current calculation should be done every time as the morphology of the me-combination will change and it won't be emodel threshold to be sure we are not using the emodel threshold. The value was overwritten just to initialise of the cell.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can be called after we compute the threshold, and we just get it from MEModelCalibration. So we can remove the threshold computation

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. I will not overwrite the threshold and holding currents. I will pass the emodel_properties instead.

if holding_current is None:
holding_current = 0.0

emodel_properties = EmodelProperties(
threshold_current=threshold_current,
holding_current=holding_current
)

# Initialise cell
thumbnail_cell = Cell(
template_path=template_path,
morphology_path=morphology_path,
template_format=template_format,
emodel_properties=emodel_properties,
)
holding_current = 0.0
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here also, you are overwriting holding current and computing rheobase even if we have passed those values to the function

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same but for holding current.

# Calculate rheobase
newthreshold_current = calculate_rheobase(
thumbnail_cell,
section="soma[0]",
segx=0.5
)
print(f"Threshold current: {newthreshold_current}")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use logger.info instead of print

print(f"Holding current: {holding_current}")
#delete thumbnail_cell
thumbnail_cell.delete()

# Update emodel properties
emodel_properties = EmodelProperties(
threshold_current=newthreshold_current,
holding_current=holding_current
)

# Create cell
thumbnail_cell = Cell(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you don't need to re-create the cell

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you don't recreate the cell, the thumbnail generation step protocol would persist even after the thumbnail was generated. I also delete the cell at the end.

template_path=template_path,
morphology_path=morphology_path,
template_format=template_format,
emodel_properties=emodel_properties,
)

# Add 130% threshold current injection
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment is not consistent with code

step_amplitude = newthreshold_current * 1.1
step_duration = 2000
start_time = 700
thumbnail_cell.add_step(
start_time=start_time,
stop_time=start_time+step_duration,
level=step_amplitude
)

# Set up simulation
sim = thumbnail_cell.simulation if hasattr(thumbnail_cell, 'simulation') else None
if sim is None:
sim = Simulation()
sim.add_cell(thumbnail_cell)

neuron_globals = NeuronGlobals.get_instance()
# neuron_globals.temperature = celsius
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are those commented out?

# neuron_globals.v_init = v_init

# Run simulation
tstop = step_duration+300
sim.run(tstop)
# Get recording data
time = thumbnail_cell.get_time()
voltage = thumbnail_cell.get_soma_voltage()

# Create figure with default size (matplotlib's default is [6.4, 4.8])
fig, ax = plt.subplots(figsize=(6.4, 4.8))
ax.plot(time, voltage, 'k-', linewidth=1.5)
ax.set_xlabel('Time (ms)', fontsize=10)
ax.set_ylabel('Membrane potential (mV)', fontsize=10)
plt.tight_layout()

if save_figure:
# Create parent directories if they don't exist
output_path = pathlib.Path(output_path)
output_path.parent.mkdir(parents=True, exist_ok=True)

# Save figure with 300 DPI
plt.savefig(output_path, dpi=300, bbox_inches='tight')

if show_figure:
plt.show()
else:
plt.close()

thumbnail_cell.delete()

return time, voltage


def plot_iv_curve(
Expand Down
740 changes: 440 additions & 300 deletions examples/1-singlecell/singlecell.ipynb

Large diffs are not rendered by default.

140 changes: 140 additions & 0 deletions tests/test_analysis_plotting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
"""Tests for analysis plotting functions."""
import tempfile
from pathlib import Path
from typing import Optional, Tuple
import pytest
import numpy as np
from bluecellulab.analysis.plotting import generate_cell_thumbnail
from bluecellulab.circuit.circuit_access.definition import EmodelProperties

# Test data paths
TEST_DATA_DIR = (Path(__file__).parent / "examples"
/ "circuit_sonata_quick_scx" / "components")
HOC_FILE = TEST_DATA_DIR / "hoc" / "cADpyr_L2TPC.hoc"
MORPH_FILE = TEST_DATA_DIR / "morphologies" / "asc" / "rr110330_C3_idA.asc"

# Fixtures
@pytest.fixture(scope="module")
def emodel_properties() -> EmodelProperties:
"""Fixture providing default emodel properties for testing."""
return EmodelProperties(threshold_current=1.1433533430099487,
holding_current=1.4146618843078613,
AIS_scaler=1.4561502933502197,
soma_scaler=1.0)

@pytest.fixture
def output_dir():
"""Fixture providing a temporary directory for test outputs."""
with tempfile.TemporaryDirectory() as temp_dir:
yield Path(temp_dir)

class TestGenerateCellThumbnail:
"""Test cases for generate_cell_thumbnail function."""

def test_with_explicit_output_path(self, output_dir: Path, emodel_properties: EmodelProperties):
"""Test thumbnail generation with explicit output path."""
# Skip test if required files don't exist
if not HOC_FILE.exists() or not MORPH_FILE.exists():
pytest.skip("Required test files not found")

# Arrange
output_path = output_dir / "cell_thumbnail.png"

# Act
time, voltage = generate_cell_thumbnail(
template_path=str(HOC_FILE),
morphology_path=str(MORPH_FILE),
template_format="v6",
emodel_properties=emodel_properties,
output_path=output_path
)

# Assert
assert output_path.exists(), "Thumbnail file was not created"
assert output_path.stat().st_size > 0, "Thumbnail file is empty"
assert len(time) == len(voltage) > 0, "Time and voltage arrays should have matching lengths"
assert isinstance(time, np.ndarray), "Time should be a numpy array"
assert isinstance(voltage, np.ndarray), "Voltage should be a numpy array"

def test_with_default_output_path(self, tmp_path, emodel_properties: EmodelProperties):
"""Test thumbnail generation with default output path."""
# Skip test if required files don't exist
if not HOC_FILE.exists() or not MORPH_FILE.exists():
pytest.skip("Required test files not found")

# Arrange
default_path = Path("cell_thumbnail.png")

try:
# Act
time, voltage = generate_cell_thumbnail(
template_path=str(HOC_FILE),
morphology_path=str(MORPH_FILE),
template_format="v6",
emodel_properties=emodel_properties
)

# Assert
assert default_path.exists(), "Default thumbnail file was not created"
assert default_path.stat().st_size > 0, "Default thumbnail file is empty"
assert len(time) > 0 and len(voltage) > 0, "Output arrays should not be empty"
finally:
# Cleanup
if default_path.exists():
default_path.unlink()

@pytest.mark.parametrize("threshold_value", [0, None])
def test_without_threshold(self, output_dir: Path, emodel_properties: EmodelProperties, threshold_value: Optional[float]):
"""Test thumbnail generation when threshold needs to be calculated."""
# Skip test if required files don't exist
if not HOC_FILE.exists() or not MORPH_FILE.exists():
pytest.skip("Required test files not found")

# Arrange
output_path = output_dir / "cell_no_threshold.png"

# Create a copy of emodel_properties with no threshold
test_props = EmodelProperties(
threshold_current=0.0 if threshold_value == 0 else 0.1, # Will trigger calculate_rheobase
holding_current=emodel_properties.holding_current,
AIS_scaler=emodel_properties.AIS_scaler,
soma_scaler=emodel_properties.soma_scaler
)

# Act
time, voltage = generate_cell_thumbnail(
template_path=str(HOC_FILE),
morphology_path=str(MORPH_FILE),
template_format="v6",
emodel_properties=test_props,
output_path=output_path
)

# Assert
assert output_path.exists(), "Thumbnail file was not created"
assert output_path.stat().st_size > 0, "Thumbnail file is empty"
assert len(time) > 0 and len(voltage) > 0, "Output arrays should not be empty"

@pytest.mark.parametrize("show_figure", [True, False])
def test_show_figure_parameter(self, output_dir: Path, emodel_properties: EmodelProperties, show_figure: bool):
"""Test that show_figure parameter works as expected."""
# Skip test if required files don't exist
if not HOC_FILE.exists() or not MORPH_FILE.exists():
pytest.skip("Required test files not found")

# Arrange
output_path = output_dir / f"show_figure_{show_figure}.png"

# Act
time, voltage = generate_cell_thumbnail(
template_path=str(HOC_FILE),
morphology_path=str(MORPH_FILE),
template_format="v6",
emodel_properties=emodel_properties,
output_path=output_path,
show_figure=show_figure
)

# Assert
assert output_path.exists(), "Thumbnail file was not created"
assert output_path.stat().st_size > 0, "Thumbnail file is empty"
Loading