Skip to content

Add OptogenSIMLight: Monte Carlo–based light model - #74

Merged
kjohnsen merged 42 commits into
masterfrom
light-model-improvements
Oct 5, 2026
Merged

kjohnsen merged 42 commits into
masterfrom
light-model-improvements

Conversation

@akhilsnarra-ui

Copy link
Copy Markdown
Contributor

Summary

Adds OptogenSIMLight, a light model built from OptogenSIM Monte Carlo simulations of light propagation through gray matter, as a simulation alternative to the analytical OpticFiber (Foutz 2012). Built from a 78-run sweep (13 wavelengths (380–740 nm) × 6 beam radii (10–800 µm)) assembled into a 4D dataset.

What's included

  • OptogenSIMLight class in a new optogensim_light.py module (separate from light.py)
  • Packaged 4D dataset (light_model_4d.nc.gz, gzipped NetCDF3, ~6 MB)
  • Export from cleo.light; data loaded via importlib.resources
  • Dedicated transmittance test for the finite-range model
  • Multi-opto tutorial updated to use OptogenSIMLight for both amber light and blue ligh

Design decisions

  • Separate module to keep the MC-data-backed model isolated from the analytical models.
  • Data as gzipped NetCDF3 via the scipy backend — no new dependency.
  • 4D dataset with linear interpolation, kept raw rather than regressions
  • z re-zeroed to the on-axis fluence peak so z=0 is the source, matching Cleo's convention.

Dataset generation & validation

Tissue optical properties follow Jacques (2013, Phys. Med. Biol. 58:R37), which makeTissueList_OGS implements. The dataset was regenerated after finding the original sweep held tissue properties fixed across all wavelengths (the sweep loop didn't recompute the tissue list per wavelength, so wavelength-dependence wasn't captured). After fixing, penetration correctly tracks the tissue absorption spectrum: shallowest near the 410 nm hemoglobin Soret peak (~0.33 mm), a Q-band dip at 560 nm, deepest in the red (~1.5 mm) which is consistent with the Jacques model, in agreement with OpticFiber/Aravanis, and within the literature range for red light in gray matter.
Screenshot 2026-07-30 at 7 45 04 AM
Screenshot 2026-07-30 at 7 44 13 AM

Testing

Full light test suite passes (132 tests).
Dataset validated: penetration traces the absorption spectrum across all 13 wavelengths, beam width scales with beam size, wavelength interpolation stays in bounds.

Zero transmittance only beyond the data's negative-z range rather than
blanket-zeroing everything behind the source, preserving near-source
backscatter. Update test to assert small positive backscatter within
range and hard zero beyond it.
Previous sweep used stale tissue properties (fixed at startup wavelength)
for all 78 runs. Fixed OptogenSIM GO loop to recompute tissue per
wavelength. Penetration now traces hemoglobin absorption: shallowest at
410nm (0.33mm, Soret peak), deepest in red (~1.6mm), Q-band dip at 560nm.
Both fibers (590nm amber, 470nm blue) now use OptogenSIMLight. Amber
previously used OpticFiber blue coefficients scaled 0.8x as a 590nm
stand-in; now uses real per-wavelength MC data. Removed redundant
additive demo section. Narrative claims verified against regenerated figures.
@akhilsnarra-ui
akhilsnarra-ui requested a review from kjohnsen July 30, 2026 11:45
@akhilsnarra-ui akhilsnarra-ui self-assigned this Jul 30, 2026
@gitnotebooks

gitnotebooks Bot commented Jul 30, 2026

Copy link
Copy Markdown

Found 2 changed notebooks. Review the changes at https://app.gitnotebooks.com/siplab-gt/cleo/pull/74

@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.

Marvelous! Some refinements to be made, but an excellent start.

Comment thread cleo/light/optogensim.py
Comment thread cleo/light/optogensim.py Outdated
Comment thread cleo/light/optogensim.py Outdated
Comment thread cleo/light/optogensim.py
Comment thread docs/tutorials/multi_opto.ipynb
Comment thread docs/tutorials/multi_opto.ipynb
Comment thread notebooks/light_viz.ipynb
Comment thread tests/light/test_light.py Outdated
Comment thread tests/light/test_light.py
Comment thread tests/light/test_light.py
@akhilsnarra-ui

