copying to personal repo

This commit is contained in:
Alan
2022-06-19 13:45:53 -05:00
commit bf2ffa7315
287 changed files with 54032 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
"""
Definitions of cell populations for each cell type.
"""
from .population import Population
from .bushy import Bushy
from .tstellate import TStellate
from .dstellate import DStellate
from .pyramidal import Pyramidal
from .tuberculoventral import Tuberculoventral
from .sgc import SGC

View File

@@ -0,0 +1,58 @@
import scipy.stats
import numpy as np
from .population import Population
from .. import cells
class Bushy(Population):
"""Population of bushy cells.
Cells are distributed uniformly from 2kHz to 64kHz.
Note that `cf` is the mean value used when selecting SGCs to connect;
it is NOT the measured CF of the cell (although it should be close).
"""
type = "bushy"
def __init__(self, species="mouse", **kwds):
freqs = self._get_cf_array(species)
fields = [
("cf", float),
("input_sr", list), # distribution probability of SGC SR groups
("sr", int),
]
super(Bushy, self).__init__(species, len(freqs), fields=fields, **kwds)
self._cells["cf"] = freqs
self._cells["input_sr"] = [np.tile([1.0, 1.0, 1.0], len(freqs))]
def create_cell(self, cell_rec):
""" Return a single new cell to be used in this population. The
*cell_rec* argument is the row from self.cells that describes the cell
to be created.
"""
return cells.Bushy.create(species=self.species, **self._cell_args)
def connection_stats(self, pop, cell_rec):
""" The population *pop* is being connected to the cell described in
*cell_rec*. Return the number of presynaptic cells that should be
connected and a dictionary of distributions used to select cells
from *pop*.
"""
size, dist = Population.connection_stats(self, pop, cell_rec)
from .. import populations
if isinstance(pop, populations.SGC):
# only select SGC inputs from a single SR group
# (this relationship is hypothesized based on reconstructions of
# endbulbs)
sr_vals = pop.cells["sr"]
u = np.random.choice(sr_vals) # assign input sr for this cell
# print('u: ', u)
# pick from one sr group for all inputs, with prob same as distribution in nerve
dist["sr"] = (sr_vals == u).astype(float)
self._cells["sr"] = u
return size, dist

View File

@@ -0,0 +1,42 @@
import scipy.stats
import numpy as np
from .population import Population
from .. import cells
class DStellate(Population):
type = "dstellate"
def __init__(self, species="mouse", **kwds):
# Note that `cf` is the mean value used when selecting SGCs to connect;
# it is NOT the measured CF of the cell (although it should be close).
freqs = self._get_cf_array(species)
fields = [
("cf", float),
("input_sr", list), # distribution probability of SGC SR groups
]
super(DStellate, self).__init__(species, len(freqs), fields=fields, **kwds)
self._cells["cf"] = freqs
self._cells["input_sr"] = [np.tile([1.0, 1.0, 1.0], len(freqs))]
def create_cell(self, cell_rec):
""" Return a single new cell to be used in this population. The
*cell_rec* argument is the row from self.cells that describes the cell
to be created.
"""
return cells.DStellate.create(species=self.species, **self._cell_args)
def connection_stats(self, pop, cell_rec):
""" The population *pop* is being connected to the cell described in
*cell_rec*. Return the number of presynaptic cells that should be
connected and a dictionary of distributions used to select cells
from *pop*.
"""
size, dist = Population.connection_stats(self, pop, cell_rec)
from .. import populations
if isinstance(pop, populations.SGC):
dist["sr"] = (pop.cells["sr"] < 2).astype(float)
return size, dist

View File

