Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -153,4 +153,8 @@ dmypy.json
.pytype/

# Cython debug symbols
cython_debug/
cython_debug/**/.DS_Store

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only need the latter one; will ignore anywhere


**/.DS_Store
*.pkl
cleo/test_store.py
5 changes: 5 additions & 0 deletions cleo/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,11 @@ def _default_registry(self):
_processing_net_op: NetworkOperation = field(default=None, init=False, repr=False)
_net_store_name: str = field(default="cleo default", init=False, repr=False)

@property

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This API is different now. CLSimulator simply contains a Registry. You need to merge from master to get latest changes

def registry(self):
"""Returns the DeviceInteractionRegistry for this simulator."""
return registry_for_sim(self)

def inject(
self, device: InterfaceDevice, *neuron_groups: NeuronGroup, **kwparams: Any
) -> CLSimulator:
Expand Down
56 changes: 48 additions & 8 deletions cleo/imaging/scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,21 @@

import warnings
from datetime import datetime
from typing import Callable

import matplotlib as mpl
import neo
import quantities as pq
from attrs import define, field, fields
from brian2 import NeuronGroup, Quantity, Unit, meter, mm, ms, np, um
from brian2 import NeuronGroup, Quantity, Unit, meter, mm, ms, np, um, second
from jaxtyping import Float, UInt
from matplotlib.artist import Artist
from mpl_toolkits.mplot3d import Axes3D
from numpy import pi

