Multi-channel, bidirectional optogenetics#
Cleo supports the simultaneous use of multiple Light
devices, multiple channels per device, and multiple opsins per neuron group.
Here’ we’ll see how to use all these features.
# boilerplate
%load_ext autoreload
%autoreload 2
from brian2 import *
import matplotlib.pyplot as plt
import cleo
from cleo import *
cleo.utilities.style_plots_for_docs()
# numpy faster than cython for lightweight example
prefs.codegen.target = 'numpy'
# for reproducibility
np.random.seed(1866)
# colors
c = {
'main': '#C500CC',
'473nm': '#72b5f2',
'590nm': (1, .875, 0),
}
The autoreload extension is already loaded. To reload it, use:
%reload_ext autoreload
Network setup#
We’ll use excitatory and inhibitory populations of exponential integrate-and-fire neurons.
n = 500
ng = NeuronGroup(
n,
"""
dv/dt = (
-(v - E_L)
+ Delta_T*exp((v-theta)/Delta_T)
+ Rm*(I_exc + I_inh + I_bg)
) / tau_m + sigma*sqrt(2/tau_m)*xi: volt
I_exc : amp
I_inh : amp
""",
threshold="v > -10*mV",
reset="v=E_L",
namespace={
"tau_m": 20 * ms,
"Rm": 500 * Mohm,
"theta": -50 * mV,
"Delta_T": 2 * mV,
"E_L": -70*mV,
"sigma": 5 * mV,
"I_bg": 60 * pamp,
},
)
ng.v = np.random.uniform(-70, -50, n) * mV
W = 1 * mV
p_S = 0.3
n_neighbors = 40
S_ee = Synapses(ng, model='w: 1', on_pre="v_post+=W*w/sqrt(N)")
S_ee.connect(condition='abs(i-j)<=n_neighbors and i!=j')
S_ee.w = np.exp(np.random.randn(int(S_ee.N - 0))) * np.random.choice([-1, 1], int(S_ee.N - 0))
spike_mon = SpikeMonitor(ng)
# for visualizing currents
Imon = StateMonitor(ng, ('I_exc', 'I_inh'), record=range(50))
net = Network(ng, S_ee, spike_mon, Imon)
sim = cleo.CLSimulator(net)
from cleo.coords import assign_coords_grid_rect_prism
xmax_mm = 2
assign_coords_grid_rect_prism(ng, xlim=(0, xmax_mm), ylim=(-.15, .15), zlim=(0, .3), shape=(20, 5, 5))
cleo.viz.plot(ng, colors=[c['main']])
(<Figure size 640x480 with 1 Axes>,
<Axes3D: xlabel='x (um)', ylabel='y (um)', zlabel='z (um)'>)

Injecting a multi-channel Light
#
A Light
device can have multiple channels; all the user needs is to specify the coordinates (and optionally direction) of each light source (channel).
A LightModel
(e.g., that of an optical fiber) defines how light propagates from each source.
Here we inject 590 nm light for activating Vf-Chrimson. Lacking a more rigorous quantification, we assume absorption and scattering coefficients of 590 nm light in the brain are roughly 0.8 times that of 470 nm light (see Jacques 2013 for some justification).
from cleo.light import Light, OpticFiber
from cleo.opto import vfchrimson_4s
n_fibers = 4
coords = np.zeros((n_fibers, 3))
end_space = 1 / (2 * n_fibers)
coords[:, 0] = np.linspace(end_space, 1 - end_space, n_fibers) * xmax_mm
coords[:, 1] = -0.025
amber_fibers = Light(
name="amber fibers",
coords=coords * mm,
light_model=OpticFiber(K=0.125 * 0.8 / mm, S=7.37 * 0.8 / mm),
wavelength=590 * nmeter,
)
sim.inject(amber_fibers, ng)
vfc = vfchrimson_4s()
sim.inject(vfc, ng, Iopto_var_name="I_exc")
cleo.viz.plot(ng, colors=[c["main"]], sim=sim)
(<Figure size 640x480 with 1 Axes>,
<Axes3D: xlabel='x (um)', ylabel='y (um)', zlabel='z (um)'>)

Bidirectional control via a second opsin#
Here we will demonstrate increasing and decreasing activity in the same experiment by injecting an inhibitory opsin with a minimally overlapping activation spectrum. Alternatively, we could achieve bidirectional control with excitatory opsins on excitatory and inhibitory neurons.
We will use the anion channel GtACR2 which is maximally activated by 470 nm light.
Let’s first visualize how the action spectra for the two opsins overlap:
gtacr2 = cleo.opto.gtacr2_4s()
cleo.light.plot_spectra(vfc, gtacr2)
(<Figure size 640x480 with 1 Axes>,
<Axes: title={'center': 'Action/excitation spectra'}, xlabel='λ (nm)', ylabel='ε'>)