@@ -0,0 +1,443 @@
import logging
import scipy.stats
import numpy as np
from .. import data
class Population(object):
"""
A Population represents a group of cell all having the same type.
Populations provide methods for:
* Adding cells to the population with characteristic distributions.
* Connecting the cells in one population to the cells in another.
* Automatically adding cells to satisfy connectivity requirements when
connecting populations together.
Populations have a concept of a "natural" underlying distribution of
neurons, and behave as if all neurons in this distribution already exist
in the model. However, initially all neurons are virtual, and are only
instantiated to become a part of the running model if the neuron provides
synaptic input to another non-virtual neuron, or if the user explicitly
requests a recording of the neuron.
Subclasses represent populations for a specific cell type, and at least
need to reimplement the `create_cell` and `connection_stats` methods.
"""
def __init__(self, species, size, fields, synapsetype="multisite", **kwds):
self._species = species
self._post_connections = [] # populations this one connects to
self._pre_connections = [] # populations connecting to this one
self._synapsetype = synapsetype
# numpy record array with information about each cell in the
# population
fields = [
("id", int),
("cell", object),
("input_resolved", bool),
("connections", object), # {pop: [cells], ...}
] + fields
self._cells = np.zeros(size, dtype=fields)
self._cells["id"] = np.arange(size)
self._cell_indexes = {} # maps cell:index
self._cell_args = kwds
@property
def cells(self):
""" The array of cells in this population.
For all populations, this array has a 'cell' field that is either 0
(for virtual cells) or a Cell instance (for real cells).
Extra fields may be added by each Population subclass.
"""
return self._cells.copy()
@property
def species(self):
return self._species
def unresolved_cells(self):
""" Return indexes of all real cells whose inputs have not been
resolved.
"""
real = self._cells["cell"] != 0
unresolved = self._cells["input_resolved"] == False
return np.argwhere(real & unresolved)[:, 0]
def real_cells(self):
""" Return indexes of all real cells in this population.
Initially, all cells in the population are virtual--they are accounted
for, but not actually instantiated as part of the NEURON simulation.
Virtual cells can be made real by calling `get_cell()`. This method
returns the indexes of all cells for which `get_cell()` has already
been invoked.
"""
return np.argwhere(self._cells["cell"] != 0)[:, 0]
def connect(self, *pops):
""" Connect this population to any number of other populations.
A connection is unidirectional; calling ``pop1.connect(pop2)`` can only
result in projections from pop1 to pop2.
Note that the connection is purely symbolic at first; no cells are
actually connected by synapses at this time.
"""
self._post_connections.extend(pops)
for pop in pops:
pop._pre_connections.append(self)
@property
def pre_connections(self):
""" The list of populations connected to this one.
"""
return self._pre_connections[:]
def cell_connections(self, index):
""" Return a dictionary containing, for each population, a list of
cells connected to the cell in this population at *index*.
"""
return self._cells[index]["connections"]
def resolve_inputs(self, depth=1, showlog=False):
""" For each _real_ cell in the population, select a set of
presynaptic partners from each connected population and generate a
synapse from each.
Although it is allowed to call ``resolve_inputs`` multiple times for
a single population, each individual cell will only resolve its inputs
once. Therefore, it is recommended to create and connect all
populations before making any calls to ``resolve_inputs``.
"""
for i in self.unresolved_cells():
# loop over all cells whose presynaptic inputs have not been resolved
cell = self._cells[i]["cell"]
if showlog:
logging.info("Resolving inputs for %s %d", self, i)
self._cells[i]["connections"] = {}
# select cells from each population to connect to this cell
for pop in self._pre_connections:
pre_cells = self.connect_pop_to_cell(pop, i)
if showlog:
logging.info(" connected %d cells from %s", len(pre_cells), pop)
assert pre_cells is not None
self._cells[i]["connections"][pop] = pre_cells
self._cells[i]["input_resolved"] = True
# recursively resolve inputs in connected populations
if depth > 1:
for pop in self.pre_connections:
pop.resolve_inputs(depth - 1, showlog=showlog)
def connect_pop_to_cell(self, pop, cell_index):
""" Connect cells in a presynaptic population to the cell in this
population at *cell_index*, and return the presynaptic indexes of cells
that were connected.
This method is responsible for choosing pairs of cells to be connected
by synapses, and may be overridden in subclasses.
The default implementation calls `self.connection_stats()` to determine
the number and selection criteria of presynaptic cells.
"""
cell_rec = self._cells[cell_index]
cell = cell_rec["cell"]
size, dist = self.connection_stats(pop, cell_rec)
# Select SGCs from distribution, create, and connect to this cell
# todo: select sgcs with similar spont. rate?
pre_cells = pop.select(size=size, create=False, **dist)
for j in pre_cells:
pre_cell = pop.get_cell(j)
# use default settings for connecting these.
pre_cell.connect(cell, type=self._synapsetype)
return pre_cells
def connection_stats(self, pop, cell_rec):
""" The population *pop* is being connected to the cell described in
*cell_rec*.
This method is responsible for deciding the distributions of presynaptic
cell properties for any given postsynaptic cell (for example, a cell
with cf=10kHz might receive SGC input from 10 cells selected from a
normal distribution centered at 10kHz).
The default implementation of this method uses the 'convergence' and
'convergence_range' values from the data tables to specify a lognormal
distribution of presynaptic cells around the postsynaptic cell's CF.
This method must return a tuple (size, dist) with the following values:
* size: integer giving the number of cells that should be selected from
the presynaptic population and connected to the postsynaptic cell.
* dist: dictionary of {property_name: distribution} pairs that describe
how cells should be selected from the presynaptic population. See
keyword arguments to `select()` for more information on the content
of this dictionary.
"""
cf = cell_rec["cf"]
# Convergence distributions (how many presynaptic
# cells to connect)
try:
n_connections = data.get(
"convergence",
species=self.species,
pre_type=pop.type,
post_type=self.type,
)
except KeyError:
raise TypeError(
"Cannot connect population %s to %s; no convergence specified in data table."
% (pop, self)
)
if isinstance(n_connections, tuple):
size_dist = scipy.stats.norm(loc=n_connections[0], scale=n_connections[1])
size = max(0, size_dist.rvs())
else:
size = n_connections
size = int(size) # must be an integer at this point
# Convergence ranges -- over what range of CFs should we
# select presynaptic cells.
try:
input_range = data.get(
"convergence_range",
species=self.species,
pre_type=pop.type,
post_type=self.type,
)
except KeyError:
raise TypeError(
"Cannot connect population %s to %s; no convergence range specified in data table."
% (pop, self)
)
dist = {"cf": scipy.stats.lognorm(input_range, scale=cf)}
# print(cf, input_range, dist)
return size, dist
def get_sgcsr_array(self, freqs, species="mouse"):
"""
Create an array of length (freqs) (number of SGCs)
Each entry is a value indicating the SR group, according to some proportion
2 = high, 1 = medium, 0 = low
Parameters
----------
freqs : nunpy array
species : str (default: 'mouse')
name of the species for the map.
Returns:
numpy array
An array matched to freqs, with SR's indicated numerically
"""
assert species == "mouse" # only mice so far.
nhs = np.random.random_sample(
freqs.shape[0]
) # uniform random distribution across frequency
sr_array = np.zeros_like(freqs) # build array - initially all low sponts
sr_array[
np.argwhere(nhs < 0.53)
] = 2 # high spont (53% estimated from Taberner and Liberman, 2005)
sr_array[
np.argwhere((nhs >= 0.53) & (nhs < 0.77))
] = 1 # medium spont, about 24% (1-20 sp/sec)
# the rest have SR value of 0, corresponding to the low-spont group
return sr_array
def select_sgcsr_inputs(self, sr_array, weights):
"""
Subsample the arrays above to create a distribution for cells that might only get
a subset of inputs (for example, only msr and lsr fibers)
Parameters
----------
sr_array : numpy array
the SR array to draw the samples from
weights : 3 element list
Weights for [lsr, msr, hsr] ANFs. Proportions will be computed
from these weights (e.g., [1,1,1] is uniform for all fibers)
weights of [1,1,0] means all hsr fibers will be masked
Returns:
numpy array of "dist"
Values of 0 are sgcs masked from input, 1 are ok
"""
assert len(weights) == 3
dist = np.zeros_like(sr_array) # boolean array, all values
norm_wt = 3.0 * np.array(
weights / np.sum(weights)
) # fraction from within each group
for i in range(len(weights)):
dx = np.where(sr_array == i)[0]
ind = np.random.choice(len(dx), int(norm_wt[i] * len(dx)))
dist[dx[ind]] = 1
return dist
def _get_cf_array(self, species):
"""Return the array of CF values that should be used when instantiating
this population.
Commonly used by subclasses durin initialization.
"""
size = data.get(
"populations", species=species, cell_type=self.type, field="n_cells"
)
fmin = data.get(
"populations", species=species, cell_type=self.type, field="cf_min"
)
fmax = data.get(
"populations", species=species, cell_type=self.type, field="cf_max"
)
s = (fmax / fmin) ** (1.0 / size)
freqs = fmin * s ** np.arange(size)
# print('frqs #: ', len(freqs))
# Cut off at 40kHz because the auditory nerve model only goes that far :(
freqs = freqs[freqs <= 40e3]
return freqs
def select(self, size, create=False, **kwds):
""" Return a list of indexes for cells matching the selection criteria.
The *size* argument specifies the number of cells to return.
If *create* is True, then any selected cells that are virtual will be
instantiated.
Each keyword argument must be the name of a field in self.cells. Values
may be:
* A distribution (see scipy.stats), in which case the distribution
influences the selection of cells
* An array giving the probability to assign to each cell in the
population
* A number, in which case the cell(s) with the closest match
are returned. If this is used, it overrides all other criteria except
where they evaluate to 0.
If multiple distributions are provided, then the product of the survival
functions of all distributions determines the probability of selecting
each cell.
"""
if len(kwds) == 0:
raise TypeError("Must specify at least one selection criteria")
full_dist = np.ones(len(self._cells))
nearest = None
nearest_field = None
for field, dist in kwds.items():
if np.isscalar(dist):
if nearest is not None:
raise Exception(
"May not specify multiple single-valued selection criteria."
)
nearest = dist
nearest_field = field
elif isinstance(dist, scipy.stats.distributions.rv_frozen):
vals = self._cells[field]
dens = np.diff(vals)
dens = np.concatenate([dens[:1], dens])
pdf = dist.pdf(vals) * dens
full_dist *= pdf / pdf.sum()
elif isinstance(dist, np.ndarray):
full_dist *= dist
else:
raise TypeError("Distributed criteria must be array or rv_frozen.")
# Select cells nearest to the requested value, but only pick from
# cells with nonzero probability.
if nearest is not None:
cells = []
mask = full_dist == 0
err = np.abs(self._cells[nearest_field] - nearest).astype(float)
for i in range(size):
err[mask] = np.inf
cell = np.argmin(err)
mask[cell] = True
cells.append(cell)
# Select cells randomly from the specified combined probability
# distribution
else:
cells = []
full_dist /= full_dist.sum()
vals = np.random.uniform(size=size)
vals.sort()
cumulative = np.cumsum(full_dist)
for val in vals:
u = np.argwhere(cumulative >= val)
if len(u) > 0:
cell = u[0, 0]
cells.append(cell)
if create:
self.create_cells(cells)
return cells
def get_cell(self, i, create=True):
""" Return the cell at index i. If the cell is virtual, then it will
be instantiated first unless *create* is False.
"""
if create and self._cells[i]["cell"] == 0:
self.create_cells([i])
return self._cells[i]["cell"]
def get_cell_index(self, cell):
"""Return the index of *cell*.
"""
return self._cell_indexes[cell]
def create_cells(self, cell_inds):
""" Instantiate each cell in *cell_inds*, which is a list of indexes into
self.cells.
"""
for i in cell_inds:
if self._cells[i]["cell"] != 0:
continue
cell = self.create_cell(self._cells[i])
self._cells[i]["cell"] = cell
self._cell_indexes[cell] = i
def create_cell(self, cell_rec):
""" Return a single new cell to be used in this population. The
*cell_rec* argument is the row from self.cells that describes the cell
to be created.
Subclasses must reimplement this method.
"""
raise NotImplementedError()
def __str__(self):
return "<Population %s (%d/%d real)>" % (
type(self).__name__,
(self._cells["cell"] != 0).sum(),
len(self._cells),
)
def __getstate__(self):
"""Return a picklable copy of self.__dict__.
Note that we remove references to the actual cells in order to allow pickling.
"""
state = self.__dict__.copy()
state["_cells"] = state["_cells"].copy()
for cell in state["_cells"]:
if cell["cell"] != 0:
cell["cell"] = cell[
"cell"
].type # replace neuron object with just the cell type
cell = str(cell) # make a string
return state

