Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ sim = ms.Simulation(
truth_space=ms.ShapeScaleSpace(shape=(128, 512, 512), scale=(0.02, 0.01, 0.01)),
output_space={"downscale": 8},
sample=ms.Sample(labels=[ms.MatsLines(density=0.5, length=30, azimuth=5, max_r=1)]),
modality=ms.Confocal(pinhole_au=0.2),
output_path="au02.tiff",
modality=ms.Confocal(pinhole_au=1),
output_path="au1.tiff",
)

# run it
Expand Down
Binary file removed docs/images/au02.png
Binary file not shown.
Binary file added docs/images/au1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/laser_hyd.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
49 changes: 44 additions & 5 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Construct and run a [`microsim.Simulation`][] object.
truth_space=ms.ShapeScaleSpace(shape=(128, 512, 512), scale=(0.02, 0.01, 0.01)),
output_space={"downscale": 8},
sample=ms.Sample(labels=[ms.MatsLines(density=0.5, length=30, azimuth=5, max_r=1)]),
modality=ms.Confocal(pinhole_au=0.2),
modality=ms.Confocal(pinhole_au=1),
)
result = sim.run()
ortho_plot(result)
Expand Down Expand Up @@ -78,13 +78,13 @@ Construct and run a [`microsim.Simulation`][] object.
}
]
),
modality={"type": "confocal", "pinhole_au": 0.2},
modality={"type": "confocal", "pinhole_au": 1},
)
result = sim.run()
ortho_plot(result)
```

![Image](./images/au02.png)
![Image](./images/au1.png)

Note that you can also construct a simulation from a JSON object.
This is particularly useful with the `output` parameter, to dump
Expand Down Expand Up @@ -112,9 +112,9 @@ the result of the simulation to a file.
},
"modality": {
"type": "confocal",
"pinhole_au": 0.2
"pinhole_au": 1
},
"output_path": "au02.tiff"
"output_path": "au1.tiff"
}
```

Expand All @@ -129,5 +129,44 @@ sim = Simulation.model_validate_json(spec)
sim.run()
```

### A more realistic confocal

The example above uses the defaults: a generic fluorophore, a widefield FITC filter
cube, and no detector (so the result is noise-free photon counts). To simulate what
you'd actually see on a point-scanning confocal, pick a real fluorophore, a laser line,
a point detector, and a pixel dwell time:

```python
from microsim import schema as ms
from microsim.schema.detectors.lib import HYD_SP8
from microsim.schema.optical_config.lib import LASER_488
from microsim.util import ortho_plot

sim = ms.Simulation(
truth_space=ms.ShapeScaleSpace(shape=(128, 512, 512), scale=(0.02, 0.01, 0.01)),
output_space={"downscale": 8},
sample=ms.Sample(
labels=[
ms.FluorophoreDistribution(
distribution=ms.MatsLines(density=0.5, length=30, azimuth=5, max_r=1),
fluorophore="EGFP",
)
]
),
channels=[LASER_488], # 488 nm laser, with a 525/50 emission filter
modality=ms.Confocal(pinhole_au=1),
detector=HYD_SP8,
exposure_ms=0.002, # 2 µs pixel dwell time
)
result = sim.run()
ortho_plot(result)
```

![Image](./images/laser_hyd.png)

The laser `power` is the peak irradiance at the focus, in W/cm². It defaults to 1e5
(about 50 µW at the sample with a 1.4 NA objective); to change it, use e.g.
`LASER_488.model_copy(update={"power": 1e4})`.

For more details, see the [tutorial](./tutorial.ipynb), [conceptual
overview](./concept.md) and the [API docs](./api)
12 changes: 7 additions & 5 deletions docs/stages.md
Original file line number Diff line number Diff line change
Expand Up @@ -379,15 +379,17 @@ bin in the detector.
!!! example

This example sets up a simulation with a confocal microscope with a 1.4 NA
objective lens and a 1.2 AU pinhole. The simulation is set up to use the "491"
optical configuration from the [Example Yokogawa
Setup](https://www.fpbase.org/microscope/4yL4ggAo/?c=491) (spinning disc confocal)
microscope on FPbase.
objective lens, a 1.2 AU pinhole, and a 488 nm laser (`LASER_488`: the laser,
a dichroic, and a 525/50 emission filter). For point-scanning modalities, the
laser `power` is the peak irradiance at the focus.

```python
from microsim.schema.optical_config.lib import LASER_488