Copy link
Copy Markdown
Contributor Author

Pushed a round of updates addressing the recent review comments — the multi_opto light tuning, removing the unused enphr3 opsin, the factory docstring cleanup, and the wavelength refactor moving it to the LightModel. Re-requesting review so you can take a look at the updated changes whenever you get a chance. A few threads are still open with questions for you where I wasn't sure of the direction. Thanks!

@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.

Looking very nice! Just a few more refinements to make.

Comment thread cleo/light/light.py
Comment thread cleo/light/optogensim.py Outdated
Comment thread cleo/light/optogensim.py Outdated
Comment thread cleo/light/optogensim.py Outdated
Comment thread cleo/light/optogensim.py Outdated
Comment thread tests/light/test_light.py Outdated
Comment thread tests/light/test_light.py Outdated
Comment thread notebooks/light_profile_explorer.py
Comment thread notebooks/light_profile_explorer.py Outdated
Comment thread cleo/light/optogensim.py Outdated
optogensim.py:
- Remove unnecessary slots=False (cached_property works slotted)
- Remove data_path from defocused_gaussian_beam
- Expand factory + data docstrings (structure, units, ranges; repo link to follow)
- Extend viz to show backscatter behind the source
- Default n_points_per_source to 16000 to reduce marker size

Also: declare xarray+plotly in pyproject; update light_viz Light(wavelength=)
call sites to the new light-model wavelength API.
Generalized test_OptogenSIM_T_increases_with_beam_radius into
test_T_increases_with_beam_width, parametrized across fiber (R0),
GaussianEllipsoid (sigma_lateral), and OptogenSIM (beam_radius), each
with a model-appropriate target point. Addresses review comment.
Replace direct xarray loading with the OptogenSIM model (simpler, acts as
an integration test). Set heatmap axes to equal scale (scaleanchor) so
1mm is the same vertically and horizontally.

