Skip to content
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

Improve tutorials #102

Merged
merged 4 commits into from
Aug 23, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Add spectral phasor, remove apparent lifetimes from tutorial
  • Loading branch information
cgohlke committed Aug 23, 2024
commit 1ed8de3352302e6788b597713b9ecfb1a6237c36
6 changes: 3 additions & 3 deletions src/phasorpy/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def two_fractions_from_phasor(

For now, calculation of fraction of components from different
channels or frequencies is not supported. Only one pair of components can
be analyzed and will be broadcasted to all channels/frequencies.
be analyzed and will be broadcast to all channels/frequencies.

Examples
--------
Expand Down Expand Up @@ -136,7 +136,7 @@ def graphical_component_analysis(
r"""Return fractions of two or three components from phasor coordinates.

The graphical method is based on moving circular cursors along the line
between pairs of components, and quantifying the phasors for each
between pairs of components and quantifying the phasors for each
fraction.

Parameters
Expand Down Expand Up @@ -182,7 +182,7 @@ def graphical_component_analysis(
-----
For now, calculation of fraction of components from different
channels or frequencies is not supported. Only one set of components can
be analyzed and will be broadcasted to all channels/frequencies.
be analyzed and will be broadcast to all channels/frequencies.

The graphical method was first introduced in [1]_.

Expand Down
10 changes: 5 additions & 5 deletions src/phasorpy/phasor.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,15 +428,15 @@ def phasor_to_signal(
Returns
-------
signal : ndarray
Reconstructed signal with samples of one period along last axis.
Reconstructed signal with samples of one period along the last axis.

See Also
--------
phasorpy.phasor.phasor_from_signal

Notes
-----
The reconstructed signal may be undefined if the input phasor coordinates
The reconstructed signal may be undefined if the input phasor coordinates,
or signal mean contain `NaN` values.

Examples
Expand Down Expand Up @@ -1967,7 +1967,7 @@ def phasor_from_lifetime(
Return arrays of shape `(frequency.size, fraction.shape[1])`.

- `lifetime` and `fraction` are up to two-dimensional of same shape.
The last dimensions holding lifetime components and their fractions.
The last dimensions hold lifetime components and their fractions.
Return arrays of shape `(frequency.size, lifetime.shape[0])`.

Length-one dimensions are removed from returned arrays
Expand Down Expand Up @@ -2108,7 +2108,7 @@ def polar_to_apparent_lifetime(
phase : array_like
Angular component of polar coordinates.
imag : array_like
Radial component of pholar coordinates.
Radial component of polar coordinates.
frequency : array_like
Laser pulse or modulation frequency in MHz.
unit_conversion : float, optional
Expand Down Expand Up @@ -2768,7 +2768,7 @@ def phasor_threshold(
modulation_max : array_like, optional
Upper threshold for modulation.
open_interval : bool, optional
If True, the interval is open and the threshold values are
If true, the interval is open, and the threshold values are
not included in the interval.
If False, the interval is closed, and the threshold values are
included in the interval. The default is False.
Expand Down
151 changes: 74 additions & 77 deletions tutorials/phasorpy_introduction.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
# latest source code on GitHub. This requires a C compiler, such as
# XCode, Visual Studio, or gcc, to be installed::
#
# python -m pip install git+https://github.com/phasorpy/phasorpy.git
# python -m pip install -U git+https://github.com/phasorpy/phasorpy.git

# %%
# Import phasorpy
Expand Down Expand Up @@ -109,7 +109,7 @@
# of a signal at certain harmonics, normalized by the mean intensity
# (the zeroth harmonic).
# Phasor coordinates are named ``real`` and ``imag`` in the phasorpy library.
# In the literature and other software, they are also known as
# In literature and other software, they are also known as
# (:math:`G`) and (:math:`S`).
#
# Phasor coordinates of the first harmonic are calculated from the signal,
Expand All @@ -135,7 +135,7 @@
plot_phasor_image(mean, real, imag, title='Sample')

# %%
# By default, only the phasor coordinates at the first harmonic is calculated.
# By default, only the phasor coordinates at the first harmonic are calculated.
# However, only when the phasor coordinates at all harmonics are considered
# (including the mean intensity) is the signal completely described:

Expand All @@ -159,7 +159,7 @@
# phase shifted and modulated (scaled) by unknown amounts.
# The phasor coordinates must therefore be calibrated with coordinates
# obtained from a reference standard of known lifetime, acquired with
# exactly the same instrument settings.
# the same instrument settings.
#
# In this case, a homogeneous solution of Fluorescein with a lifetime of
# 4.2 ns was imaged.
Expand Down Expand Up @@ -283,93 +283,90 @@
phasorplot.show()

# %%
# Convert to apparent single lifetimes
# ------------------------------------
#
# The cartesian phasor coordinates can be converted to polar coordinates,
# phase and modulation. Theoretical single exponential lifetimes, the
# "apparent single lifetimes", can be calculated from either phase or
# modulation at each pixel:
# Cursor selection
# ----------------

from phasorpy.phasor import phasor_to_apparent_lifetime
# TODO

phase_lifetime, modulation_lifetime = phasor_to_apparent_lifetime(
real, imag, frequency=frequency
)
# %%
# Component analysis
# ------------------

# TODO

# %%
# Plot the apparent single lifetimes as histograms or images using the
# matplotlib library:

fig = pyplot.figure()
fig.suptitle('Apparent single lifetimes')
ax0 = fig.add_subplot(1, 2, 1)
ax0.set(title='from Phase')
ax0.imshow(phase_lifetime, vmin=0.0, vmax=3.5)
ax1 = fig.add_subplot(1, 2, 2)
ax1.set(title='from Modulation')
pos = ax1.imshow(modulation_lifetime, vmin=0.0, vmax=3.5)
fig.colorbar(pos, ax=[ax0, ax1], shrink=0.4, location='bottom')
pyplot.show()
# Spectral phasors
# ----------------
#
# Phasor coordinates can be calculated from hyperspectral images (acquired
# at many equidistant emission wavelengths) and processed in much the same
# way as time-resolved signals. Calibration is not necessary.
#
# Open a hyperspectral dataset acquired with a laser scanning microscope:

from phasorpy.io import read_lsm

signal = read_lsm(fetch('paramecium.lsm'))

plot_signal_image(signal, axis=0, title='Hyperspectral image')

# %%
# Calculate phasor coordinates at the first harmonic and filter out
# pixels with low intensities:

fig, ax = pyplot.subplots()
ax.set(
title='Apparent single lifetimes',
xlim=[0, 3.5],
xlabel='lifetime [ns]',
ylabel='number pixels',
)
ax.hist(phase_lifetime.flat, bins=64, range=(0, 3.5), label='from Phase')
ax.hist(
modulation_lifetime.flat, bins=64, range=(0, 3.5), label='from Modulation'
)
ax.legend()
pyplot.show()
mean, real, imag = phasor_from_signal(signal, axis=0)
_, real, imag = phasor_threshold(mean, real, imag, mean_min=1)

# %%
# Geometrically, the apparent single lifetimes are defined by the intersections
# of the universal semicircle with a line (phase) and circle (modulation)
# from/around the origin through the phasor coordinate,
# here demonstrated for the average phasor coordinates:
# Plot the phasor coordinates as a two-dimensional histogram and select two
# clusters in the phasor plot by means of elliptical cursors:

from phasorpy.color import CATEGORICAL

cursors_real = [-0.33, 0.54]
cursors_imag = [-0.72, -0.74]
radius = [0.1, 0.06]
radius_minor = [0.3, 0.25]

phasorplot = PhasorPlot(allquadrants=True, title='Spectral phasor plot')
phasorplot.hist2d(real, imag, cmap='Greys')
for i in range(len(cursors_real)):
phasorplot.cursor(
cursors_real[i],
cursors_imag[i],
radius=radius[i],
radius_minor=radius_minor[i],
color=CATEGORICAL[i],
linestyle='-',
)
phasorplot.show()

from phasorpy.phasor import phasor_center, phasor_from_apparent_lifetime
# %%
# Use the elliptic cursors to mask regions of interest in the phasor space:

real_center, imag_center = phasor_center(real, imag)
apparent_lifetimes = phasor_to_apparent_lifetime(
real_center, imag_center, frequency=frequency
)
apparent_lifetime_phasors = phasor_from_apparent_lifetime(
apparent_lifetimes, None, frequency=frequency
)
from phasorpy.cursors import mask_from_elliptic_cursor

phasorplot = PhasorPlot(
frequency=frequency, title='Average apparent single lifetimes'
)
phasorplot.hist2d(real, imag)
phasorplot.cursor(real_center, imag_center, color='tab:orange')
phasorplot.plot(real_center, imag_center, color='tab:orange')
phasorplot.plot(*apparent_lifetime_phasors, color='tab:orange')
phasorplot.ax.annotate(
f'{apparent_lifetimes[0]:.2} ns',
(
apparent_lifetime_phasors[0][0] + 0.02,
apparent_lifetime_phasors[1][0] + 0.02,
),
)
phasorplot.ax.annotate(
f'{apparent_lifetimes[1]:.2} ns',
(
apparent_lifetime_phasors[0][1],
apparent_lifetime_phasors[1][1] + 0.02,
),
elliptic_masks = mask_from_elliptic_cursor(
real,
imag,
cursors_real,
cursors_imag,
radius=radius,
radius_minor=radius_minor,
)
phasorplot.show()

# %%
# To be continued
# ---------------
# Plot a pseudo-color image, composited from the elliptic cursor masks and
# the mean intensity image:

from phasorpy.cursors import pseudo_color

pseudo_color_image = pseudo_color(*elliptic_masks, intensity=mean)

fig, ax = pyplot.subplots()
ax.set_title('Pseudo-color image from circular cursors')
ax.imshow(pseudo_color_image)
pyplot.show()

# %%
# Appendix
Expand All @@ -380,5 +377,5 @@
print(phasorpy.versions())

# %%
# sphinx_gallery_thumbnail_number = -1
# sphinx_gallery_thumbnail_number = -5
# mypy: disable-error-code="arg-type"