A simple dendrite#

In this example, we will set up a single dendrite represented by a passive cable. On one end the dendrite is stimulated by a short current pulse. We will investigate how this pulse travels along the dendrite and calculate the conduction velocity.

Note

Concepts covered in this example:

  1. Creating a simulation recipe of a single dendrite.

  2. Placing probes on the morphology.

  3. Running the simulation and extracting the results.

  4. Investigating the influence of control volume policies.

The full code#

You can find the full code of the example at python/examples/single_cell_cable.py

Executing the script will run the simulation with default parameters.

Walk-through#

We set up a recipe for the simulation of a single dendrite with some parameters.


class Cable(A.recipe):
    def __init__(
        self,
        probes,
        Vm,
        length,
        radius,
        cm,
        rL,
        g,
        stimulus_start,
        stimulus_duration,
        stimulus_amplitude,
        cv_policy_max_extent,
    ):
        """
        probes -- list of probes

        Vm -- membrane leak potential
        length -- length of cable in μm
        radius -- radius of cable in μm
        cm -- membrane capacitance in F/m^2
        rL -- axial resistivity in Ω·cm
        g -- membrane conductivity in S/cm^2

        stimulus_start -- start of stimulus in ms
        stimulus_duration -- duration of stimulus in ms
        stimulus_amplitude -- amplitude of stimulus in nA

        cv_policy_max_extent -- maximum extent of control volume in μm
        """

        A.recipe.__init__(self)

        self.the_probes = probes

        self.Vm = Vm
        self.length = length
        self.radius = radius
        self.cm = cm
        self.rL = rL
        self.g = g

        self.stimulus_start = stimulus_start * U.ms
        self.stimulus_duration = stimulus_duration * U.ms
        self.stimulus_amplitude = stimulus_amplitude * U.nA

        self.cv_policy_max_extent = cv_policy_max_extent

        self.the_props = A.neuron_cable_properties()

    def num_cells(self):
        return 1

    def cell_kind(self, _):
        return A.cell_kind.cable

    def global_properties(self, _):
        return self.the_props

    def cell_description(self, _):
        """A high level description of the cell with global identifier gid.

        For example the morphology, synapses and ion channels required

Implementing the cell_description member function constructs the morphology and sets the properties of the dendrite as well as the current stimulus and the discretization policy.

        """

        tree = A.segment_tree()

        tree.append(
            A.mnpos,
            (0, 0, 0, self.radius),
            (self.length, 0, 0, self.radius),
            tag=1,
        )

        labels = A.label_dict({"cable": "(tag 1)", "start": "(location 0 0)"})

        decor = (
            A.decor()
            .set_property(
                Vm=self.Vm * U.mV, cm=self.cm * U.F / U.m2, rL=self.rL * U.Ohm * U.cm
            )
            .paint('"cable"', A.density(f"pas/e={self.Vm}", g=self.g))
            .place(
                '"start"',
                A.iclamp(
                    self.stimulus_start, self.stimulus_duration, self.stimulus_amplitude
                ),
                "iclamp",
            )
        )

        policy = A.cv_policy_max_extent(self.cv_policy_max_extent)
        decor.discretization(policy)

        return A.cable_cell(tree, decor, labels)

    def probes(self, _):
        return self.the_probes


We parse the command line arguments, instantiate the recipe, run the simulation, extract results and plot:


    parser.add_argument(
        "--Vm", help="membrane leak potential in mV", type=float, default=-65
    )
    parser.add_argument("--length", help="cable length in μm", type=float, default=1000)
    parser.add_argument("--radius", help="cable radius in μm", type=float, default=1)
    parser.add_argument(
        "--cm", help="membrane capacitance in F/m^2", type=float, default=0.01
    )
    parser.add_argument(
        "--rL", help="axial resistivity in Ω·cm", type=float, default=90
    )
    parser.add_argument(
        "--g", help="membrane conductivity in S/cm^2", type=float, default=0.001
    )

    parser.add_argument(
        "--stimulus_start", help="start of stimulus in ms", type=float, default=10
    )
    parser.add_argument(
        "--stimulus_duration",
        help="duration of stimulus in ms",
        type=float,
        default=0.1,
    )
    parser.add_argument(
        "--stimulus_amplitude",
        help="amplitude of stimulus in nA",
        type=float,
        default=1,
    )

    parser.add_argument(
        "--cv_policy_max_extent",
        help="maximum extent of control volume in μm",
        type=float,
        default=10,
    )

    # parse the command line arguments
    args = parser.parse_args()

    # set up membrane voltage probes equidistantly along the dendrites
    probe_locations = [
        (f"(location 0 {r})", f"Um-(0, {r})") for r in np.linspace(0, 1, 11)
    ]
    probes = [A.cable_probe_membrane_voltage(loc, tag) for loc, tag in probe_locations]
    recipe = Cable(probes, **vars(args))

    # configure the simulation and handles for the probes
    sim = A.simulation(recipe)
    dt = 1 * U.us
    handles = [
        sim.sample((0, tag), A.regular_schedule(dt)) for _, tag in probe_locations
    ]

    # run the simulation for 30 ms
    sim.run(tfinal=30 * U.ms, dt=dt)

    # retrieve the sampled membrane voltages and convert to a pd DataFrame
    print("Plotting results ...")
    df_list = []
    for probe in range(len(handles)):
        samples, meta = sim.samples(handles[probe])[0]
        df_list.append(
            pd.DataFrame(
                {"t/ms": samples[:, 0], "U/mV": samples[:, 1], "Probe": f"{probe}"}
            )
        )

    df = pd.concat(df_list, ignore_index=True)
    sns.relplot(
        data=df, kind="line", x="t/ms", y="U/mV", hue="Probe", errorbar=None
    ).set(xlim=(9, 14)).savefig("single_cell_cable_result.svg")

    # calculcate the idealized conduction velocity, cf. cable equation
    data = [sim.samples(handle)[0][0] for handle in handles]

The output plot below shows how the current pulse is attenuated and gets broader the further it travels along the dendrite from the current clamp.

../_images/single_cell_cable_result.svg

The conduction velocity in simulation is calculated from the time of the maximum of the membrane potential.

    taum = get_taum(args.cm, rm)
    lambdam = get_lambdam(args.radius * 1e-6, rm, args.rL * 0.01)
    vcond_ideal = get_vcond(lambdam, taum)

    # take the last and second probe
    delta_t = get_tmax(data[-1]) - get_tmax(data[1])

    # 90% because we took the second probe
    delta_x = args.length * 0.9
    vcond = delta_x * 1e-6 / (delta_t * 1e-3)

    print(f"calculated conduction velocity: {vcond_ideal:.2f} m/s")
    print(f"simulated conduction velocity:  {vcond:.2f} m/s")

Keep in mind that the calculated (idealized) conduction velocity is only correct for an infinite cable.

calculated conduction velocity: 0.47 m/s
simulated conduction velocity:  0.50 m/s

When we set the control volume policy to cover the full dendrite, all probes will see the same voltage.

./single_cell_cable.py --length 1000 --cv_policy_max_extent 1000

Output:

../_images/single_cell_cable_result_cv_policy.svg