Source code for bluepyefe.protocol
"""Protocol class"""
"""
Copyright (c) 2022, EPFL/Blue Brain Project
This file is part of BluePyEfe <https://github.com/BlueBrain/BluePyEfe>
This library is free software; you can redistribute it and/or modify it under
the terms of the GNU Lesser General Public License version 3.0 as published
by the Free Software Foundation.
This library is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
details.
You should have received a copy of the GNU Lesser General Public License
along with this library; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""
import numpy
import logging
from bluepyefe.ecode import eCodes
logger = logging.getLogger(__name__)
[docs]
class Protocol():
"""Protocol informs about the current stimulus that was used to obtain
efeatures at a given amplitude for a given protocol name. This class
is mainly used to produce a description of the experimental protocol
that can be used in BluePyOpt"""
def __init__(
self,
name,
amplitude,
tolerance,
feature_targets=None,
global_rheobase=None,
mode="mean",
):
"""Constructor
Args:
name (str): name of the protocol (ex: 'APWaveform')
amplitude (float): amplitude of the current stimuli for the
present protocol (expressed as a percentage of the
threshold amplitude or in absolute current depending on the
setting absolute_amplitude)
tolerance (float): tolerance around the target amplitude in which
an experimental recording will be seen as a hit during
efeatures extraction (expressed as a percentage of the
threshold amplitude or in absolute current depending on the
setting absolute_amplitude)
feature_targets (list): list of EFeatureTarget associated to the
protocol
global_rheobase (float): average rheobase across all cells
mode (str): if the protocol matches several recordings, the mode
set the logic of how the output will be generating. Must be
'mean', 'median' or 'lnmc'
"""
self.name = name
self.amplitude = amplitude
self.tolerance = tolerance
self.feature_targets = feature_targets
if self.feature_targets is None:
self.feature_targets = []
self.global_rheobase = global_rheobase
self.mode = mode
self.recordings = []
@property
def stimulus_name(self):
"""Name of the stimulus associated to the protocol"""
return f"{self.name}_{self.amplitude}"
@property
def n_match(self):
"""Number of recordings whose amplitude matched the present protocol"""
return sum([f.sample_size for f in self.feature_targets])
@property
def ecode(self):
"""Create a temporary eCode that matches all the recordings for the
present protocol. The eCode's parameters are computed differently
depending on the mode of the protocol"""
if not self.recordings:
return None
for ecode in eCodes.keys():
if ecode.lower() in self.name.lower():
ecode = eCodes[ecode]({}, {}, self.name)
break
else:
raise KeyError(
"There is no eCode linked to the stimulus name {}. See "
"ecode/__init__.py for the available stimuli names"
"".format(self.name.lower())
)
if self.mode == "mean":
self.reduce_ecode(ecode, operator=numpy.nanmean)
elif self.mode == "median":
self.reduce_ecode(ecode, operator=numpy.nanmedian)
elif self.mode == "min":
self.reduce_ecode(ecode, operator=numpy.nanmin)
elif self.mode == "max":
self.reduce_ecode(ecode, operator=numpy.nanmax)
else:
raise ValueError("'mode' should be mean or median")
return ecode
[docs]
def append(self, recording):
"""Append a Recording to the present protocol"""
for i, target in enumerate(self.feature_targets):
if target.efeature_name in recording.efeatures:
self.feature_targets[i].append(
recording.efeatures[target.efeature_name],
recording.files
)
if (
recording.auto_threshold is not None and
"Threshold" not in self.feature_targets[i].efel_settings
):
self.feature_targets[i]._auto_thresholds.append(
recording.auto_threshold)
self.recordings.append(recording)
[docs]
def as_dict(self):
"""Returns a dictionary that defines the present protocol. This
definition is computed differently depending on the mode of the
protocol
"""
return {
"holding": {
"delay": 0.0,
"amp": self.ecode.hypamp,
"duration": self.ecode.tend,
"totduration": self.ecode.tend,
},
"step": self.ecode.get_stimulus_parameters(),
}
[docs]
def reduce_ecode(self, ecode, operator):
"""Creates an eCode defined from the parameters of all the recordings
matching the present protocol"""
if not self.recordings:
logger.warning(
"Could not compute average ecode for protocol {} target {} "
"because it didn't match any recordings".format(
self.name, self.amplitude
)
)
return None
params = [r.get_params() for r in self.recordings]
if self.global_rheobase is None and "amp" in params[0]:
logger.warning(
"No global threshold amplitude passed. This can result"
" in inconsistencies in-between protocols if some cells "
"only matched a subset of the targets."
)
for key in params[0]:
if isinstance(params[0][key], (list, numpy.ndarray)):
logger.warning(
"Parameter {} for protocol {} is a list and cannot be "
"averaged across recordings".format(key, self.name)
)
setattr(ecode, key, params[0][key])
continue
if key == "amp" and self.global_rheobase:
amp_rel = operator([c["amp_rel"] for c in params])
mean_param = float(amp_rel) * self.global_rheobase / 100.
elif key == "amp2" and self.global_rheobase:
amp_rel2 = operator([c["amp2_rel"] for c in params])
mean_param = float(amp_rel2) * self.global_rheobase / 100.
else:
mean_param = operator([numpy.nan if c[key] is None else c[key] for c in params])
if numpy.isnan(mean_param):
mean_param = None
setattr(ecode, key, mean_param)
return ecode
def __str__(self):
"""String representation"""
str_form = "Protocol {} {:.1f}%:\n".format(
self.name, self.amplitude
)
str_form += "Number of matching recordings: {}".format(self.n_match)
if self.n_match:
str_form += "\neCode: {}\n".format(self.as_dict)
return str_form