Skip to content
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

[BBPBGLIB-1137] Account for nonzero total current when using SEClamp; allow nonzero total current when simulating physical electrodes #134

Merged
merged 68 commits into from
May 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
73d945d
Replaces SEClamp with new conductanceSource point process
joseph-tharayil Feb 27, 2024
b794902
Checks if conductanceSource point process is available before trying …
joseph-tharayil Feb 27, 2024
a310e18
Bug fixes
joseph-tharayil Feb 27, 2024
0c8467d
Fix flake8 complains
jorblancoa Feb 28, 2024
b5b2c1e
Uses hasattr() to check if conductanceSource mechanism is available
joseph-tharayil Feb 28, 2024
8d65f18
Merge branch 'new_conductance_source' of github.com:BlueBrain/neuroda…
joseph-tharayil Feb 28, 2024
4177aa2
Replaces SEClamp with new conductanceSource point process
joseph-tharayil Feb 27, 2024
a0b4aea
Checks if conductanceSource point process is available before trying …
joseph-tharayil Feb 27, 2024
b78f5a2
Bug fixes
joseph-tharayil Feb 27, 2024
62fd6e2
Fix flake8 complains
jorblancoa Feb 28, 2024
d477fed
Uses hasattr() to check if conductanceSource mechanism is available
joseph-tharayil Feb 28, 2024
4572e1e
Use Neuron.h instead of Nd.h
jorblancoa Feb 29, 2024
f812ab5
Resolve merge conflicts
joseph-tharayil Feb 29, 2024
5ed7037
Replaces SEClamp with new conductanceSource point process
joseph-tharayil Feb 27, 2024
76131a3
Checks if conductanceSource point process is available before trying …
joseph-tharayil Feb 27, 2024
a01f99c
Bug fixes
joseph-tharayil Feb 27, 2024
fadf7b1
Fix flake8 complains
jorblancoa Feb 28, 2024
3f918f4
Uses hasattr() to check if conductanceSource mechanism is available
joseph-tharayil Feb 28, 2024
1bec510
Use Neuron.h instead of Nd.h
jorblancoa Feb 29, 2024
c132575
Merge branch 'main' into new_conductance_source
joseph-tharayil Mar 8, 2024
74083f4
Merge branch 'main' into new_conductance_source
joseph-tharayil Mar 11, 2024
2ab4f9e
Update online-lfp.rst
joseph-tharayil Mar 12, 2024
2ed5c64
Merge branch 'main' into new_conductance_source
joseph-tharayil Mar 12, 2024
ecf26d0
Merge branch 'new_conductance_source' of github.com:BlueBrain/neuroda…
joseph-tharayil Mar 13, 2024
d343786
Default behavior is for electrodes to input membrane current instead …
joseph-tharayil Apr 10, 2024
7d2d937
fixup! Default behavior is for electrodes to input membrane current i…
jorblancoa Apr 10, 2024
4c1cc6d
fixup! Default behavior is for electrodes to input membrane current i…
jorblancoa Apr 10, 2024
1aaca21
Makes ConductanceSource uppercase
joseph-tharayil Apr 10, 2024
b08ac53
Replaces SEClamp with new conductanceSource point process
joseph-tharayil Feb 27, 2024
697c83a
Checks if conductanceSource point process is available before trying …
joseph-tharayil Feb 27, 2024
f9a4c5f
Bug fixes
joseph-tharayil Feb 27, 2024
d5ff3eb
Fix flake8 complains
jorblancoa Feb 28, 2024
d8c3422
Replaces SEClamp with new conductanceSource point process
joseph-tharayil Feb 27, 2024
9162fcc
Checks if conductanceSource point process is available before trying …
joseph-tharayil Feb 27, 2024
4e4968d
Bug fixes
joseph-tharayil Feb 27, 2024
92b517f
Fix flake8 complains
jorblancoa Feb 28, 2024
2115a6e
Uses hasattr() to check if conductanceSource mechanism is available
joseph-tharayil Feb 28, 2024
e85a9a5
Use Neuron.h instead of Nd.h
jorblancoa Feb 29, 2024
81b27fd
Merge branch 'new_conductance_source' of github.com:BlueBrain/neuroda…
joseph-tharayil Apr 10, 2024
3499d94
Checks if conductanceSource point process is available before trying …
joseph-tharayil Feb 27, 2024
3982126
Bug fixes
joseph-tharayil Feb 27, 2024
4f3a8a2
Fix flake8 complains
jorblancoa Feb 28, 2024
a2a7cab
Uses hasattr() to check if conductanceSource mechanism is available
joseph-tharayil Feb 28, 2024
e6118c9
Use Neuron.h instead of Nd.h
jorblancoa Feb 29, 2024
89d7bca
Update online-lfp.rst
joseph-tharayil Mar 12, 2024
eb7cdf8
Default behavior is for electrodes to input membrane current instead …
joseph-tharayil Apr 10, 2024
8d11cb2
fixup! Default behavior is for electrodes to input membrane current i…
jorblancoa Apr 10, 2024
c282a46
fixup! Default behavior is for electrodes to input membrane current i…
jorblancoa Apr 10, 2024
5851c5d
Change ConductanceSource name
jorblancoa Apr 10, 2024
c403093
Fix parameter name
jorblancoa Apr 10, 2024
bcb043b
Merge branch 'new_conductance_source' of github.com:BlueBrain/neuroda…
joseph-tharayil Apr 11, 2024
1a42a94
Stimulus keys are in CamelCase
jorblancoa Apr 11, 2024
46682c4
Adds test for new stimulus sources
joseph-tharayil Apr 15, 2024
ea19b3a
Merge branch 'new_conductance_source' of github.com:BlueBrain/neuroda…
joseph-tharayil Apr 15, 2024
83c7c61
Bugfix
joseph-tharayil Apr 15, 2024
7ced8cf
Minor bugfixes
joseph-tharayil Apr 15, 2024
bbc1079
Fixes bugs in stimulus test
joseph-tharayil Apr 15, 2024
cdab66b
Fixes segfault
joseph-tharayil Apr 16, 2024
c518f6c
Merge branch 'main' into new_conductance_source
jorblancoa Apr 16, 2024
a59c43d
Merge branch 'main' into new_conductance_source
joseph-tharayil Apr 29, 2024
6a547db
Try removing conductance test
joseph-tharayil Apr 29, 2024
7fdd047
Merge branch 'new_conductance_source' of github.com:BlueBrain/neuroda…
joseph-tharayil Apr 29, 2024
5eda17a
Add stimulus injection test based on usecase3 circuit
jorblancoa May 1, 2024
b4f0f3e
base_memory consumption reduced
jorblancoa May 1, 2024
aaf45da
Adds physical electrode statement to test stimulus config
joseph-tharayil May 2, 2024
58eb8fa
Fixes typo
joseph-tharayil May 2, 2024
1347c20
Merge branch 'main' into new_conductance_source
jorblancoa May 8, 2024
1bc8d81
Updates docs
joseph-tharayil May 8, 2024
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
6 changes: 6 additions & 0 deletions docs/online-lfp.rst
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,12 @@ Subsequently, an ERROR will be encountered when instantiating the LFP report:
To ensure accurate and valid LFP reports, make sure that the electrodes file corresponds to the circuit being used in your simulation.