We see that GtACR2 will be totally unaffected by amber (590 nm) light, while Vf-Chrimson will be slightly activated by blue (470 nm) light.
coords[:, 1] = 0.025
blue_fibers = Light(
name="blue fibers",
coords=coords * mm,
light_model=cleo.light.fiber473nm(),
)
sim.inject(blue_fibers, ng)
sim.inject(gtacr2, ng, Iopto_var_name="I_inh")
cleo.viz.plot(ng, colors=[c["main"]], sim=sim)
WARNING /home/kyle/Dropbox (GaTech)/projects/cleo/cleo/light/light_dependence.py:107: UserWarning: λ = 590.0 nm is outside the range of the action spectrum data for GtACR2. Assuming ε = 0.
warnings.warn(
[py.warnings]
(<Figure size 640x480 with 1 Axes>,
<Axes3D: xlabel='x (um)', ylabel='y (um)', zlabel='z (um)'>)

Open-loop stimulation#
We will now design a stimulus pattern to demonstrate bidirectional control segregated by channel.
from cleo.ioproc import LatencyIOProcessor
class OpenLoopOpto(LatencyIOProcessor):
def __init__(self):
super().__init__(sample_period_ms=1)
# since this is open-loop, we don't use state_dict
def process(self, state_dict, time_ms):
amplitude_mW_mm2 = 1
time_offsets = np.array([0, -20, -40, -60])
t = time_ms + time_offsets
amber = ((t >= 20) & (t < 40)) * amplitude_mW_mm2
blue = ((t >= 60) & (t < 63)) * amplitude_mW_mm2
# return output dict and time
return ({"amber fibers": amber, "blue fibers": blue}, time_ms)
sim.set_io_processor(OpenLoopOpto())
CLSimulator(io_processor=<__main__.OpenLoopOpto object at 0x7fe758a361d0>, devices={BansalFourStateOpsin(sim=..., name='GtACR2', save_history=True, on_pre='', spectrum=[(400, 0.4), (410, 0.49), (420, 0.56), (430, 0.65), (440, 0.82), (450, 0.88), (460, 0.88), (470, 1.0), (480, 0.91), (490, 0.67), (500, 0.41), (510, 0.21), (520, 0.12), (530, 0.06), (540, 0.02), (550, 0.0), (560, 0.0)], required_vars=[('Iopto', amp), ('v', volt)], Gd1=17. * hertz, Gd2=10. * hertz, Gr0=0.58 * hertz, g0=44. * nsiemens, phim=2.e+23 * (second ** -1) / (meter ** 2), k1=40. * khertz, k2=20. * khertz, Gf0=1. * hertz, Gb0=3. * hertz, kf=1. * hertz, kb=5. * hertz, gamma=0.05, p=1, q=0.1, E=-69.5 * mvolt, model='\n dC1/dt = Gd1*O1 + Gr0*C2 - Ga1*C1 : 1 (clock-driven)\n dO1/dt = Ga1*C1 + Gb*O2 - (Gd1+Gf)*O1 : 1 (clock-driven)\n dO2/dt = Ga2*C2 + Gf*O1 - (Gd2+Gb)*O2 : 1 (clock-driven)\n C2 = 1 - C1 - O1 - O2 : 1\n\n Theta = int(phi_pre > 0*phi_pre) : 1\n Hp = Theta * phi_pre**p/(phi_pre**p + phim**p) : 1\n Ga1 = k1*Hp : hertz\n Ga2 = k2*Hp : hertz\n Hq = Theta * phi_pre**q/(phi_pre**q + phim**q) : 1\n Gf = kf*Hq + Gf0 : hertz\n Gb = kb*Hq + Gb0 : hertz\n\n fphi = O1 + gamma*O2 : 1\n\n IOPTO_VAR_NAME_post = -g0*fphi*(V_VAR_NAME_post-E)*rho_rel : ampere (summed)\n rho_rel : 1'), Light(sim=..., name='amber fibers', save_history=True, value=array([0., 0., 0., 0.]), light_model=OpticFiber(R0=100. * umetre, NAfib=0.37, K=100. * metre ** -1, S=5896. * metre ** -1, ntis=1.36), wavelength=0.59 * umetre, direction=array([0., 0., 1.]), max_Irr0_mW_per_mm2=None, max_Irr0_mW_per_mm2_viz=None), BansalFourStateOpsin(sim=..., name='VfChrimson', save_history=True, on_pre='', spectrum=[(470.0, 0.4123404255319149), (490.0, 0.593265306122449), (510.0, 0.7935294117647058), (530.0, 0.8066037735849055), (550.0, 0.8912727272727272), (570.0, 1.0), (590.0, 0.9661016949152542), (610.0, 0.7475409836065574), (630.0, 0.4342857142857143)], required_vars=[('Iopto', amp), ('v', volt)], Gd1=0.37 * khertz, Gd2=175. * hertz, Gr0=0.667 * mhertz, g0=17.5 * nsiemens, phim=1.5e+22 * (second ** -1) / (meter ** 2), k1=3. * khertz, k2=200. * hertz, Gf0=20. * hertz, Gb0=3.2 * hertz, kf=10. * hertz, kb=10. * hertz, gamma=0.05, p=1, q=1, E=0. * volt, model='\n dC1/dt = Gd1*O1 + Gr0*C2 - Ga1*C1 : 1 (clock-driven)\n dO1/dt = Ga1*C1 + Gb*O2 - (Gd1+Gf)*O1 : 1 (clock-driven)\n dO2/dt = Ga2*C2 + Gf*O1 - (Gd2+Gb)*O2 : 1 (clock-driven)\n C2 = 1 - C1 - O1 - O2 : 1\n\n Theta = int(phi_pre > 0*phi_pre) : 1\n Hp = Theta * phi_pre**p/(phi_pre**p + phim**p) : 1\n Ga1 = k1*Hp : hertz\n Ga2 = k2*Hp : hertz\n Hq = Theta * phi_pre**q/(phi_pre**q + phim**q) : 1\n Gf = kf*Hq + Gf0 : hertz\n Gb = kb*Hq + Gb0 : hertz\n\n fphi = O1 + gamma*O2 : 1\n\n IOPTO_VAR_NAME_post = -g0*fphi*(V_VAR_NAME_post-E)*rho_rel : ampere (summed)\n rho_rel : 1'), Light(sim=..., name='blue fibers', save_history=True, value=array([0., 0., 0., 0.]), light_model=OpticFiber(R0=100. * umetre, NAfib=0.37, K=125. * metre ** -1, S=7370. * metre ** -1, ntis=1.36), wavelength=0.473 * umetre, direction=array([0., 0., 1.]), max_Irr0_mW_per_mm2=None, max_Irr0_mW_per_mm2_viz=None)})
Run simulation and plot results#
sim.reset()
sim.run(200*ms)
INFO No numerical integration method specified for group 'neurongroup', using method 'euler' (took 0.15s, trying other methods took 0.00s). [brian2.stateupdaters.base.method_choice]
INFO No numerical integration method specified for group 'syn_GtACR2_neurongroup', using method 'euler' (took 0.02s, trying other methods took 0.06s). [brian2.stateupdaters.base.method_choice]
INFO No numerical integration method specified for group 'syn_VfChrimson_neurongroup', using method 'euler' (took 0.01s, trying other methods took 0.01s). [brian2.stateupdaters.base.method_choice]
fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True)
ax1.plot(spike_mon.t / ms, spike_mon.i[:], ",")
ax1.set(ylabel="neuron index", title="spiking")
ax2.step(
blue_fibers.t_ms, blue_fibers.values + np.arange(n_fibers) * 1.3 + 0.1, c=c["473nm"]
)
ax2.step(amber_fibers.t_ms, amber_fibers.values + np.arange(n_fibers) * 1.3, c=c["590nm"])
ax2.set(
yticks=[],
ylabel="intensity per channel",
title="photostimulation",
xlabel="time (ms)",
)
[[],
Text(0, 0.5, 'intensity per channel'),
Text(0.5, 1.0, 'photostimulation'),
Text(0.5, 0, 'time (ms)')]

As you can see, Vf-Chrimson has fast dynamics, enabling high-frequency control. GtACR2, on the other hand, continues acting long after the light is removed due to its slow deactivation kinetics. We can confirm this by plotting the current from each of the opsins in the first segment of neurons:
fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True)
ax1.plot(Imon.t / ms, Imon.I_exc.T / namp, lw=0.2, c=c["590nm"])
ax1.set(title="Vf-Chrimson current", ylabel="$I_{exc}$ (nA)")
ax2.plot(Imon.t / ms, Imon.I_inh.T / namp, lw=0.2, c=c["473nm"])
ax2.set(title="GtACR2 current", ylabel="$I_{inh}$ (nA)")
[Text(0.5, 1.0, 'GtACR2 current'), Text(0, 0.5, '$I_{inh}$ (nA)')]

We can also confirm that blue light has a non-negligible effect on Vf-Chrimson.
Conclusion#
As a recap, in this tutorial we’ve seen how to:
configure a multi-channel
Light
device,use more than one of them simultaneously, and
inject multiple opsins of overlapping action spectra into the same network.