Skip to content

Raster Scanning Code - #55

Open
GlennZZY wants to merge 16 commits into
masterfrom
raster-scanning
Open

GlennZZY wants to merge 16 commits into
masterfrom
raster-scanning

Conversation

@GlennZZY

Copy link
Copy Markdown

Uploaded the code for raster scanning.

Since there's only one registry for simulation, the flag might not work at this moment

Comment thread cleo/imaging/scope.py Outdated
factory=list, repr=False, init=False
)
"""relative expression levels of neurons selected from each injection"""
Registry: DeviceInteractionRegistry = 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.

Are you sure we need to store the registry when we can just do registry_for_sim(self.sim)?

Comment thread cleo/imaging/scope.py Outdated
self.focus_coords_per_injct.append(focus_coords)
self.rho_rel_per_injct.append(rho_rel)
self.Registry = registry_for_sim(self.sim)
self.Registry.update_fov(self.img_width)

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.

would this need to be updated on every injection? Also, it can't be right that there's a single update_fov for the whole registry, since this should vary for each light device

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 Scope should optionally have an imaging_light object, and the img_width and scan_freq would be updated specific to that light

Comment thread cleo/registry.py

raster_fov: int = field(default=500 * 1e-6 * meter, kw_only=True)

raster_enable: int = field(default=0, kw_only=True)

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.

again, these would be at the level of individual lights, not the whole registry

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 is still the case. These would be Brian state variables inside the Light neuron group

Comment thread cleo/light/light.py Outdated
wavelength: Quantity = field(default=473 * nmeter, kw_only=True)
"""light wavelength with unit (usually nmeter)"""

scan_freq : int = field(default=30, kw_only=True)

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'd create a subclass RasterScanningLight to hide the scan_freq details from the typical Light object

Comment thread cleo/registry.py Outdated
i_source = self.subgroup_idx_for_light[light]
light_prop_syn.epsilon[i_source, :] = epsilon
light_prop_syn.T[i_source, :] = light.transmittance(coords_from_ng(ng)).ravel()
light_prop_syn.scan_period = second / light.scan_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.

I'd put these in a separate function that you call both here (on injection) and when you change the params on the scope/imaging light (setter functions). like update_raster_scan_params(scope) or something like that

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.

like you've already done with update_fov but can include these other variables too

@kjohnsen kjohnsen assigned kjohnsen and GlennZZY and unassigned kjohnsen Nov 20, 2024

@kjohnsen kjohnsen left a comment

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.