from cleo.base import Recorder
from cleo.coords import coords_from_ng
from cleo.imaging.sensors import Sensor
from cleo.light import Light, GaussianEllipsoid
from cleo.utilities import (
analog_signal,
normalize_coords,
Expand Down Expand Up @@ -102,30 +103,38 @@ class Scope(Recorder):
rho_rel_generator : Callable[[int], Float[np.ndarray, "n"], optional
A function assigning expression levels. Takes n as an arg, outputs float array.
``lambda n: np.ones(n)`` by default.
focus_depth : Quantity, optional
The depth of the focal plane, by default that of the scope.
soma_radius : Quantity, optional
The radius of the soma of the neuron, by default that of the scope.
Used to compute noise focus factor, since smaller ROIs will have
a noisier distribution of fluorescence, averaged over fewer pixels.
focus_depth : Quantity, optional
The depth of the focal plane, by default that of the scope.
soma_radius : Quantity, optional
The radius of the soma of the neuron, by default that of the scope.
Used to compute noise focus factor, since smaller ROIs will have
a noisier distribution of fluorescence, averaged over fewer pixels.
"""

sensor: Sensor = field()
img_width: Quantity = field()
"""The width (diameter) of the (circular) image captured by the microscope.
Specified in distance units."""

focus_depth: Quantity = None
"""The depth of the focal plane, with distance units"""

location: Quantity = [0, 0, 0] * mm
"""Location of the objective lens."""

direction: Float[np.ndarray, "3"] = field(
default=(0, 0, 1), converter=normalize_coords
)
"""Direction in which the microscope is pointing.
By default straight down (`+z` direction)"""

soma_radius: Quantity = field(default=10 * um)
"""Assumed radius of neurons, used to compute noise focus factor.
Smaller neurons have noisier signals."""

t_flyback: Quantity = field(default=0 * ms, kw_only=True)
"""Dead time per scan cycle for the scanning mirror to reset (flyback)."""

snr_cutoff: float = field(default=1)
"""SNR below which neurons are discarded.
Applied only when focus_depth is not None"""
Expand Down Expand Up @@ -157,6 +166,9 @@ class Scope(Recorder):
)
"""relative expression levels of neurons selected from each injection"""

imaging_light: Light = field(init=False, repr=False, default=None)
"""raster scanning parameters"""

@property
def n(self) -> int:
"""Number of imaged ROIs"""
Expand Down Expand Up @@ -314,6 +326,10 @@ def connect_to_neuron_group(self, neuron_group: NeuronGroup, **kwparams) -> None
self.sigma_per_injct.append(sigma_noise)
self.focus_coords_per_injct.append(focus_coords)
self.rho_rel_per_injct.append(rho_rel)
if self.imaging_light is not None:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we support separate img_width per injection? I don't think so, right? So the point here is to just to make sure that if the light is new, that img_width gets set properly? In that case, we'd have other variables that need to get set on the light, like frequency, width, power

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, since we are taking the shortcut of asking the user to inject the sensor after injecting into target neurons, I think we'd want to do the same with the light. In which case, we'd never have a light already created when we're injecting the scope into neurons

self.sim.registry.set_img_width(
self.imaging_light, self.img_width, self.soma_radius
)

def i_targets_for_neuron_group(self, neuron_group):
"""can handle multiple injections into same ng"""
Expand Down Expand Up @@ -451,3 +467,27 @@ def to_neo(self) -> neo.AnalogSignal:
)
return signal
# want ROI coordinates and size in image

# Method for optional raster scanning
def create_imaging_light(self, wavelength=920e-9 * meter, pulse_freq=30):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we said we wanted to keep the light tucked away, controlled by scope. So probably inject_imaging_light_for_targets() instead, and inject directly here

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Gemini chat talks about how to calculate the duty cycle/dwell time more accurately

spot_area = pi * self.soma_radius**2
img_width_area = pi * (self.img_width / 2) ** 2
effective_scan_time = (1 / pulse_freq) * second - self.t_flyback
pulse_width = spot_area / img_width_area * effective_scan_time
self.imaging_light = Light(
name=f"{self.name}_imlight",
light_model=GaussianEllipsoid(),
wavelength=wavelength, # user can pass in wavelength values
pulse_freq=pulse_freq, # user sets pulse freq when creating imaging light
pulse_stagger=True,
is_pulsed=True,
pulse_width=pulse_width,
)
return self.imaging_light

# keep img_width validator and update imaging width
@img_width.validator
def _update_img_width(self, attr, value):
# Make sure create_imaging_light has been called
if self.imaging_light is not None:
self.sim.registry.set_img_width(self.imaging_light, value, self.soma_radius)
42 changes: 32 additions & 10 deletions cleo/light/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,15 @@

import matplotlib as mpl
import quantities as pq
from attrs import define, field
from attrs import define, field, setters
from brian2 import (
NeuronGroup,
Subgroup,
np,
)
from brian2.units import (
Quantity,
mm,
mm2,
mwatt,
nmeter,
um,
)


from brian2.units import Quantity, mm, mm2, mwatt, nmeter, um, second, ms
from jaxtyping import Float
from matplotlib import colors
from matplotlib.artist import Artist
Expand Down Expand Up @@ -282,7 +277,22 @@ def _is_irr(q: Quantity) -> bool:
return q.has_same_dimensions(mwatt / mm2)


@define(eq=False)
def _sync_scan_param_to_registry(instance, attribute, value):
"""attrs on_setattr hook: pushes scan-related field changes down to the
Brian variable on light.source, if this Light is part of a running sim."""
if instance.sim is not None:
setter_name = f"set_{attribute.name}"
if hasattr(instance.sim.registry, setter_name):
getattr(instance.sim.registry, setter_name)(instance, value)
return value


@define(
eq=False,
on_setattr=setters.pipe(
setters.convert, setters.validate, _sync_scan_param_to_registry
),
)
class Light(Stimulator):
"""Delivers light to the network for photostimulation and (when implemented) imaging.

Expand Down Expand Up @@ -316,6 +326,18 @@ class Light(Stimulator):
wavelength: Quantity = field(default=473 * nmeter, kw_only=True)
"""light wavelength with unit (usually nmeter)"""

pulse_freq: float = field(default=30, kw_only=True)
"""Rate (Hz) at which the raster scan sweeps over target neurons."""

pulse_width: Quantity = field(default=0 * ms, kw_only=True)
"""Duration of each per-neuron light pulse during raster scanning."""

is_pulsed: bool = field(default=False, kw_only=True)
"""Whether this light delivers discrete pulses (raster scanning) vs. constant illumination."""

pulse_stagger: bool = field(default=False, kw_only=True)
"""Whether per-neuron pulses are staggered in time to avoid simultaneous activation."""

@coords.validator
def _check_coords(self, attribute, value):
if len(value.shape) != 2 or value.shape[1] != 3:
Expand Down
Empty file removed cleo/net.pkl
Empty file.
Binary file removed cleo/netstore.pkl
Binary file not shown.
10 changes: 10 additions & 0 deletions cleo/opto/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@
enphr3_3s,
gtacr2_4s,
vfchrimson_4s,
reachr_4s,
chrimsonr_4s,
cschrimson_4s,
breaches_4s,
chrmine_4s,
cheta_4s,
chronos_4s,
cheriff_4s,
nphr_3s,
jaws_3s,
)
from cleo.opto.opsins import (
BansalFourStateOpsin,
Expand Down
Loading