- **Stimulus Electrode Compatibility**: A common use case is that current will be injected into a population to account for synaptic inputs from neural populations that are not modeled. In this case,the injected current should be considered a membrane current rather than an electrode current, and it is neccessary that total current over the neuron sums to zero in order to produce valid extracellular recording results. The Neuron SEClamp class does not fulfill these criteria due to numerical issues. We have created a new point process, `ConductanceSource`, available in `neurodamus-neocortex`, which does fulfill the criteria. If an conductance source stimulus is present in the simulation config file, `ConductanceSource` will be used by default instead of the SEClamp mechanism. The injected current will be reported as part of the `i_membrane` variable, rather than as an electrode current.

However, it may be the case that the user wishes to model a physical electrode, rather than missing synaptic input, using the conductance source mechanism. In this case, the total current over the neuron is nonzero, and the injected current should not be considered a membrane current. For this reason, we have added the key `represents_phsyical_electrode` to the stimulus block. With the key-value pair `represents_physical_electrode:true`, SEClamp will be used rather than ConductanceSource.

Similarly, current sources may also be used to model the effects of missing synaptic inputs. We have created a new point process, `MembraneCurrentSource`, which is used instead of IClamp if the key `represents_phsyical_electrode` is set to false or is not set. `MembraneCurrentSource` behaves identically to IClamp, but is considered a membrane current, and is therefore accounted for in the calculation of the extracellular signal. It is not reported on as an electrode current. Setting `represents_physical_electrode:true` will result in using IClamp instead of `MembraneCurrentSource`