View File

@@ -0,0 +1,26 @@
import scipy.stats
import numpy as np
from .population import Population
from .. import cells
class Pyramidal(Population):
type = "pyramidal"
def __init__(
self, species="mouse", **kwds
): # ***** NOTE Species - no direct data for mouse (uses RAT data)
# Note that `cf` is the mean value used when selecting SGCs to connect;
# it is NOT the measured CF of the cell (although it should be close).
freqs = self._get_cf_array(species)
fields = [("cf", float)]
super(Pyramidal, self).__init__(species, len(freqs), fields=fields, **kwds)
self._cells["cf"] = freqs
def create_cell(self, cell_rec):
""" Return a single new cell to be used in this population. The
*cell_rec* argument is the row from self.cells that describes the cell
to be created.
"""
return cells.Pyramidal.create(species=self.species, **self._cell_args)

View File

@@ -0,0 +1,87 @@
import logging
import numpy as np
import pyqtgraph.multiprocess as mp
from .population import Population
from .. import cells
class SGC(Population):
"""A population of spiral ganglion cells.
The cell distribution is uniform from 2kHz to 64kHz, evenly divided between
spontaneous rate groups.
"""
type = "sgc"
def __init__(self, species="mouse", model="dummy", **kwds):
# Completely fabricated cell distribution: uniform from 2kHz to 40kHz,
# evenly divided between SR groups. We only go up to 40kHz because the
# auditory periphery model does not support >40kHz.
freqs = self._get_cf_array(species)
fields = [("cf", float), ("sr", int)] # 0=low sr, 1=mid sr, 2=high sr
super(SGC, self).__init__(
species, len(freqs), fields=fields, model=model, **kwds
)
self._cells["cf"] = freqs
# old version:
# evenly distribute SR groups
# self._cells['sr'] = np.arange(len(freqs)) % 3
# new version:
# draw from distributions matching approximate SR distribution
sr_vals = self.get_sgcsr_array(freqs, species="mouse")
self._cells["sr"] = sr_vals
def set_seed(self, seed):
self.next_seed = seed
def create_cell(self, cell_rec):
""" Return a single new cell to be used in this population. The
*cell_rec* argument is the row from self.cells that describes the cell
to be created.
"""
return cells.SGC.create(
species=self.species,
cf=cell_rec["cf"],
sr=cell_rec["sr"],
**self._cell_args
)
def connect_pop_to_cell(self, pop, index):
# SGC does not support any inputs
assert len(self.connections) == 0
def set_sound_stim(self, stim, parallel=False):
"""Set a sound stimulus to generate spike trains for all (real) cells
in this population.
"""
real = self.real_cells()
logging.info("Assigning spike trains to %d SGC cells..", len(real))
if not parallel:
for i, ind in enumerate(real):
# logging.info("Assigning spike train to SGC %d (%d/%d)", ind, i, len(real))
cell = self.get_cell(ind)
cell.set_sound_stim(stim, self.next_seed)
self.next_seed += 1
else:
seeds = range(self.next_seed, self.next_seed + len(real))
self.next_seed = seeds[-1] + 1
tasks = zip(seeds, real)
trains = [None] * len(tasks)
# generate spike trains in parallel
with mp.Parallelize(
enumerate(tasks),
trains=trains,
progressDialog="Generating SGC spike trains..",
) as tasker:
for i, x in tasker:
seed, ind = x
cell = self.get_cell(ind)
train = cell.generate_spiketrain(stim, seed)
tasker.trains[i] = train
# collected all trains; now assign to cells
for i, ind in enumerate(real):
cell = self.get_cell(ind)
cell.set_spiketrain(trains[i])

