<jupyter_start><jupyter_text>Ball and stick 3: Extensible network of cells This is the third part of a tutorial series where we build a multicompartment cell and evolve it into a network of cells running on a parallel machine. In this part, we take the functionality of the ring network we constructed in the previous page and encapsulate it into various classes so that the network is more extensible. We also begin parameterizing the model so that particular values are not hard-coded, but remain variable so that the model is flexible.  Loading libraries As before, we will begin by loading relevant NEURON libraries:<jupyter_code>from neuron import h, gui
from neuron.units import ms, mV
h.load_file('stdrun.hoc')<jupyter_output><empty_output><jupyter_text>Generic Cell class In the last tutorial, we created a generic `Cell` class (actually, two versions) but we can expand this to make it more powerful. For example, let's make each Cell record its spike times, some membrane potential timeseries, and keep track of NetCons.<jupyter_code>class Cell:
    def __init__(self, gid, x, y, z, theta):
        self._gid = gid
        self._setup_morphology()
        self.all = self.soma.wholetree()
        self._setup_biophysics()
        self.x = self.y = self.z = 0
        h.define_shape()
        self._rotate_z(theta)
        self._set_position(x, y, z)
        
        # everything below here in this method is NEW
        self._spike_detector = h.NetCon(self.soma(0.5)._ref_v, None, sec=self.soma)
        self.spike_times = h.Vector()
        self._spike_detector.record(self.spike_times)
        
        self._ncs = []
        
        self.soma_v = h.Vector().record(self.soma(0.5)._ref_v)

        
    def __repr__(self):
        return '{}[{}]'.format(self.name, self._gid)
    
    def _set_position(self, x, y, z):
        for sec in self.all:
            for i in range(sec.n3d()):
                sec.pt3dchange(i,
                               x - self.x + sec.x3d(i),
                               y - self.y + sec.y3d(i),
                               z - self.z + sec.z3d(i),
                              sec.diam3d(i))
        self.x, self.y, self.z = x, y, z
        
    def _rotate_z(self, theta):
        """Rotate the cell about the Z axis."""
        for sec in self.all:
            for i in range(sec.n3d()):
                x = sec.x3d(i)
                y = sec.y3d(i)
                c = h.cos(theta)
                s = h.sin(theta)
                xprime = x * c - y * s
                yprime = x * s + y * c
                sec.pt3dchange(i, xprime, yprime, sec.z3d(i), sec.diam3d(i))<jupyter_output><empty_output><jupyter_text>Since the `BallAndStick` has a simple geometry, we could modify it to assume that all inputs go into a single location; we will call it the `.syn`.<jupyter_code>class BallAndStick(Cell):
    name = 'BallAndStick'
    
    def _setup_morphology(self):
        self.soma = h.Section(name='soma', cell=self)
        self.dend = h.Section(name='dend', cell=self)
        self.dend.connect(self.soma)
        self.soma.L = self.soma.diam = 12.6157
        self.dend.L = 200
        self.dend.diam = 1

    def _setup_biophysics(self):
        for sec in self.all:
            sec.Ra = 100    # Axial resistance in Ohm * cm
            sec.cm = 1      # Membrane capacitance in micro Farads / cm^2
        self.soma.insert('hh')                                          
        for seg in self.soma:
            seg.hh.gnabar = 0.12  # Sodium conductance in S/cm2
            seg.hh.gkbar = 0.036  # Potassium conductance in S/cm2
            seg.hh.gl = 0.0003    # Leak conductance in S/cm2
            seg.hh.el = -54.3     # Reversal potential in mV
        # Insert passive current in the dendrite
        self.dend.insert('pas')                 
        for seg in self.dend:
            seg.pas.g = 0.001  # Passive conductance in S/cm2
            seg.pas.e = -65    # Leak reversal potential mV

        # NEW: the synapse
        self.syn = h.ExpSyn(self.dend(0.5))
        self.syn.tau = 2 * ms<jupyter_output><empty_output><jupyter_text>Due to the nature of `h.ExpSyn` decay, there is mathematically no difference between having two ExpSyn objects at the same point or one synapse where multiple inputs add linearly, so it suffices to have just the one as long as we're happy with all inputs going into `dend(0.5)`.  Make a Ring class Encapsulating code into discrete objects is not only conceptually useful for code management, but as we know with cell objects, it lets us make several instances of the object for use in a network. Thinking ahead, we may very well need several networks – each network configured differently. This allows scripting of several simulations en masse, either in a for loop that sequentially processes the networks, or it can be used with NEURON's subworlds architecture in a parallel context.<jupyter_code>class Ring:
    """A network of *N* ball-and-stick cells where cell n makes an
    excitatory synapse onto cell n + 1 and the last, Nth cell in the
    network projects to the first cell.
    """
    def __init__(self, N=5, stim_w=0.04, stim_t=9, stim_delay=1, syn_w=0.01, syn_delay=5, r=50):
        """
        :param N: Number of cells.
        :param stim_w: Weight of the stimulus
        :param stim_t: time of the stimulus (in ms)
        :param stim_delay: delay of the stimulus (in ms)
        :param syn_w: Synaptic weight
        :param syn_delay: Delay of the synapse
        :param r: radius of the network
        """ 
        self._syn_w = syn_w
        self._syn_delay = syn_delay
        self._create_cells(N, r)
        self._connect_cells()
        # add stimulus
        self._netstim = h.NetStim()
        self._netstim.number = 1
        self._netstim.start = stim_t
        self._nc = h.NetCon(self._netstim, self.cells[0].syn)
        self._nc.delay = stim_delay
        self._nc.weight[0] = stim_w
    
    def _create_cells(self, N, r):
        self.cells = []
        for i in range(N):
            theta = i * 2 * h.PI / N
            self.cells.append(BallAndStick(i, h.cos(theta) * r, h.sin(theta) * r, 0, theta))
    
    def _connect_cells(self):
        for source, target in zip(self.cells, self.cells[1:] + [self.cells[0]]):
            nc = h.NetCon(source.soma(0.5)._ref_v, target.syn, sec=source.soma)
            nc.weight[0] = self._syn_w
            nc.delay = self._syn_delay
            source._ncs.append(nc)<jupyter_output><empty_output><jupyter_text>The `_create_cells` method is basically the same as the `create_n_BallAndStick` function in the previous part of the tutorial; the only difference is that the cells are stored in `self._cells` instead of being returned. `_connect_cells` is shorter than the previous version because it can take advantage of the existing synapses and lists.  Test the network Let's make a `Ring` object with 5 cells, render it using NEURON's built-in graphics, and run a simulation.<jupyter_code>ring = Ring(N=5)<jupyter_output><empty_output><jupyter_text>Now to check that it is constructed correctly:<jupyter_code>shape_window = h.PlotShape(True)
