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 phasor_from_lifetime tutorial #55

Merged
merged 1 commit into from
Apr 15, 2024
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
Improve phasor_from_lifetime tutorial
  • Loading branch information
cgohlke committed Apr 13, 2024
commit 0a82acb491384ea4fca09ac99bf55da447497bd7
1 change: 1 addition & 0 deletions src/phasorpy/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ def plot(
self._lines = ax.plot(re, im, fmt, label=lbl, **kwargs)
if label is not None:
ax.legend()
self._reset_limits()

def _histogram2d(
self,
Expand Down
110 changes: 76 additions & 34 deletions tutorials/phasorpy_phasor_from_lifetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,24 @@
from phasorpy.phasor import phasor_from_lifetime, phasor_to_polar
from phasorpy.plot import PhasorPlot, plot_phasor, plot_polar_frequency

rng = numpy.random.default_rng()

# %%
# Single-component lifetimes
# --------------------------
#
# The phasor coordinates of single-component lifetimes are located
# on the universal semicircle.
# For example, 3.9788735 ns and 0.9947183 ns at a frequency of 80 MHz:
# For example, 4.0 ns and 1.0 ns at a frequency of 80 MHz:

lifetime = numpy.array([3.9788735, 0.9947183])
frequency = 80.0
lifetimes = [4.0, 1.0]

plot_phasor(*phasor_from_lifetime(80.0, lifetime), frequency=80.0)
plot_phasor(
*phasor_from_lifetime(frequency, lifetimes),
frequency=frequency,
title='Single-component lifetimes',
)

# %%
# Multi-component lifetimes
Expand All @@ -39,12 +46,15 @@
# fractional intensities are linear combinations of the coordinates
# of the pure components:

fraction = numpy.array(
fractions = numpy.array(
[[1, 0], [0.25, 0.75], [0.5, 0.5], [0.75, 0.25], [0, 1]]
)

plot_phasor(
*phasor_from_lifetime(80.0, lifetime, fraction), fmt='o-', frequency=80.0
*phasor_from_lifetime(frequency, lifetimes, fractions),
fmt='o-',
frequency=frequency,
title='Multi-component lifetimes',
)

# %%
Expand All @@ -55,84 +65,116 @@
# pre-exponential amplitudes are also located on a line:

plot_phasor(
*phasor_from_lifetime(80.0, lifetime, fraction, preexponential=True),
*phasor_from_lifetime(
frequency, lifetimes, fractions, preexponential=True
),
fmt='o-',
frequency=80.0,
frequency=frequency,
title='Pre-exponential amplitudes',
)

# %%
# Average of lifetime distributions
# ---------------------------------
#
# The average phasor coordinates for wider distributions of lifetimes
# lie further inside the universal semicircle compared to the narrower
# distributions:

frequency = 80.0
lifetime = 4.0
standard_deviations = [0.1, 0.5, 1.0]
samples = 100000

plot = PhasorPlot(
frequency=frequency, title='Average of lifetime distributions'
)
for sigma in standard_deviations:
phi = numpy.sqrt(sigma * sigma / lifetime)
phasor_average = numpy.average(
phasor_from_lifetime(
frequency, rng.gamma(lifetime / phi, phi, samples)
),
axis=1,
)
plot.plot(*phasor_average, label=f'{sigma=:.1f}')
plot.show()

# %%
# Lifetime distributions at multiple frequencies
# ----------------------------------------------
#
# Phasor coordinates can be calculated at once for many frequencies,
# lifetime components, and their fractions. As an example, random distrinutions
# of lifetimes and their fractions are plotted at three frequencies.
# lifetime components, and their fractions. As an example, random
# distrinutions of lifetimes and their fractions are plotted at
# three frequencies.
# Lifetimes are passed in units of s and frequencies in Hz, requiring to
# specify a `unit_conversion` factor:

rng = numpy.random.default_rng()

samples = 100
lifetime_distribution = (
lifetimes = [4.0, 2.0, 1.0]

lifetime_distributions = (
numpy.column_stack(
(
rng.normal(3.9788735, 0.05, samples),
rng.normal(1.9894368, 0.05, samples),
rng.normal(0.9947183, 0.05, samples),
)
[rng.gamma(lifetime / 0.01, 0.01, samples) for lifetime in lifetimes]
)
* 1e-9
)
fraction_distribution = numpy.column_stack(
(rng.random(samples), rng.random(samples), rng.random(samples))
fraction_distributions = numpy.column_stack(
[rng.random(samples) for lifetime in lifetimes]
)

plot_phasor(
*phasor_from_lifetime(
frequency=[40e6, 80e6, 160e6],
lifetime=lifetime_distribution,
fraction=fraction_distribution,
lifetime=lifetime_distributions,
fraction=fraction_distributions,
unit_conversion=1.0,
),
fmt='.',
label=('40 MHz', '80 MHz', '160 MHz'),
title='Lifetime distributions at multiple frequencies',
)

# %%
# FRET efficiency
# ---------------
#
# The phasor coordinates of a fluorescence energy transfer donor
# with a single lifetime component of 4.2 ns as a function of FRET efficiency
# with a single lifetime component of 4.0 ns as a function of FRET efficiency
# at a frequency of 80 MHz, with some background signal and about 90 %
# of the donors participating in energy transfer, are on a curved trajectory.
# For comparison, when 100% donors participate in FRET and there is no
# background signal, the phasor coordinates lie on the universal semicircle:

frequency = 80.0
samples = 25
lifetime = 4.0
efficiency = numpy.linspace(0.0, 1.0, samples)

plot = PhasorPlot(frequency=80.0)
lifetime_quenched = lifetime * (1.0 - efficiency)

plot = PhasorPlot(frequency=frequency, title='FRET efficiency')
plot.plot(
*phasor_from_lifetime(80.0, 4.2 * (1.0 - efficiency)),
*phasor_from_lifetime(frequency, lifetime_quenched),
label='100% Donor in FRET',
fmt='k.',
)
plot.plot(
*phasor_from_lifetime(
80.0,
frequency,
lifetime=numpy.column_stack(
(
numpy.full(samples, 4.2), # donor-only lifetime
4.2 * (1.0 - efficiency), # donor lifetime with FRET
numpy.full(samples, lifetime), # donor-only lifetime
lifetime_quenched, # donor lifetime with FRET
numpy.full(samples, 1e9), # background with long lifetime
)
),
fraction=[0.1, 0.9, 0.1 / 1e9],
preexponential=True,
),
label='90% Donor in FRET',
fmt='o-',
label='90% Donor in FRET',
)
plot.show()

Expand All @@ -143,14 +185,14 @@
# Phase shift and demodulation of multi-component lifetimes can be calculated
# as a function of the excitation light frequency and fractional intensities:

frequency = numpy.logspace(-1, 4, 32)
fraction = numpy.array([[1, 0], [0.5, 0.5], [0, 1]])
frequencies = numpy.logspace(-1, 4, 32)
lifetimes = [4.0, 1.0]
fractions = numpy.array([[1, 0], [0.5, 0.5], [0, 1]])

plot_polar_frequency(
frequency,
*phasor_to_polar(
*phasor_from_lifetime(frequency, [3.9788735, 0.9947183], fraction)
),
frequencies,
*phasor_to_polar(*phasor_from_lifetime(frequencies, lifetimes, fractions)),
title='Multi-frequency plot',
)

# %%
Expand Down