View File

@@ -0,0 +1,43 @@
import scipy.stats
import numpy as np
from .population import Population
from .. import cells
class TStellate(Population):
type = "tstellate"
def __init__(self, species="mouse", **kwds):
# Note that `cf` is the mean value used when selecting SGCs to connect;
# it is NOT the measured CF of the cell (although it should be close).
freqs = self._get_cf_array(species)
fields = [("cf", float), ("input_sr", list)]
super(TStellate, self).__init__(species, len(freqs), fields=fields, **kwds)
self._cells["cf"] = freqs
self._cells["input_sr"] = [np.tile([1.0, 1.0, 1.0], len(freqs))]
def create_cell(self, cell_rec):
""" Return a single new cell to be used in this population. The
*cell_rec* argument is the row from self.cells that describes the cell
to be created.
"""
return cells.TStellate.create(species=self.species, **self._cell_args)
def connection_stats(self, pop, cell_rec):
""" The population *pop* is being connected to the cell described in
*cell_rec*. Return the number of presynaptic cells that should be
connected and a dictionary of distributions used to select cells
from *pop*.
"""
size, dist = Population.connection_stats(self, pop, cell_rec)
from .. import populations
if isinstance(pop, populations.SGC):
# Select SGC inputs from a all SR groups
sr_vals = pop.cells["sr"]
# print('SRs for TS: ', np.bincount(sr_vals)/sr_vals.shape[0], np.unique(sr_vals))
dist["sr"] = (sr_vals < 3).astype(float)
return size, dist