Note: marimo run still errors, but the cause is brian2 installing a SIGINT
handler at import (fails in marimo run's worker thread), not the
file-loading. marimo edit works.
@akhilsnarra-ui

Copy link
Copy Markdown
Contributor Author

Re: the registry warnings (multi_opto thread): Investigated this — you're right about the cause. When a second light is injected, init_register_light removes the old light_source_ng and light_prop_syns with _remove_brian_object (which calls network.remove()) and recreates them combined. The warning fires because network.remove() takes them out of the Network, but brian2 still tracks them from creation, so they warn when garbage-collected.

A few ways to fix it: dispose of the removed objects so brian2 doesn't flag them, suppress the warning at removal, or restructure to avoid the throwaway objects. Do you have a preference?

@kjohnsen

Copy link
Copy Markdown
Member

Re: the registry warnings (multi_opto thread): Investigated this — you're right about the cause. When a second light is injected, init_register_light removes the old light_source_ng and light_prop_syns with _remove_brian_object (which calls network.remove()) and recreates them combined. The warning fires because network.remove() takes them out of the Network, but brian2 still tracks them from creation, so they warn when garbage-collected.

A few ways to fix it: dispose of the removed objects so brian2 doesn't flag them, suppress the warning at removal, or restructure to avoid the throwaway objects. Do you have a preference?

Deleting the objects would be cleanest I think

When a second light is injected, init_register_light removes the old
light_source_ng and light_prop_syns and rebuilds combined ones. brian2's
BrianObject.__del__ warned these discarded objects were 'never included in
a network' (fires when _network is None at GC). Set _network to network.id
in _remove_brian_object after removal so the intentional discards don't
warn. multi_opto now runs clean; full suite passes (269).
@akhilsnarra-ui

Copy link
Copy Markdown
Contributor Author

Worked through the rest of the review — registry warnings fixed (multi_opto runs clean now), and the OptogenSIM fork is published and linked in the docstring. Thank You!!

@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.

Looking wonderful. Just a teeny tiny bit more on the light profile notebook, to better function as an integration test

FYI I pushed some trivial changes I made as I tested and reviewed locally

Oh, and looks like some merge conflicts with master now...will have to merge master here and resolve for this branch to be ready to merge

Comment thread cleo/registry.py
Comment thread notebooks/light_profile_explorer.py
Comment thread notebooks/light_profile_explorer.py Outdated
…ments

# Conflicts:
#	docs/tutorials/multi_opto.ipynb
#	docs/tutorials/optogenetics.ipynb
#	pyproject.toml
#	tests/light/test_light.py
- Add comment explaining the _network warning workaround in registry
- Rework light_profile_explorer to call model.transmittance() with a
  coordinate grid instead of _rz_slice; plot raw transmittance (no scaling)
- Recalibrate multi_opto amplitude (0.2 -> 8) for the merged aggregation fix
@akhilsnarra-ui

Copy link
Copy Markdown
Contributor Author

Addressed the latest comments. Reworked the profile explorer to use model.transmittance(), dropped the scaling, and added the registry comment. Also merged master and recalibrated the multi_opto amplitude for the aggregation fix. Tests pass locally.

On the red CI check: the actual "Run tests" step passes. What fails is the "Post Set up Python" cleanup step, with Error: Cache folder path is retrieved for pip but doesn't exist on disk: /home/runner/.cache/pip. That looks an issue from the GitHub Actions Node 20 to 24 migration rather than anything in this PR, so it may need a workflow update (bumping setup-python or disabling pip cache) on the repo side. Ready for another look!!

@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.

Awesome. Down to 2 small things:

  • we need to issue a warning for going outside the data range (and confirm behavior. I see code to zero out along z axis where outside data, but not R. Does that mean we simply inherit whatever xarray does?)
  • Could you take a stab at fixing the CI problems? Try upgrading the whole uv/setup-python flow. Look at the most recent documentation for the uv action to see the recommended way to do it

Comment thread cleo/light/optogensim.py
transmittance now warns if any r/z target coordinate is outside the
simulated data range, and documents the behavior (r/z clipped to the
data edge; z beyond the near edge zeroed). Adds a unit test checking
the warning fires and out-of-range output stays finite in [0, 1].
@akhilsnarra-ui

Copy link
Copy Markdown
Contributor Author

Added the data-range warning: transmittance now warns when target r/z fall outside the OptogenSIM data range, plus a unit test checking the warning fires and out-of-range output stays finite/clipped. (Documented the behavior: r/z clip to the data edge, z beyond the near edge zeroes out.)

On the CI fix: the failure is actions/setup-python's pip-cache step breaking on the Node 24 runners (the project uses uv, not pip). Per the setup-uv docs, the fix is to drop setup-python and have setup-uv install Python via python-version. I have it ready but my token lacks workflow scope to push to .github/workflows/. Could you apply this, or let me know if I should get access?

yaml
steps:
- uses: actions/checkout@v5
- name: Install uv and set up Python
uses: astral-sh/setup-uv@v10
with:
enable-cache: true
python-version: ${{ matrix.python }}
- name: Install package
run: uv pip install .[dev]
- name: Run tests
run: uv run pytest -n auto --ignore=docs/tutorials/optimal_ctrl.ipynb

Out-of-range targets (r beyond far edge, z outside grid) now get
transmittance 0 instead of the clipped edge value, and the warning
says so. Targets nearer the axis than the first radial bin keep the
near-axis value. far_behind test expects the warning.
actions/setup-python's pip-cache post step fails on the Node 24
runners (pip cache dir missing; project uses uv, not pip).
Bump checkout v3->v5 and setup-uv v5->v10, pass python-version
through setup-uv instead. Also correct a stale peak-normalization
comment in the profile explorer.

@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.

Both items addressed: out-of-range transmittance now warns and zeros (with tests incl. peak-normalization), CI fixed and green across the matrix.

@kjohnsen
kjohnsen merged commit 7b5d728 into master Oct 5, 2026
13 checks passed
@kjohnsen
kjohnsen deleted the light-model-improvements branch October 5, 2026 18:10
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.

2 participants