Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
14aa1b8
Add OptogenSIMLight prototype and compressed 4D dataset
akhilsnarra-ui Jul 11, 2026
57bfebe
Store dataset as gzipped NetCDF3 (scipy backend, no new dependency)
akhilsnarra-ui Jul 15, 2026
3d89dd8
Load packaged data via importlib.resources; include data file in Poet…
akhilsnarra-ui Jul 16, 2026
3abb054
Add OptogenSIMLight to light visualization notebook
akhilsnarra-ui Jul 18, 2026
3db06a3
Add dedicated OptogenSIMLight transmittance test and viz_params cover…
akhilsnarra-ui Jul 20, 2026
9317a61
Add OptogenSIMLight demonstration to multi-opto tutorial
akhilsnarra-ui Jul 21, 2026
bed38a7
Expose real backscatter behind source in OptogenSIMLight
akhilsnarra-ui Jul 25, 2026
df44ed5
Regenerate dataset with per-wavelength tissue fix
akhilsnarra-ui Jul 25, 2026
4629a3f
Re-run light_viz notebook with regenerated dataset
akhilsnarra-ui Jul 25, 2026
fa6aeed
Replace OpticFiber with OptogenSIMLight in multi-opto tutorial
akhilsnarra-ui Jul 25, 2026
e684943
Rename optogensim_light.py to optogensim.py per review
akhilsnarra-ui Aug 5, 2026
80fb94e
Clean notebooks: remove change markers, strip execution counts, fresh…
akhilsnarra-ui Aug 5, 2026
f2a58d6
Restructure OptogenSIM: data as field, derived properties, add defocu…
akhilsnarra-ui Aug 6, 2026
2329245
Re-run notebooks against renamed OptogenSIM class
akhilsnarra-ui Aug 6, 2026
e26da6c
Update OptogenSIM reference in profile explorer notebook
akhilsnarra-ui Aug 6, 2026
06efe53
Add OptogenSIM to transmittance test; add beam_radius and wavelength …
akhilsnarra-ui Aug 6, 2026
62c6ea9
Tune multi_opto light amplitude to 0.6 mW/mm2 for OptogenSIM
akhilsnarra-ui Aug 6, 2026
aa6acab
Reduce docstring duplication in defocused_gaussian_beam
akhilsnarra-ui Aug 6, 2026
069a4fb
Remove unused enphr3 opsin from multi_opto tutorial
akhilsnarra-ui Aug 6, 2026
f88c835
Move wavelength from Light to LightModel
akhilsnarra-ui Aug 7, 2026
56d081b
Fix two_photon call site missed in wavelength refactor
akhilsnarra-ui Aug 8, 2026
3cf2aec
Address review on optogensim.py + fix light_viz wavelength calls
akhilsnarra-ui Aug 14, 2026
d280664
Extend beam-width transmittance test to fiber and GaussianEllipsoid
akhilsnarra-ui Aug 17, 2026
8b2fc0e
Apply ruff formatting to optogensim.py
akhilsnarra-ui Aug 18, 2026
81dd5e5
Tune multi_opto amplitude to 0.45 and use np.ptp for trace spacing
akhilsnarra-ui Aug 19, 2026
47a6540
Use OptogenSIM LightModel in profile explorer; fix heatmap aspect ratio
akhilsnarra-ui Aug 21, 2026
838a676
Add OptogenSIM fork link to data docstring
akhilsnarra-ui Aug 26, 2026
d6319a0
Silence brian2 warning for discarded light-source objects
akhilsnarra-ui Aug 28, 2026
9949c01
tweak visualization
kjohnsen Aug 31, 2026
5e10b0e
format tests
kjohnsen Aug 31, 2026
b42b63b
Merge remote-tracking branch 'origin/master' into light-model-improve…
akhilsnarra-ui Sep 5, 2026
3b60daf
Address final review comments
akhilsnarra-ui Sep 12, 2026
9b0e017
Warn when OptogenSIM targets fall outside the data range
akhilsnarra-ui Sep 19, 2026
24e74f1
Zero OptogenSIM transmittance outside data range
kjohnsen Oct 3, 2026
5bdc7b4
Fix CI: drop setup-python, let setup-uv provide Python
kjohnsen Oct 3, 2026
8ff3df5
Test OptogenSIM packaged data is peak-normalized to 1
kjohnsen Oct 3, 2026
e51e7ff
Use setup-uv@v7 (no v10 major tag exists)
kjohnsen Oct 3, 2026
3213f9e
CI: prefer uv-managed Python (runner Pythons are PEP 668 locked)
kjohnsen Oct 3, 2026
4fc4657
CI: setup-uv v7 cancels on all runners, use known-good v5
kjohnsen Oct 3, 2026
193d00e
CI: install into uv venv instead of system python
kjohnsen Oct 3, 2026
69c46e4
CI: drop empty env block (invalid workflow)
kjohnsen Oct 3, 2026
6e8b96b
CI: setup-uv already provides .venv, drop manual uv venv
kjohnsen Oct 3, 2026
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 @@ -153,4 +153,6 @@ dmypy.json
.pytype/