sim = Simulation(
# ...,
channels=["4yL4ggAo::491"],
channels=[LASER_488], # 1e5 W/cm² at the focus
exposure_ms=0.002, # 2 µs pixel dwell time
objective_lens={
"numerical_aperture": 1.4,
"immersion_medium_ri": 1.515,
Expand Down
171 changes: 89 additions & 82 deletions docs/tutorial.ipynb

Large diffs are not rendered by default.

10 changes: 7 additions & 3 deletions examples/basic_confocal.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from microsim import schema as ms
from microsim.schema.detectors.lib import HYD_SP8
from microsim.schema.optical_config.lib import LASER_488
from microsim.util import ortho_plot

sim = ms.Simulation(
Expand All @@ -11,10 +13,12 @@
concentration=5,
)
],
exposure_ms=200,
# 488 nm laser, 1e5 W/cm^2 peak irradiance at the focus (~50 µW at NA 1.4)
channels=[LASER_488],
modality=ms.Confocal(pinhole_au=0.5),
settings=ms.Settings(random_seed=100, max_psf_radius_aus=8),
detector=ms.CameraCCD(qe=0.82, read_noise=2, bit_depth=12),
detector=HYD_SP8,
exposure_ms=0.002, # 2 µs pixel dwell time
settings=ms.Settings(random_seed=100),
)

result = sim.run()
Expand Down
4 changes: 2 additions & 2 deletions examples/fpbase_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
)
]
),
modality=ms.Confocal(pinhole_au=0.5),
modality=ms.Widefield(),
channels=[ms.OpticalConfig.from_fpbase("i6WL2W", "Widefield Dual Green")],
settings=ms.Settings(random_seed=100, max_psf_radius_aus=8),
settings=ms.Settings(random_seed=100),
detector=ms.CameraCCD(qe=0.82, read_noise=2, bit_depth=12),
# output_path="au1.tif",
)
Expand Down
16 changes: 10 additions & 6 deletions examples/multi_channel_confocal.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import time

from microsim import schema as ms
from microsim.schema.optical_config.lib import EYFP, FITC
from microsim.schema.detectors.lib import HYD_SP8
from microsim.schema.optical_config.lib import LASER_488, LASER_515
from microsim.util import ndview

# short stubby lines labeled in EGFP and long thin lines labeled in EYFP
# bleedthrough of the EGFP signal in to the EYFP is more visible than vice versa
# short stubby lines labeled in EGFP and long thin lines labeled in EYFP, imaged
# with 488 and 515 nm lasers. EYFP is also excited at 488 nm and emits in the
# 525/50 band, so it bleeds through into the 488 channel (about half as bright as
# EGFP there); much less EGFP bleeds through into the 515 channel.

sim = ms.Simulation(
truth_space=ms.ShapeScaleSpace(shape=(64, 256, 256), scale=(0.04, 0.02, 0.02)),
Expand All @@ -23,9 +26,10 @@
]
),
modality=ms.Confocal(pinhole_au=1),
settings=ms.Settings(random_seed=100, max_psf_radius_aus=4),
detector=ms.CameraCCD(qe=0.82, read_noise=4, bit_depth=12),
channels=[FITC, EYFP],
settings=ms.Settings(random_seed=100),
detector=HYD_SP8,
exposure_ms=0.002, # 2 µs pixel dwell time
channels=[LASER_488, LASER_515],
# output_path="au1.tif",
)

Expand Down
165 changes: 131 additions & 34 deletions examples/notebooks/confocal.ipynb

Large diffs are not rendered by default.

60 changes: 22 additions & 38 deletions examples/notebooks/cosem.ipynb

Large diffs are not rendered by default.

64 changes: 45 additions & 19 deletions examples/notebooks/direct_truth.ipynb

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions examples/point_detectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from microsim import schema as ms
from microsim.schema.detectors import lib
from microsim.schema.optical_config.lib import FITC
from microsim.schema.optical_config.lib import LASER_488

DWELL_MS = 0.001 # 1 µs pixel dwell

