Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -146,4 +146,6 @@ 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
5 changes: 5 additions & 0 deletions cleo/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,11 @@ class CLSimulator(NeoExportable):
_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
48 changes: 46 additions & 2 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 @@ -157,6 +158,11 @@ 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"""

_is_scanning: bool = field(default=False, init=False, repr=False)

@property
def n(self) -> int:
"""Number of imaged ROIs"""
Expand Down Expand Up @@ -314,6 +320,8 @@ 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)

def i_targets_for_neuron_group(self, neuron_group):
"""can handle multiple injections into same ng"""
Expand Down Expand Up @@ -451,3 +459,39 @@ 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
pulse_width = spot_area / img_width_area * (1 / pulse_freq) * second
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_scanning=True,
pulse_width=pulse_width,
)
# attach soma_radius for registry use
self.imaging_light.soma_radius = self.soma_radius
return self.imaging_light

# turn raster scanning on/off
@property
def is_scanning(self):
return self._is_scanning

@is_scanning.setter
Comment thread
randismaael marked this conversation as resolved.
Outdated
def is_scanning(self, flag: bool):
self._is_scanning = flag
if self.imaging_light is not None:
self.sim.registry.set_is_scanning(self.imaging_light, flag)

# 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)
41 changes: 41 additions & 0 deletions cleo/light/light.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Contains Light device and propagation models"""

from __future__ import annotations

import datetime
Expand All @@ -14,13 +15,17 @@
Subgroup,
np,
)

from brian2 import ms
Comment thread
randismaael marked this conversation as resolved.
Outdated

from brian2.units import (
Quantity,
mm,
mm2,
mwatt,
nmeter,
um,
second,
)
from jaxtyping import Float
from matplotlib import colors
Expand Down Expand Up @@ -316,6 +321,42 @@ 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, alias="pulse_freq")

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.

rather than store these as fields on the scope, let's let them be purely derived from the Light. so in the @property, it'd be sth like return self.imaging_light.source...

So while the interface lives here on scope, the single source of truth would be the light itself (and for that, it will often be the source neurons that instantiate the light, so another layer of properties where needed)

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.

and remember from the point of view of imaging, we want to call this scan_freq, then under-the-hood what gets changed on the light is pulse_freq and pulse_width. The latter of which is calculated from dwell time, which is calculated from img_width, pulse_freq, soma_radius, and t_flyback

_pulse_width: Quantity = field(default=0 * ms, kw_only=True, alias="pulse_width")

is_scanning: bool = field(default=False, kw_only=True)
_pulse_stagger: bool = field(default=False, kw_only=True, alias="pulse_stagger")

@property
def pulse_freq(self):
return self._pulse_freq

@pulse_freq.setter
def pulse_freq(self, hz: float):
self._pulse_freq = hz
if self.sim is not None:
registry_for_sim(self.sim).set_pulse_freq(self, hz)

@property
def pulse_width(self):
return self._pulse_width

@pulse_width.setter
def pulse_width(self, width: Quantity):
self._pulse_width = width
if self.sim is not None:
registry_for_sim(self.sim).set_pulse_width(self, width)

@property
def pulse_stagger(self):
return self._pulse_stagger

@pulse_stagger.setter

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.

We can assume the pulses would always be staggered for imaging right? so we don't need this here. This would need to be an option for Light though, and correctly set from here.

def pulse_stagger(self, stagger: bool):
self._pulse_stagger = stagger
if self.sim is not None:
registry_for_sim(self.sim).set_pulse_stagger(self, stagger)

@coords.validator
def _check_coords(self, attribute, value):
if len(value.shape) != 2 or value.shape[1] != 3:
Expand Down
Loading