Repository navigation
Add OptogenSIMLight: Monte Carlo–based light model #74
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 21 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 57bfebe
Store dataset as gzipped NetCDF3 (scipy backend, no new dependency)
akhilsnarra-ui 3d89dd8
Load packaged data via importlib.resources; include data file in Poet…
akhilsnarra-ui 3abb054
Add OptogenSIMLight to light visualization notebook
akhilsnarra-ui 3db06a3
Add dedicated OptogenSIMLight transmittance test and viz_params cover…
akhilsnarra-ui 9317a61
Add OptogenSIMLight demonstration to multi-opto tutorial
akhilsnarra-ui bed38a7
Expose real backscatter behind source in OptogenSIMLight
akhilsnarra-ui df44ed5
Regenerate dataset with per-wavelength tissue fix
akhilsnarra-ui 4629a3f
Re-run light_viz notebook with regenerated dataset
akhilsnarra-ui fa6aeed
Replace OpticFiber with OptogenSIMLight in multi-opto tutorial
akhilsnarra-ui e684943
Rename optogensim_light.py to optogensim.py per review
akhilsnarra-ui 80fb94e
Clean notebooks: remove change markers, strip execution counts, fresh…
akhilsnarra-ui f2a58d6
Restructure OptogenSIM: data as field, derived properties, add defocu…
akhilsnarra-ui 2329245
Re-run notebooks against renamed OptogenSIM class
akhilsnarra-ui e26da6c
Update OptogenSIM reference in profile explorer notebook
akhilsnarra-ui 06efe53
Add OptogenSIM to transmittance test; add beam_radius and wavelength …
akhilsnarra-ui 62c6ea9
Tune multi_opto light amplitude to 0.6 mW/mm2 for OptogenSIM
akhilsnarra-ui aa6acab
Reduce docstring duplication in defocused_gaussian_beam
akhilsnarra-ui 069a4fb
Remove unused enphr3 opsin from multi_opto tutorial
akhilsnarra-ui f88c835
Move wavelength from Light to LightModel
akhilsnarra-ui 56d081b
Fix two_photon call site missed in wavelength refactor
akhilsnarra-ui 3cf2aec
Address review on optogensim.py + fix light_viz wavelength calls
akhilsnarra-ui d280664
Extend beam-width transmittance test to fiber and GaussianEllipsoid
akhilsnarra-ui 8b2fc0e
Apply ruff formatting to optogensim.py
akhilsnarra-ui 81dd5e5
Tune multi_opto amplitude to 0.45 and use np.ptp for trace spacing
akhilsnarra-ui 47a6540
Use OptogenSIM LightModel in profile explorer; fix heatmap aspect ratio
akhilsnarra-ui 838a676
Add OptogenSIM fork link to data docstring
akhilsnarra-ui d6319a0
Silence brian2 warning for discarded light-source objects
akhilsnarra-ui 9949c01
tweak visualization
kjohnsen 5e10b0e
format tests
kjohnsen b42b63b
Merge remote-tracking branch 'origin/master' into light-model-improve…
akhilsnarra-ui 3b60daf
Address final review comments
akhilsnarra-ui 9b0e017
Warn when OptogenSIM targets fall outside the data range
akhilsnarra-ui 24e74f1
Zero OptogenSIM transmittance outside data range
kjohnsen 5bdc7b4
Fix CI: drop setup-python, let setup-uv provide Python
kjohnsen 8ff3df5
Test OptogenSIM packaged data is peak-normalized to 1
kjohnsen e51e7ff
Use setup-uv@v7 (no v10 major tag exists)
kjohnsen 3213f9e
CI: prefer uv-managed Python (runner Pythons are PEP 668 locked)
kjohnsen 4fc4657
CI: setup-uv v7 cancels on all runners, use known-good v5
kjohnsen 193d00e
CI: install into uv venv instead of system python
kjohnsen 69c46e4
CI: drop empty env block (invalid workflow)
kjohnsen 6e8b96b
CI: setup-uv already provides .venv, drop manual uv venv
kjohnsen File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -153,4 +153,6 @@ dmypy.json | |
| .pytype/ | ||
|
|
||
| # Cython debug symbols | ||
| cython_debug/ | ||
| cython_debug/ | ||
| .DS_Store | ||
| **/.DS_Store | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
Binary file not shown.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
|
akhilsnarra-ui marked this conversation as resolved.
kjohnsen marked this conversation as resolved.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| import xarray as xr | ||
|
akhilsnarra-ui marked this conversation as resolved.
Outdated
|
||
| import numpy as np | ||
| from functools import cached_property | ||
| 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(slots=False) | ||
|
akhilsnarra-ui marked this conversation as resolved.
Outdated
|
||
| class OptogenSIM(LightModel): | ||
| """Light model from OptogenSIM Monte Carlo simulations.""" | ||
|
|
||
| beam_radius: Quantity = 100 * um | ||
| """Beam (1/e^2) radius. Must be within the simulated grid range.""" | ||
| data: xr.DataArray = field(default=None, repr=False) | ||
| """The 4D (wavelength, beam_size, r, z) dataset. Defaults to the dataset | ||
|
kjohnsen marked this conversation as resolved.
Outdated
|
||
| packaged with Cleo if not provided.""" | ||
|
|
||
| def __attrs_post_init__(self): | ||
| if self.data is None: | ||
| path = str(files("cleo.light.data") / "light_model_4d.nc.gz") | ||
| self.data = xr.open_dataarray(path, engine="scipy") | ||
|
|
||
| @cached_property | ||
| def _rz_slice(self): | ||
| rz = self.data.interp( | ||
| wavelength=self.wavelength / nmeter, beam_size=self.beam_radius / um | ||
|
akhilsnarra-ui marked this conversation as resolved.
|
||
| ) | ||
| # 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")]) | ||
|
akhilsnarra-ui marked this conversation as resolved.
|
||
| return rz.assign_coords(z=rz.z - z_source) | ||
|
|
||
| @cached_property | ||
| def _r_range(self): | ||
| return (float(self._rz_slice.r.min()), float(self._rz_slice.r.max())) | ||
|
|
||
| @cached_property | ||
| def _z_range(self): | ||
| return (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, | ||
|
akhilsnarra-ui marked this conversation as resolved.
Outdated
|
||
| 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] | ||
|
akhilsnarra-ui marked this conversation as resolved.
Outdated
|
||
| 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 | ||
|
|
||
|
|
||
| def defocused_gaussian_beam( | ||
| wavelength: Quantity = 473 * nmeter, | ||
| beam_radius: Quantity = 100 * um, | ||
| data_path: str = None, | ||
|
akhilsnarra-ui marked this conversation as resolved.
Outdated
|
||
| ) -> OptogenSIM: | ||
| """Construct an :class:`OptogenSIM` light model from a dataset file. | ||
|
kjohnsen marked this conversation as resolved.
Outdated
|
||
|
|
||
| ``wavelength`` and ``beam_radius`` are as documented on | ||
| :class:`OptogenSIM`. ``data_path`` is an optional path to a 4D dataset; | ||
| if None, the dataset packaged with Cleo is used. | ||
| """ | ||
| if data_path is None: | ||
| data_path = str(files("cleo.light.data") / "light_model_4d.nc.gz") | ||
| data = xr.open_dataarray(data_path, engine="scipy") | ||
| return OptogenSIM(wavelength=wavelength, beam_radius=beam_radius, data=data) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
|
akhilsnarra-ui marked this conversation as resolved.
kjohnsen marked this conversation as resolved.
kjohnsen marked this conversation as resolved.
kjohnsen marked this conversation as resolved.
|
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
|
kjohnsen marked this conversation as resolved.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| import marimo | ||
|
|
||
| __generated_with = "0.23.14" | ||
| app = marimo.App(width="medium") | ||
|
|
||
|
|
||
| @app.cell | ||
| def _(): | ||
| return | ||
|
|
||
|
|
||
| @app.cell | ||
| def _(mo): | ||
| mo.md(""" | ||
| # OptogenSIM profile explorer | ||
|
|
||
| Interactive 2D cross-sections of the OptogenSIM Monte Carlo light model. | ||
| Drag the sliders to set a wavelength and beam radius and see how light | ||
| propagates through gray matter. The heatmap shows log₁₀ transmittance in | ||
| the (r, z) plane, mirrored across the beam axis. z is depth from the | ||
| source; r is radial distance. | ||
|
|
||
| *Note: values between simulated grid points (wavelengths every 30 nm; | ||
| beam radii 10, 20, 100, 200, 400, 800 µm) are linearly interpolated. | ||
| """) | ||
| return | ||
|
|
||
|
|
||
| @app.cell | ||
| def _(): | ||
| import numpy as np | ||
| import xarray as xr | ||
| import plotly.graph_objects as go | ||
|
kjohnsen marked this conversation as resolved.
|
||
|
|
||
| return go, np, xr | ||
|
|
||
|
|
||
| @app.cell | ||
| def _(xr): | ||
| from importlib.resources import files | ||
| _path = str(files("cleo.light.data") / "light_model_4d.nc.gz") | ||
| da = xr.open_dataarray(_path, engine="scipy") | ||
| da.load() | ||
| return (da,) | ||
|
|
||
|
|
||
| @app.cell | ||
| def _(da, mo): | ||
| wl_vals = da.wavelength.values | ||
| bs_vals = da.beam_size.values | ||
| wavelength = mo.ui.slider( | ||
| start=float(wl_vals.min()), | ||
| stop=float(wl_vals.max()), | ||
| step=5, | ||
| value=470.0, | ||
| label="Wavelength (nm)", | ||
| show_value=True, | ||
| ) | ||
| beam = mo.ui.slider( | ||
| start=float(bs_vals.min()), | ||
| stop=float(bs_vals.max()), | ||
| step=10, | ||
| value=200.0, | ||
| label="Beam radius (µm)", | ||
| show_value=True, | ||
| ) | ||
| mo.hstack([wavelength, beam], justify="start") | ||
| return beam, wavelength | ||
|
|
||
|
|
||
| @app.cell | ||
| def _(beam, da, go, np, wavelength): | ||
| sl = da.interp(wavelength=wavelength.value, beam_size=beam.value) | ||
|
|
||
| r = sl.r.values * 10.0 # cm -> mm | ||
| z = sl.z.values * 10.0 # cm -> mm | ||
|
|
||
| on_axis = sl.isel(r=0).values | ||
| z0 = z[int(np.argmax(on_axis))] | ||
| z_rel = z - z0 | ||
|
|
||
| T = sl.values | ||
| T = T / np.nanmax(T) | ||
|
kjohnsen marked this conversation as resolved.
Outdated
|
||
| logT = np.log10(np.clip(T, 1e-4, None)) | ||
|
|
||
| r_full = np.concatenate([-r[::-1], r]) | ||
| logT_full = np.vstack([logT[::-1, :], logT]) | ||
|
|
||
| fig = go.Figure( | ||
| data=go.Heatmap( | ||
| x=z_rel, | ||
| y=r_full, | ||
| z=logT_full, | ||
| colorscale="Viridis", | ||
| zmin=-4, zmax=0, | ||
| colorbar=dict(title="log₁₀ T/peak"), | ||
| ) | ||
| ) | ||
| fig.update_layout( | ||
| title=f"{wavelength.value:.0f} nm, {beam.value:.0f} µm beam radius", | ||
| xaxis_title="z, depth from source (mm)", | ||
| yaxis_title="r, radial distance (mm)", | ||
| width=750, height=450, | ||
|
kjohnsen marked this conversation as resolved.
Outdated
|
||
| ) | ||
| fig.update_xaxes(range=[float(z_rel.min()), min(float(z_rel.max()), 3)]) | ||
| fig | ||
| return | ||
|
|
||
|
|
||
| @app.cell | ||
| def _(): | ||
| import marimo as mo | ||
|
|
||
| return (mo,) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| app.run() | ||
|
kjohnsen marked this conversation as resolved.
|
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.