Expand All @@ -21,11 +21,11 @@
concentration=1,
)
],
# peak irradiance at focus (~25 µW in a diffraction-limited spot). Gives tens
# peak irradiance at focus (~10 µW at NA 1.4). Gives tens
# of Mcps at the brightest pixels; EGFP saturation is minor here (k*tau ~ 0.03)
channels=[FITC.model_copy(update={"power": 2e4})], # W/cm^2
channels=[LASER_488.model_copy(update={"power": 2e4})], # W/cm^2
modality=ms.Confocal(pinhole_au=1),
settings=ms.Settings(random_seed=100, max_psf_radius_aus=8),
settings=ms.Settings(random_seed=100),
# QE is applied when the optical image is computed (see microsim#152). All the
# detectors below are GaAsP (peak QE 0.45, nearly identical spectra), so one
# optical image serves them all.
Expand Down
60 changes: 60 additions & 0 deletions src/microsim/schema/optical_config/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,66 @@
)


# Laser lines for point-scanning confocals: a laser, a dichroic (main beam splitter)
# and an emission bandpass. Filter values are generic (not a specific product).
# `power` is the peak irradiance at the focus (W/cm^2): 1e5 is ~50 uW at NA 1.4.
# To change it: `LASER_488.model_copy(update={"power": 1e4})`
LASER_405 = OpticalConfig(
name="405",
lights=[LightSource.laser(405)],
power=1e5,
filters=[
Longpass(cuton=420, placement="BS"),
Bandpass(bandcenter=450, bandwidth=50, placement="EM"),
],
)
LASER_445 = OpticalConfig(
name="445",
lights=[LightSource.laser(445)],
power=1e5,
filters=[
Longpass(cuton=455, placement="BS"),
Bandpass(bandcenter=480, bandwidth=40, placement="EM"),
],
)
LASER_488 = OpticalConfig(
name="488",
lights=[LightSource.laser(488)],
power=1e5,
filters=[
Longpass(cuton=495, placement="BS"),
Bandpass(bandcenter=525, bandwidth=50, placement="EM"),
],
)
LASER_515 = OpticalConfig(
name="515",
lights=[LightSource.laser(515)],
power=1e5,
filters=[
Longpass(cuton=520, placement="BS"),
Bandpass(bandcenter=550, bandwidth=40, placement="EM"),
],
)
LASER_561 = OpticalConfig(
name="561",
lights=[LightSource.laser(561)],
power=1e5,
filters=[
Longpass(cuton=570, placement="BS"),
Bandpass(bandcenter=600, bandwidth=50, placement="EM"),
],
)
LASER_640 = OpticalConfig(
name="640",
lights=[LightSource.laser(640)],
power=1e5,
filters=[
Longpass(cuton=650, placement="BS"),
Bandpass(bandcenter=690, bandwidth=50, placement="EM"),
],
)


# This is just a rough example... needs to be refined
def spectral_detector(
bins: int,
Expand Down
4 changes: 3 additions & 1 deletion tests/test_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
CODE_BLOCK = re.compile(r"```python([^`]*)```", re.DOTALL)
JSON_BLOCK = re.compile(r"```json([^`]*)```", re.DOTALL)

# these docs fetch fluorophores or filters from FPbase
NEEDS_INTERNET = {"stages.md", "index.md"}
if not HAVE_INTERNET:
DOCS_MDS = [p for p in DOCS_MDS if p.name not in {"stages.md"}]
DOCS_MDS = [p for p in DOCS_MDS if p.name not in NEEDS_INTERNET]


@pytest.mark.parametrize("doc", DOCS_MDS, ids=lambda p: p.name)
Expand Down
11 changes: 11 additions & 0 deletions tests/test_photometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
emission_lines,
excitation_lines,
)
from microsim.schema.optical_config import lib
from microsim.schema.optical_config.lib import FITC
from tests._util import skipif_no_internet

Expand Down Expand Up @@ -501,3 +502,13 @@ def test_widefield_plane_brightness_independent_of_stack_depth(nz_out: int) -> N
rate = float(sim.filtered_emission_rates().sum())
assert rate > 0
assert per_fluor == pytest.approx(rate, rel=0.15)


@pytest.mark.parametrize("laser", [405, 445, 488, 515, 561, 640])
def test_laser_channels(laser: int) -> None:
ch = getattr(lib, f"LASER_{laser}")
assert ch.emission_band_center() == pytest.approx(ch.filters[-1].bandcenter)
# the dichroic passes the laser to the sample, and the emission path blocks it
illum = ch.illumination
assert illum.wavelength[np.argmax(illum.intensity)] == laser
assert ch.emission.spectrum.at([laser])[0] < 1e-6
Loading