By keeping these considerations in mind, you can ensure a smooth and successful usage of the online LFP calculation feature.

Conclusion
Expand Down
46 changes: 34 additions & 12 deletions neurodamus/core/stimuli.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,22 @@

class SignalSource:

def __init__(self, base_amp=0.0, *, delay=0, rng=None):
def __init__(self, base_amp=0.0, *, delay=0, rng=None, represents_physical_electrode=False):
"""
Creates a new signal source, which can create composed signals
Args:
base_amp: The base (resting) amplitude of the signal (Default: 0)
rng: The Random Number Generator. Used in the Noise functions
represents_physical_electrode: Whether the source represents a phsyical
electrode or missing synaptic input
"""
h = Neuron.h
self.stim_vec = h.Vector()
self.time_vec = h.Vector()
self._cur_t = 0
self._base_amp = base_amp
self._rng = rng
self._represents_physical_electrode = represents_physical_electrode
if delay > .0:
self._add_point(base_amp)
self._cur_t = delay
Expand Down Expand Up @@ -356,7 +359,7 @@ def shot_noise(cls, tau_D, tau_R, rate, amp_mean, var, duration, dt=0.25, base_a

@classmethod
def ornstein_uhlenbeck(cls, tau, sigma, mean, duration, dt=0.25, base_amp=.0, **kw):
return cls(base_amp, **kw).add_ornstein_uhlenbeck(tau, sigma, mean,duration, dt)
return cls(base_amp, **kw).add_ornstein_uhlenbeck(tau, sigma, mean, duration, dt)

# Operations
def __add__(self, other):
Expand All @@ -367,19 +370,27 @@ def __add__(self, other):
class CurrentSource(SignalSource):
_all_sources = []

def __init__(self, base_amp=0.0, *, delay=0, rng=None):
def __init__(self, base_amp=0.0, *, delay=0, rng=None, physical_electrode=False):
"""
Creates a new current source that injects a signal under IClamp
"""
super().__init__(base_amp, delay=delay, rng=rng)
super().__init__(base_amp, delay=delay, rng=rng,
represents_physical_electrode=physical_electrode)
self._clamps = set()
self._all_sources.append(self)

class _Clamp:
def __init__(self, cell_section, position=0.5, clamp_container=None,
stim_vec_mode=True, time_vec=None, stim_vec=None,
**clamp_params):
self.clamp = Neuron.h.IClamp(position, sec=cell_section)
represents_physical_electrode = clamp_params.get('represents_physical_electrode', False)
# Checks if new MembraneCurrentSource mechanism is available and if source does not
# represent physical electrode, otherwise fall back to IClamp.
if not represents_physical_electrode and hasattr(Neuron.h, "MembraneCurrentSource"):
self.clamp = Neuron.h.MembraneCurrentSource(position, sec=cell_section)
else:
self.clamp = Neuron.h.IClamp(position, sec=cell_section)

if stim_vec_mode:
assert time_vec is not None and stim_vec is not None
self.clamp.dur = time_vec[-1]
Expand All @@ -397,8 +408,9 @@ def detach(self):
del self.clamp # Force del on the clamp (there might be references to self)

def attach_to(self, section, position=0.5):
return CurrentSource._Clamp(section, position, self._clamps, True,
self.time_vec, self.stim_vec)
return CurrentSource._Clamp(
section, position, self._clamps, True, self.time_vec, self.stim_vec,
represents_physical_electrode=self._represents_physical_electrode)

# Constant has a special attach_to and doesnt share any composing method
class Constant:
Expand All @@ -418,14 +430,16 @@ def attach_to(self, section, position=0.5):
class ConductanceSource(SignalSource):
_all_sources = []

def __init__(self, reversal=0.0, *, delay=.0, rng=None):
def __init__(self, reversal=0.0, *, delay=.0, rng=None, physical_electrode=False):
"""
Creates a new conductance source that injects a conductance by driving
the rs of an SEClamp at a given reversal potential.
reversal: reversal potential of conductance (mV)
"""
super().__init__(0.0, delay=delay, rng=rng) # set SignalSource's base_amp to zero
# set SignalSource's base_amp to zero
super().__init__(reversal, delay=delay, rng=rng,
represents_physical_electrode=physical_electrode)
self._reversal = reversal # set reversal from base_amp parameter in classmethods
self._clamps = set()
self._all_sources.append(self)
Expand All @@ -434,7 +448,14 @@ class _DynamicClamp:
def __init__(self, cell_section, position=0.5, clamp_container=None,
stim_vec_mode=True, time_vec=None, stim_vec=None,
reversal=0.0, **clamp_params):
self.clamp = Neuron.h.SEClamp(position, sec=cell_section)
represents_physical_electrode = clamp_params.get('represents_physical_electrode', False)
# Checks if new conductanceSource mechanism is available and if source does not
# represent physical electrode, otherwise fall back to SEClamp.
if not represents_physical_electrode and hasattr(Neuron.h, "ConductanceSource"):
self.clamp = Neuron.h.ConductanceSource(position, sec=cell_section)
else:
self.clamp = Neuron.h.SEClamp(position, sec=cell_section)

if stim_vec_mode:
assert time_vec is not None and stim_vec is not None
self.clamp.dur1 = time_vec[-1]
Expand All @@ -460,8 +481,9 @@ def detach(self):
del self.clamp # Force del on the clamp (there might be references to self)

def attach_to(self, section, position=0.5):
return ConductanceSource._DynamicClamp(section, position, self._clamps, True,
self.time_vec, self.stim_vec, self._reversal)
return ConductanceSource._DynamicClamp(
section, position, self._clamps, True, self.time_vec, self.stim_vec,
self._reversal, represents_physical_electrode=self._represents_physical_electrode)


# EStim class is a derivative of TStim for stimuli with an extracelular electrode. The main
Expand Down
38 changes: 28 additions & 10 deletions neurodamus/stimulus_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ class BaseStim:
def __init__(self, _target, stim_info: dict, _cell_manager):
self.duration = float(stim_info["Duration"]) # duration [ms]
self.delay = float(stim_info["Delay"]) # start time [ms]
self.represents_physical_electrode = stim_info.get('RepresentsPhysicalElectrode', False)


@StimulusManager.register_type
Expand Down Expand Up @@ -123,7 +124,10 @@ def __init__(self, target, stim_info: dict, cell_manager):

rng = random.Random123(seed1, seed2, seed3(gid)) # setup RNG
ou_args = (self.tau, self.sigma, self.mean, self.duration)
ou_kwargs = {'dt': self.dt, 'delay': self.delay, 'rng': rng}
ou_kwargs = {
'dt': self.dt, 'delay': self.delay, 'rng': rng,
'physical_electrode': self.represents_physical_electrode
}
# inject Ornstein-Uhlenbeck signal
if stim_info["Mode"] == "Conductance":
cs = ConductanceSource.ornstein_uhlenbeck(*ou_args, **ou_kwargs,
Expand Down Expand Up @@ -252,7 +256,10 @@ def __init__(self, target, stim_info: dict, cell_manager):
rng = random.Random123(seed1, seed2, seed3(gid)) # setup RNG
shotnoise_args = (self.tau_D, self.tau_R, self.rate,
self.amp_mean, self.amp_var, self.duration)
shotnoise_kwargs = {'dt': self.dt, 'delay': self.delay, 'rng': rng}
shotnoise_kwargs = {
'dt': self.dt, 'delay': self.delay, 'rng': rng,
'physical_electrode': self.represents_physical_electrode
}
# generate shot noise current source
if stim_info["Mode"] == "Conductance":
cs = ConductanceSource.shot_noise(*shotnoise_args, **shotnoise_kwargs,
Expand Down Expand Up @@ -454,8 +461,11 @@ def __init__(self, target, stim_info: dict, cell_manager):
continue

# generate ramp current source
cs = CurrentSource.ramp(self.amp_start, self.amp_end, self.duration,
delay=self.delay)
cs = CurrentSource.ramp(
self.amp_start, self.amp_end, self.duration,
delay=self.delay,
physical_electrode=self.represents_physical_electrode
)
# attach current source to section
cs.attach_to(sc.sec, tpoint_list.x[sec_id])
self.stimList.append(cs) # save CurrentSource
Expand Down Expand Up @@ -580,8 +590,10 @@ def __init__(self, target, stim_info: dict, cell_manager):
continue

# generate noise current source
cs = CurrentSource.noise(self.mean, self.var, self.duration,
dt=self.dt, delay=self.delay, rng=rng)
cs = CurrentSource.noise(
self.mean, self.var, self.duration, dt=self.dt, delay=self.delay, rng=rng,
physical_electrode=self.represents_physical_electrode
)
# attach current source to section
cs.attach_to(sc.sec, tpoint_list.x[sec_id])
self.stimList.append(cs) # save CurrentSource
Expand Down Expand Up @@ -660,8 +672,11 @@ def __init__(self, target, stim_info: dict, cell_manager):
continue

# generate pulse train current source
cs = CurrentSource.train(self.amp, self.freq, self.width,
self.duration, delay=self.delay)
cs = CurrentSource.train(
self.amp, self.freq, self.width, self.duration,
delay=self.delay,
physical_electrode=self.represents_physical_electrode
)
# attach current source to section
cs.attach_to(sc.sec, tpoint_list.x[sec_id])
self.stimList.append(cs) # save CurrentSource
Expand Down Expand Up @@ -696,8 +711,10 @@ def __init__(self, target, stim_info: dict, cell_manager):
continue

# generate sinusoidal current source
cs = CurrentSource.sin(self.amp, self.duration, self.freq,
step=self.dt, delay=self.delay)
cs = CurrentSource.sin(
self.amp, self.duration, self.freq, step=self.dt, delay=self.delay,
physical_electrode=self.represents_physical_electrode
)
# attach current source to section
cs.attach_to(sc.sec, tpoint_list.x[sec_id])
self.stimList.append(cs) # save CurrentSource
Expand Down Expand Up @@ -735,6 +752,7 @@ def __init__(self, target, stim_info: dict, cell_manager):

# create single electrode voltage clamp at location
seclamp = Nd.h.SEClamp(tpoint_list.x[sec_id], sec=sc.sec)

seclamp.rs = self.rs
seclamp.dur1 = self.duration
seclamp.amp1 = self.vhold
Expand Down
2 changes: 1 addition & 1 deletion tests/integration-e2e/test_dry_run_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def test_dry_run_workflow(USECASE3):

assert 20.0 <= nd._dry_run_stats.cell_memory_total <= 30.0
assert 0.0 <= nd._dry_run_stats.synapse_memory_total <= 1.0
assert 80.0 <= nd._dry_run_stats.base_memory <= 120.0
assert 70.0 <= nd._dry_run_stats.base_memory <= 120.0
expected_items = {
'L4_PC-dSTUT': 2,
'L4_MC-dSTUT': 1,
Expand Down
109 changes: 109 additions & 0 deletions tests/scientific/test_current_injection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import json
import numpy
import os
import pytest
from tempfile import NamedTemporaryFile


@pytest.fixture
def sonata_config_files(sonata_config, input_type):
config_files = []
for represents_physical_electrode in [True, False]:
# Create a deep copy of sonata_config for each configuration to avoid conflicts
config_copy = json.loads(json.dumps(sonata_config))

stimulus_config = {
"input_type": input_type,
"delay": 5,
"duration": 2100,
"node_set": "l4pc",
"represents_physical_electrode": represents_physical_electrode
}

if input_type == "current_clamp":
stimulus_config.update({
"module": "noise",
"mean": 0.05,
"variance": 0.01
})
elif input_type == "conductance":
stimulus_config.update({
"module": "ornstein_uhlenbeck",
"mean": 0.05,
"sigma": 0.01,
"tau": 0.1
})

config_copy["inputs"] = {"Stimulus": stimulus_config}
config_copy["reports"] = {
"current": {
"type": "summation",
"cells": "l4pc",
"variable_name": "i_membrane",
"unit": "nA",
"dt": 0.1,
"start_time": 0.0,
"end_time": 50.0
},
"voltage": {
"type": "compartment",
"cells": "l4pc",
"variable_name": "v",
"unit": "mV",
"dt": 0.1,
"start_time": 0.0,
"end_time": 50.0
}
}

with NamedTemporaryFile("w", suffix='.json', delete=False) as config_file:
json.dump(config_copy, config_file)
config_files.append(config_file.name)

yield tuple(config_files)

# Cleanup
for config_file in config_files:
os.unlink(config_file)


def _read_sonata_soma_report(report_name):
import libsonata
report = libsonata.SomaReportReader(report_name)
pop_name = report.get_population_names()[0]
ids = report[pop_name].get_node_ids()
data = report[pop_name].get(node_ids=[ids[0]])
return numpy.array(data.data).flatten()


def _run_simulation(config_file):
import subprocess
output_dir = "output_current_conductance"
command = [
"neurodamus",
config_file,
f"--output-path={output_dir}"
]
config_dir = os.path.dirname(config_file)
subprocess.run(command, cwd=config_dir, check=True)
soma_report_path = os.path.join(config_dir, output_dir, "voltage.h5")
return _read_sonata_soma_report(soma_report_path)


@pytest.mark.parametrize("input_type", [
"current_clamp",
"conductance",
])
def test_current_conductance_injection(sonata_config_files):
"""
Test the consistency of voltage traces between original and new configurations
(set by 'represents_physical_electrode': true/false)
under different types of input (current clamp and conductance).
"""
import numpy.testing as npt
config_file_original, config_file_new = sonata_config_files

voltage_vec_original = _run_simulation(config_file_original)
voltage_vec_new = _run_simulation(config_file_new)

npt.assert_equal(voltage_vec_original, voltage_vec_new)
9 changes: 6 additions & 3 deletions tests/simulations/v5_sonata/simulation_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@
"variance": 0.001,
"delay": 0.0,
"duration": 30000,
"node_set": "Excitatory"
"node_set": "Excitatory",
"represents_physical_electrode":true
},
"ThresholdInh": {
"module": "noise",
Expand All @@ -101,14 +102,16 @@
"variance": 0.001,
"delay": 0.0,
"duration": 30000,
"node_set": "Inhibitory"
"node_set": "Inhibitory",
"represents_physical_electrode":true
},
"hypamp_mosaic": {
"module": "hyperpolarizing",
"input_type": "current_clamp",
"delay": 0.0,
"duration": 30000,
"node_set": "Mosaic"
"node_set": "Mosaic",
"represents_physical_electrode":true
}
},
"reports": {
Expand Down