View File

@@ -0,0 +1,46 @@
import scipy.stats
import numpy as np
from .population import Population
from .. import cells
class Tuberculoventral(Population):
type = "tuberculoventral"
def __init__(self, species="mouse", **kwds):
# Note that `cf` is the mean value used when selecting SGCs to connect;
# it is NOT the measured CF of the cell (although it should be close).
freqs = self._get_cf_array(species)
fields = [("cf", float), ("input_sr", list)]
super(Tuberculoventral, self).__init__(
species, len(freqs), fields=fields, **kwds
)
self._cells["cf"] = freqs
self._cells["input_sr"] = [np.tile([1.0, 1.0, 1.0], len(freqs))]
def create_cell(self, cell_rec):
""" Return a single new cell to be used in this population. The
*cell_rec* argument is the row from self.cells that describes the cell
to be created.
"""
return cells.Tuberculoventral.create(species=self.species, **self._cell_args)
def connection_stats(self, pop, cell_rec):
""" The population *pop* is being connected to the cell described in
*cell_rec*. Return the number of presynaptic cells that should be
connected and a dictionary of distributions used to select cells
from *pop*.
"""
size, dist = Population.connection_stats(self, pop, cell_rec)
from .. import populations
if isinstance(pop, populations.SGC):
# only select SGC inputs from low- and medium SR. See:
# Spectral Integration by Type II Interneurons in Dorsal Cochlear Nucleus
# George A. Spirou, Kevin A. Davis, Israel Nelken, Eric D. Young
# Journal of Neurophysiology Aug 1999, 82 (2) 648-663;
dist["sr"] = (pop.cells["sr"] < 2).astype(float)
return size, dist