# Cython debug symbols
cython_debug/
cython_debug/
.DS_Store
**/.DS_Store
1 change: 1 addition & 0 deletions cleo/light/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
OpticFiber,
fiber473nm,
)
from cleo.light.optogensim_light import OptogenSIMLight
from cleo.light.light_dependence import (
LightDependent,
cubic_interpolator,
Expand Down
Empty file added cleo/light/data/__init__.py
Empty file.
Binary file added cleo/light/data/light_model_4d.nc.gz
Binary file not shown.
91 changes: 91 additions & 0 deletions cleo/light/optogensim_light.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import xarray as xr
import numpy as np
from attrs import define, field
from brian2.units import Quantity, nmeter, um, cm, mm
from jaxtyping import Float
from cleo.light.light import LightModel
from cleo.utilities import uniform_cylinder_rθz, xyz_from_rθz
from cleo.coords import coords_from_xyz
from importlib.resources import files


@define
class OptogenSIMLight(LightModel):
"""Light model from OptogenSIM Monte Carlo simulations."""

wavelength: Quantity = 473 * nmeter
"""Light wavelength. Must be within the simulated grid range."""
beam_radius: Quantity = 100 * um
"""Beam (1/e^2) radius. Must be within the simulated grid range."""
data_path: str = None
"""Path to the 4D dataset. If None, uses the dataset packaged with Cleo."""
_rz_slice: object = field(init=False, default=None, repr=False)
_r_range: object = field(init=False, default=None, repr=False)
_z_range: object = field(init=False, default=None, repr=False)

def __attrs_post_init__(self):
if self.data_path is None:
path = str(files("cleo.light.data") / "light_model_4d.nc.gz")
else:
path = self.data_path
rz = xr.open_dataarray(path, engine="scipy").interp(
wavelength=self.wavelength / nmeter, beam_size=self.beam_radius / um
)
# data measures z from the atlas top; re-zero to the source
z_source = float(rz.isel(r=0).z[rz.isel(r=0).argmax("z")])
self._rz_slice = rz.assign_coords(z=rz.z - z_source)
self._r_range = (float(self._rz_slice.r.min()), float(self._rz_slice.r.max()))
self._z_range = (float(self._rz_slice.z.min()), float(self._rz_slice.z.max()))

def transmittance(self, source_coords, source_dir_uvec, target_coords):
assert np.allclose(np.linalg.norm(source_dir_uvec, axis=-1), 1)
r, z = self._get_rz_for_xyz(source_coords, source_dir_uvec, target_coords)
r_cm = np.asarray(r / cm)
z_cm = np.asarray(z / cm)
T = self._rz_slice.interp(
r=xr.DataArray(np.clip(r_cm, *self._r_range)),
z=xr.DataArray(np.clip(z_cm, *self._z_range)),
).values
T = np.nan_to_num(T, nan=0.0)
T[z_cm < self._z_range[0]] = 0 # zero only beyond data range; keep real backscatter within it
return T

@property
def area0(self):
return np.pi * self.beam_radius**2

def viz_params(self, coords, direction, T_threshold,
n_points_per_source=4000, **kwargs):
r_thresh, zc_thresh = self._find_rz_thresholds(T_threshold)
r, theta, zc = uniform_cylinder_rθz(n_points_per_source, r_thresh, zc_thresh)
end = coords + zc_thresh * direction
x, y, z = xyz_from_rθz(r, theta, zc, coords, end)
density_factor = 3
cyl_vol = np.pi * r_thresh**2 * zc_thresh
markersize = (cyl_vol / n_points_per_source * density_factor) ** (1 / 3)
intensity_scale = 1.5 * (4e3 / n_points_per_source) ** (1 / 3)
return coords_from_xyz(x, y, z), markersize, intensity_scale

def _find_rz_thresholds(self, thresh):
"""Find r and z extents for visualization at the given transmittance threshold."""
# On-axis (r[0]) profile along z (re-zeroed so source at z=0).
on_axis = self._rz_slice.isel(r=0)
z_vals = on_axis.z.values # cm, source at 0
T_z = on_axis.values
# Only look in the forward (z >= 0) direction.
mask = z_vals >= 0
z_pos = z_vals[mask]
T_pos = T_z[mask]
below = np.where(T_pos < thresh)[0]
zc_thresh = (z_pos[below[0]] if len(below) > 0 else z_pos[-1]) * cm

# Find r threshold at z = zc_thresh / 2.
z_mid = float(zc_thresh / cm) / 2
z_mid = np.clip(z_mid, float(self._rz_slice.z.min()),
float(self._rz_slice.z.max()))
r_vals = self._rz_slice.r.values
T_r = self._rz_slice.interp(z=float(z_mid)).values
below_r = np.where(T_r < thresh)[0]
r_thresh = (r_vals[below_r[0]] if len(below_r) > 0 else r_vals[-1]) * cm

return r_thresh * 1.2, zc_thresh
98 changes: 56 additions & 42 deletions docs/tutorials/multi_opto.ipynb
Comment thread
akhilsnarra-ui marked this conversation as resolved.
Comment thread
kjohnsen marked this conversation as resolved.
Comment thread
kjohnsen marked this conversation as resolved.
Comment thread
kjohnsen marked this conversation as resolved.

Large diffs are not rendered by default.

268 changes: 229 additions & 39 deletions notebooks/light_viz.ipynb
Comment thread
kjohnsen marked this conversation as resolved.

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ dev = [

[tool.poetry]
packages = [{ include = "cleo" }]
include = [{ path = "cleo/light/data/*.nc.gz", format = ["sdist", "wheel"] }]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Science/Research",
Expand Down
41 changes: 39 additions & 2 deletions tests/light/test_light.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from brian2 import Network, NeuronGroup, asarray, mm, mm2, ms, mwatt, nmeter, np, um

import cleo
from cleo.light import GaussianEllipsoid, KoehlerBeam, Light, LightModel, fiber473nm
from cleo.light import GaussianEllipsoid, KoehlerBeam, Light, LightModel, OptogenSIMLight, fiber473nm
from cleo.utilities import normalize_coords, unit_safe_allclose


Expand Down Expand Up @@ -101,6 +101,43 @@ def test_OpticFiber():
assert np.all(T == 0)

Comment thread
akhilsnarra-ui marked this conversation as resolved.

Comment thread
akhilsnarra-ui marked this conversation as resolved.
def test_OptogenSIMLight():
from brian2 import mm2, cm
Comment thread
kjohnsen marked this conversation as resolved.
Outdated

model = OptogenSIMLight() # defaults: 473 nm, 100 um beam radius
source_coords = np.array([0, 0, 0]) * mm
source_direction = normalize_coords([0, 0, 1]) # pointing +z into tissue

# transmittance decreases with depth along the beam axis (within range)
depths = np.array([[0, 0, d] for d in [0.01, 0.05, 0.1, 0.2, 0.4]]) * mm
T_depth = model.transmittance(source_coords, source_direction, depths)
assert np.all(np.diff(T_depth.ravel()) < 0) # strictly decreasing

# behind the source: real (small) backscatter within the data's negative-z
# range, but exactly 0 beyond it (the clip must not freeze a nonzero value)
peak = model.transmittance(
source_coords, source_direction, np.array([[0, 0, 0]]) * mm
)
near_behind = np.array([[0, 0, -0.05]]) * mm # 0.05 mm behind source
T_near = model.transmittance(source_coords, source_direction, near_behind)
assert np.all(T_near >= 0)
assert np.all(T_near < peak) # backscatter is weaker than the forward peak

z_back = model._z_range[0] # cm, most-negative z with data (~ -0.29 cm)
far_behind = np.array([[0, 0, z_back - 0.05]]) * cm # beyond the data range
T_far = model.transmittance(source_coords, source_direction, far_behind)
assert np.all(T_far == 0)

# transmittance does not increase with radial distance (at fixed depth)
radial = np.array([[r, 0, 0.05] for r in [0.0, 0.02, 0.05, 0.1, 0.2]]) * mm
T_radial = model.transmittance(source_coords, source_direction, radial)
assert np.all(np.diff(T_radial.ravel()) <= 0) # non-increasing (r=0 plateau ok)

# area0 = pi * beam_radius^2
expected = np.pi * (100 * um) ** 2
assert np.isclose(float(model.area0 / mm2), float(expected / mm2))


def test_reset():
light = Light(light_model=fiber473nm())
assert light.value == 0
Expand Down Expand Up @@ -152,7 +189,7 @@ def test_light_power_irradiance(n_coords, values, shape):


@pytest.mark.parametrize(
"light_model", [fiber473nm(), GaussianEllipsoid(), KoehlerBeam(1 * mm)]
"light_model", [fiber473nm(), GaussianEllipsoid(), KoehlerBeam(1 * mm), OptogenSIMLight()]
)
@pytest.mark.parametrize(
"m, squeeze_coords, squeeze_dir",
Expand Down