Looks pretty good! Besides the comments below, as I was about to test it (with the script at the bottom that plots all the action spectra), it occurred to me it's getting to be too much to check just by looking at the numbers. I think we need a new tutorial notebook that goes over all the opsins that will serve two puposes: (1) quickly introduce the user to all the options and (2) let us visually inspect that all the opsins are behaving as expected. Can you create a docs/tutorials/opsin_selection.ipynb notebook where we plot 4 things for every opsin?

  1. The 1P action spectrum,
  2. the 2P action spectrum,
  3. the impulse response (the photocurrent after a quick (e.g., 5 ms), strong pulse),
  4. and the pulse response (what the photocurrent looks like with prolonged stimulation (should see peak, then back down to plateau)

Since there are now too many opsins to visualize all at once, can you break this into groups? For example, plot all the ChR2 variants together, all the Chrimson variants together, all the inhibitory pumps together, all the ChRmine variants together?

Comment thread .DS_Store Outdated

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.

can we remove these? we should add **/.DS_Store to .gitignore

Comment thread cleo/opto/opsin_library.py Outdated
Comment thread cleo/opto/opsin_library.py Outdated
Comment thread cleo/opto/opsin_library.py Outdated
Comment thread cleo/opto/opsin_library.py Outdated
"metadata": {
"kernelspec": {
"display_name": "python3",
"display_name": ".venv",

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.

no need to commit this notebook if you didn't change it. Also, if you did change the notebook, use nbdev clean to get rid of stuff like this before committing

Comment thread cleo/opto/opsin_library.py Outdated
(490, 0.92),
(498, 1),
(516, 0.84),
(507, 0.90),

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.

here's another place the spectrum is out of order. For legibility and reproducibility, it should be in order, but I remembered I actually should have already accounted for this in light_dependence.py, line 125...don't know why I'm getting an error. Could you add a unit test for this? I guess it would be a new file tests/light/test_light_dependence.py

@kjohnsen kjohnsen left a comment

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 overall pattern of creating a light from the scope looks good! Just time to put it all together, make some tweaks, and most importantly, get it working in an end-to-end example and in unit tests

Comment thread cleo/imaging/scope.py Outdated
Comment thread cleo/imaging/scope.py Outdated
Comment thread cleo/imaging/scope.py Outdated
Comment thread cleo/light/light.py Outdated
def init_for_simulator(self, sim: CLSimulator) -> None:
registry = registry_for_sim(sim)
registry.init_register_light(self)
"""Test Code for setter method"""

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 assume you'd move this to test_scope.py?

Comment thread cleo/registry.py Outdated
Comment thread cleo/registry.py Outdated
Comment thread cleo/registry.py Outdated

@kjohnsen kjohnsen left a comment

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.

Good progress. Quite a bit of changes to make though. Summary of the main architectural change:

Variables that live on scope (attrs fields)

  • img_width
  • t_flyback

properties/setters on scope

  • scan_freq → pulse_freq on light
    • which in turn is property/setter to light.source neurons
  • dwell_time: read-only, i.e., property only, not a setter → light.pulse_width
    • which in turn is property/setter to light.source.pulse_width

properties/setters on light, pointing to light.source Brian variables

  • we don't expose these to the user on scope since they'd always be True
    • pulse_stagger
    • is_scanning (rename to is_pulsed)
  • these get called from different names on scope, as noted above
    • pulse_width
    • pulse_freq

For user-friendliness, since there'd no longer be fields on Scope with scanning variables scan_freq or t_flyback (I guess it's just those two, right?), we'd make these more discoverable to the user by making them documented keyword arguments of the inject_imaging_light_for_targets() function. img_width would already be defined, is_pulsed would automatically be true, and dwell_time and pulse_stagger would be computed automatically.

Now I was hoping to let the Brian light.source variables and equations be the single source of truth, but we have a mix of variables defined on the scope (like img_width) and ones defined on the light. So we do need something to keep the light variables in sync when img_width, t_flyback, or scan_freq are changed on scope.

To get that setter behavior with attrs fields, I recommend using on_setattr instead of the _field style you used:

import attrs

def sync_nested_c(instance, attrib, new_value):
    if hasattr(instance, "b") and hasattr(instance.b, "c"):
        instance.b.c.att = new_value
    return new_value

@attrs.define(on_setattr=sync_nested_c)
class Container:
    att: str = attrs.field(
        converter=str.strip,
        validator=attrs.validators.instance_of(str),
    )
    b: 'Containee'

I'd write a single function that computes dwell time, effective power, etc., and updates it all on the light (which in turn uses its setters to update on neurons) and use it with on_setattr as well as setters for the non-scope variables.

Comment thread cleo/imaging/scope.py
self.focus_coords_per_injct.append(focus_coords)
self.rho_rel_per_injct.append(rho_rel)
registry_for_sim(self.sim).update_fov(self.img_width)
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

Comment thread cleo/imaging/scope.py

# Method for optional raster scanning
def create_imaging_light(self):
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

Comment thread cleo/imaging/scope.py Outdated
Comment thread cleo/light/light.py Outdated
Comment thread cleo/light/light.py Outdated
"""light wavelength with unit (usually nmeter)"""

scan_freq : int = field(default=30, kw_only=True)
_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

Comment thread tests/imaging/test_scope.py Outdated
sim.inject(light, ng)
syn = list(sim.registry.light_prop_syns.values())[0]
print(list(syn.variables.keys()))
scope.is_scanning = True

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.

wouldn't want setter here; would always use scanning 2p light

if not is_scanning:
return

# check at multiple timepoints across one scan period

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 doesn't test that the actual package code is working, but rather that the equations here are working. need to actually run a mini-simulation and check results

src = sim.registry.source_for_light(light)
print(list(src.variables.keys()))

light.pulse_freq = 50

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.

these should probably be tested in test_light.py
with the changes accounting for dwell time, this tests will be trickier. will need to set scan_freq on scope. then dwell time will be computed from img width, flyback, soma radius, etc. to then set pulse_width, like mentioned previously

Comment thread tests/test_registry.py
from cleo.opto import Opsin, chr2_4s, vfchrimson_4s
from cleo.registry import registry_for_sim
from cleo.imaging.scope import Scope

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.

remember to use ruff formatting; should automatically sort/organize imports

Comment thread .gitignore

# Cython debug symbols
cython_debug/ No newline at end of file
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

@kjohnsen kjohnsen assigned randismaael and unassigned GlennZZY Jul 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants