Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
626e54f
wip
ilkilic Jan 8, 2026
bd23ea4
Merge branch 'main' into point-process
jamesgking Mar 11, 2026
17195f0
Merge remote-tracking branch 'origin/main' into point-process
jamesgking Mar 23, 2026
3a323c0
Introduce support to detect edges for allen circuits
jamesgking Mar 24, 2026
988664b
Minor fixes to pass lint checks
jamesgking Mar 26, 2026
c9f0db8
Merge remote-tracking branch 'origin/main' into point-process
jamesgking Apr 15, 2026
e3b12e9
Intermediate steps to allow point process cells (wrapped in hoc) to b…
jamesgking Apr 22, 2026
7adda77
Corrections to load synapse data correctly from sonata edge files for…
jamesgking Apr 24, 2026
c203180
Introduce test for allen point neurons
jamesgking Apr 24, 2026
3185fbb
Fix lint
jamesgking Apr 24, 2026
63a1b59
To test for allen type edges, instead look for limited count of prope…
jamesgking May 1, 2026
dc95223
minor: limit use of docformatter to before 1.7.8 until it has bug fixed
jamesgking May 1, 2026
8249daa
Instantiate allen biophysical cells in another test, adding support data
jamesgking May 3, 2026
cba681e
Forgot updated files in previous commit
jamesgking May 3, 2026
708b303
Improved allen test instantiation
jamesgking May 3, 2026
45b18aa
Removed prototype code ultimately not used for allen use case
jamesgking May 3, 2026
e74a00b
Involve NEURON ParallelContext when testing point cells to trigger mo…
jamesgking May 3, 2026
f0264f2
Corrected SynapseType to indicate ALLEN_CHEMICAL and removed ALLEN_POINT
jamesgking May 3, 2026
7b1551e
Merge branch 'main' into point-process
WeinaJi Jun 18, 2026
b6f22e4
Address code review comments
jamesgking Jun 29, 2026
83d7a94
Add more Cell members to BasePointProcessCell and handle weight better
jamesgking Jun 30, 2026
1ae6781
fixes for tests to all point cells to use info_dict
jamesgking Jun 30, 2026
131e853
More safety with point_connection and syn_description
jamesgking Jun 30, 2026
731777e
Fix for replay when duration is used; address clean up requests
jamesgking Jun 30, 2026
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
2 changes: 1 addition & 1 deletion bluecellulab/cell/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -868,7 +868,7 @@ def add_synapse_replay(
spikes_of_interest = synapse_spikes[pre_cell_id]
spikes_of_interest = spikes_of_interest[
(spikes_of_interest >= stimulus.delay)
& (spikes_of_interest <= stimulus.duration)
& (spikes_of_interest <= stimulus.delay + stimulus.duration)
]

connection = bluecellulab.Connection(
Expand Down
242 changes: 242 additions & 0 deletions bluecellulab/cell/point_process.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
from __future__ import annotations

import logging
from pathlib import Path
import queue
from typing import Optional

import bluecellulab
from bluecellulab.cell import Cell
from bluecellulab.circuit.simulation_access import get_synapse_replay_spikes
from bluecellulab.exceptions import BluecellulabError
from bluecellulab.circuit import SynapseProperty
from bluecellulab.psection import PSection
from bluecellulab.type_aliases import HocObjectType

from neuron import h
import numpy as np

from bluecellulab.circuit.node_id import CellId
from bluecellulab.synapse.synapse_types import SynapseID
from bluecellulab.point.point_connection import PointProcessConnection
from bluecellulab.point.connection_params import PointProcessConnParameters

logger = logging.getLogger(__name__)


class BasePointProcessCell(Cell):
Comment thread
darshanmandge marked this conversation as resolved.
"""Base class for NEURON artificial point processes (IntFire1/2/...)."""

def __init__(self, cell_id: Optional[CellId]) -> None:

if cell_id is None:
raise ValueError("PointProcessCell requires valid cell_id")
self.cell_id = cell_id

self._spike_times = h.Vector()
self._spike_detector: Optional[h.NetCon] = None
self.pointcell = None # type: ignore[assignment]
self.synapses = {}
self.connections: dict[SynapseID, bluecellulab.Connection] = {}

self._replay_vecs: list[h.Vector] = []
self._replay_vecstims: list[h.VecStim] = []
self._replay_netcons: list[h.NetCon] = []

# TODO: some members used in base class Cell are init to None, empty; to refactor
self.soma = None

self.recordings = {}
self.report_sites: dict[str, list[dict]] = {}

self.post_gid = None
self.ips = {}
self.syn_mini_netcons = {}
self.hocname = None
self.record_dt = None
self.delayed_weights = queue.PriorityQueue()
self.psections: dict[int, PSection] = {}
self.secname_to_psection: dict[str, PSection] = {}
self.is_made_passive = False
self.sonata_proxy = None
self.persistent: list[HocObjectType] = []
self.hypamp = 0.0
self.threshold = 0.0

@property
def hoc_cell(self):
return self.pointcell

def init_callbacks(self):
pass

def connect_to_circuit(self, proxy) -> None:
self._circuit_proxy = proxy

def delete(self) -> None:
# Stop recording
if self._spike_detector is not None:
# NetCon will be GC'd when no Python refs remain
self._spike_detector = None
if self._spike_times is not None:
self._spike_times = None

# Drop pointer to underlying NEURON object
self.pointcell = None

def get_spike_times(self) -> list[float]:
return list(self._spike_times)

def create_netcon_spikedetector(
self,
sec, # ignored for artificial cells
location=None, # ignored for artificial cells
threshold: float = 0.0,
) -> h.NetCon:
if self.pointcell is None:
raise ValueError("attempting to create netcon without valid pointprocess")
nc = h.NetCon(self.pointcell.pointcell, None)
nc.threshold = threshold # harmless for artificial cells
return nc

def is_recording_spikes(self, location=None, threshold: float | None = None) -> bool:
return self._spike_detector is not None

def start_recording_spikes(self, sec, location=None, threshold: float = 0.0) -> None:
if self._spike_detector is not None:
return
if self.pointcell is None:
raise ValueError("attempting to record spikes without valid pointprocess")
self._spike_times = h.Vector()
self._spike_detector = h.NetCon(self.pointcell.pointcell, None)
self._spike_detector.threshold = threshold # not used by artificial cells e.g. IntFire1
self._spike_detector.record(self._spike_times)

def get_recorded_spikes(self, location="pointcell", threshold=-20):
return self._spike_times


class HocPointProcessCell(BasePointProcessCell):
"""Point process that wraps an arbitrary HOC/mod artificial mechanism."""

def __init__(
self,
cell_id: Optional[CellId],
mechanism_name: str,
spike_threshold: float = 0.0,
) -> None:
super().__init__(cell_id)

try:
mech_cls = getattr(h, mechanism_name)
except AttributeError as exc:
raise BluecellulabError(
f"Point mechanism '{mechanism_name}' not found in NEURON. "
"Make sure the mod/hoc files are compiled and loaded."
) from exc

if cell_id is None:
raise ValueError("call to create pointprocess mechanism without valid cell_id")
point = mech_cls(cell_id.id)

self.pointcell = point
self.start_recording_spikes(None, None, threshold=spike_threshold)

def add_synapse_replay(self, stimulus, spike_threshold: float, spike_location: str) -> None:
"""SONATA-style spike replay for point processes.

This is a simplified analogue of Cell.add_synapse_replay, but
instead of mapping spikes to individual synapses, we directly
connect each presynaptic node_id's spike train to this
artificial cell via VecStim → NetCon.
"""
file_path = Path(stimulus.spike_file).expanduser()

if not file_path.is_absolute():
config_dir = stimulus.config_dir
if config_dir is not None:
file_path = Path(config_dir) / file_path

file_path = file_path.resolve()

if not file_path.exists():
raise FileNotFoundError(f"Spike file not found: {str(file_path)}")

synapse_spikes = get_synapse_replay_spikes(str(file_path))

if self.pointcell is None:
raise ValueError("attempting to add replay spikes without valid pointprocess")

for synapse_id, synapse in self.synapses.items():
pre_cell_id = CellId(
str(synapse.syn_description["source_population_name"]),
int(synapse.syn_description[SynapseProperty.PRE_GID]),
)

if pre_cell_id not in synapse_spikes:
continue

spikes_of_interest = synapse_spikes[pre_cell_id]
delay = getattr(stimulus, "delay", 0.0) or 0.0
duration = getattr(stimulus, "duration", np.inf)

spikes_of_interest = spikes_of_interest[
(spikes_of_interest >= delay) & (spikes_of_interest <= delay + duration)
]

if spikes_of_interest.size == 0:
continue

vec = h.Vector(spikes_of_interest)
vs = h.VecStim()
vs.play(vec)

nc = h.NetCon(vs, self.pointcell.pointcell)
# Use stimulus weight if available, otherwise default to 1.0
weight = getattr(stimulus, "weight", 1.0)
nc.weight[0] = weight
nc.delay = 0.0 # delay already baked into spike times

self._replay_vecs.append(vec)
self._replay_vecstims.append(vs)
self._replay_netcons.append(nc)

logger.debug(
f"Added replay connection from pre_node_id={pre_cell_id} "
f"to point neuron {self.cell_id}"
)

def add_replay_synapse(self, syn_id, syn_description, syn_connection_parameters, condition_parameters,
popids, extracellular_calcium):
"""For Point Neurons, the replay simply queues events directly to the
point obj."""

# syn_connection_parameters should only have 1 element, PointProcessConnection will confirm
point_params = PointProcessConnParameters(sgid=syn_description[SynapseProperty.PRE_GID], delay=syn_description[SynapseProperty.AXONAL_DELAY],
weight=syn_description[SynapseProperty.G_SYNX])

pointConn = PointProcessConnection([point_params], syn_connection_parameters.get("Weight", 1.0))
pointConn.syn_description = syn_description
pointConn.hsynapse = self.pointcell.pointcell
pointConn.syn_id = SynapseID(*syn_id)
pointConn.post_cell_id = self.cell_id

self.synapses[pointConn.syn_id] = pointConn


def mechanism_name_from_model_template(template_path: str, model_template: str) -> str:
"""Translate SONATA model_template into a NEURON mechanism name.

Examples:
'hoc:AllenPointCell' -> 'AllenPointCell'
'nrn:IntFire1' -> 'IntFire1'
'AllenPointCell' -> 'AllenPointCell'
"""
mt = str(model_template).strip()
if ":" in mt:
prefix, name = mt.split(":", 1)
prefix = prefix.lower()
if prefix in ("hoc", "nrn"):
h.load_file(template_path)
return name
return mt
27 changes: 25 additions & 2 deletions bluecellulab/circuit/circuit_access/sonata_circuit_access.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,11 @@ def _select_edge_pop_names(self, projections) -> list[str]:
def extract_synapses(
self, cell_id: CellId, projections: Optional[list[str] | str]
) -> pd.DataFrame:
"""Extract the synapses."""
"""Extract the synapses. Checks available fields to determine which are
present in the edge file to determine the properties to extract.

If projections is None, all the synapses are extracted.
"""
snap_node_id = CircuitNodeId(cell_id.population_name, cell_id.id)
edges = self._circuit.edges

Expand All @@ -200,7 +204,10 @@ def extract_synapses(

# remove optional properties if they are not present
for optional_property in [SynapseProperty.U_HILL_COEFFICIENT,
SynapseProperty.CONDUCTANCE_RATIO]:
SynapseProperty.CONDUCTANCE_RATIO,
SynapseProperty.AFFERENT_SECTION_POS,
SynapseProperty.POST_SEGMENT_ID,
SynapseProperty.POST_SEGMENT_OFFSET]:
if optional_property.to_snap() not in edge_population.property_names:
edge_properties.remove(optional_property)

Expand All @@ -211,6 +218,22 @@ def extract_synapses(
):
edge_properties += list(SynapseProperties.plasticity)

# check that minimal tsodyks markram property exists - if missing, then try allen
# check for allen instance - replace the entire edge_properties list as appropriate
# properties for allen point/chemical neuron connection type edges
if SynapseProperty.U_SYN.to_snap() not in edge_population.property_names:
logging.debug("Missing required tsodyks markram property u_syn, checking for allen properties")
if all(
x in edge_population.property_names
for x in SynapseProperties.allen_chemical
):
edge_properties = list(SynapseProperties.allen_chemical)
elif all(
x in edge_population.property_names
for x in SynapseProperties.allen_point
):
edge_properties = list(SynapseProperties.allen_point)

Comment thread
darshanmandge marked this conversation as resolved.
snap_properties = properties_to_snap(edge_properties)
synapses: pd.DataFrame = edge_population.get(afferent_edges, snap_properties)
column_names = list(synapses.columns)
Expand Down
9 changes: 9 additions & 0 deletions bluecellulab/circuit/synapse_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class SynapseProperty(Enum):
U_HILL_COEFFICIENT = "u_hill_coefficient"
CONDUCTANCE_RATIO = "conductance_scale_factor"
AFFERENT_SECTION_POS = "afferent_section_pos"
TAU1 = "tau1"

@classmethod
def from_bluepy(cls, prop: BLPSynapse) -> SynapseProperty:
Expand Down Expand Up @@ -83,6 +84,14 @@ class SynapseProperties:
"volume_CR", "rho0_GB", "Use_d_TM", "Use_p_TM", "gmax_d_AMPA",
"gmax_p_AMPA", "theta_d", "theta_p"
)
allen_chemical = (
"afferent_section_id", "afferent_section_pos", "conductance", "delay", "tau1", "tau2", "erev",
"@source_node"
)
allen_point = (
"afferent_section_id", "afferent_section_pos", "conductance", "delay",
"@source_node"
)


snap_to_synproperty = MappingProxyType({
Expand Down
Loading