shape_window.show(0)<jupyter_output><empty_output><jupyter_text>Looks good so far; let's run the simulation and record time:<jupyter_code>t = h.Vector().record(h._ref_t)
h.finitialize(-65 * mV)
h.continuerun(100)<jupyter_output><empty_output><jupyter_text>Remember that if we are running in Jupyter to make a plot appear inline we must:<jupyter_code>%matplotlib inline<jupyter_output><empty_output><jupyter_text>Now plot the trace of cell 0's soma:<jupyter_code>import matplotlib.pyplot as plt
plt.plot(t, ring.cells[0].soma_v)
plt.show()<jupyter_output><empty_output><jupyter_text>Cell 0 looks good. Let's look at the raster diagram:<jupyter_code>plt.figure()
for i, cell in enumerate(ring.cells):
    plt.vlines(cell.spike_times, i + 0.5, i + 1.5)
plt.show()<jupyter_output><empty_output><jupyter_text>Explore effects of parameters Let's compare two simulations: one with the same parameters as above, which we'll plot in black, and one with half the synaptic weight, which we'll plot in red:<jupyter_code>plt.figure()
for syn_w, color in [(0.01, 'black'), (0.005, 'red')]:
    ring = Ring(N=5, syn_w=syn_w)
    h.finitialize(-65 * mV)
    h.continuerun(100 * ms)
    for i, cell in enumerate(ring.cells):
        plt.vlines(cell.spike_times, i + 0.5, i + 1.5, color=color)

plt.show()<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.09658352051069867, 'fraction_numerical': 0.013949639437285732, 'mean_word_length': 2.643410852713178, 'pattern_counts': {'":': 0, '<': 45, '<?xml version=': 0, '>': 45, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '11', 'dir': 'jupyter-structured-clean-dedup', 'id': '369880', 'license': 'BSD-3-Clause', 'n_tokens_mistral': 2850, 'n_tokens_neox': 2671, 'n_words': 873, 'path': 'docs/tutorials/ball-and-stick-3.ipynb', 'repo_name': 'phoenixdong/nrn', 'size': '8459'}