Principal component analysis#

Project multi-harmonic phasor coordinates onto principal plane.

The phasorpy.phasor.phasor_to_principal_plane() function is used to project multi-harmonic phasor coordinates onto a plane, along which coordinate axes the phasor coordinates have the largest variations (the first two axes of a Principal Component Analysis).

Import required modules, functions, and classes. Define a helper function:

import numpy

from phasorpy.phasor import (
    phasor_from_lifetime,
    phasor_semicircle,
    phasor_to_apparent_lifetime,
    phasor_to_principal_plane,
)
from phasorpy.plot import PhasorPlot


def distribution(values, stddev=0.05, samples=100):
    return numpy.ascontiguousarray(
        numpy.vstack(
            [numpy.random.normal(value, stddev, samples) for value in values]
        ).T
    )


numpy.random.seed(42)

Overlapping phasor distributions#

The phasor coordinates of different multi-exponential decays may be overlapping, indistinguishable at a certain frequency:

frequency = [80, 160, 240, 320, 400]

# create two distributions of phasor coordinates overlapping at 80 MHz
real0, imag0 = phasor_from_lifetime(
    frequency,
    lifetime=distribution([0.5, 4.0]),
    fraction=distribution([0.4, 0.6]),
)
real1, imag1 = phasor_from_lifetime(
    frequency,
    lifetime=distribution([1.0, 8.0]),
    fraction=distribution([0.6, 0.4]),
)
# merge the two distributions
real = numpy.hstack([real0, real1])
imag = numpy.hstack([imag0, imag1])

plot = PhasorPlot(
    frequency=frequency[0],
    title=f'Overlapping phasor distributions ({frequency[0]} MHz)',
)
plot.plot(real[0], imag[0], '.')
plot.show()
Overlapping phasor distributions (80 MHz)

The distributions are better distinguishable at other frequencies:

plot = PhasorPlot(
    frequency=frequency[2],
    title=f'Phasor distributions ({frequency[2]} MHz)',
)
plot.plot(real[2], imag[2], '.')
plot.show()
Phasor distributions (240 MHz)

Project onto principal plane#

The projection of the multi-harmonic phasor coordinates onto the principal plane should give an overall good representation of the distribution.

The transformation matrix can be used to project other multi-harmonic phasor coordinates onto the same plane, for example the universal semicircle:

x0, y0, transformation_matrix = phasor_to_principal_plane(real, imag)

lifetimes, _ = phasor_to_apparent_lifetime(
    *phasor_semicircle(), frequency=frequency[0]
)
lifetimes[0] = 1e9
x1, y1 = numpy.dot(
    transformation_matrix,
    numpy.vstack(phasor_from_lifetime(frequency, lifetimes)),
)
x2, y2 = numpy.dot(
    transformation_matrix,
    numpy.vstack(phasor_from_lifetime(frequency, [0.5, 1.0, 2.0, 4.0, 8.0])),
)

plot = PhasorPlot(
    title='Projection on principal plane', grid=False, xlabel='x', ylabel='y'
)
plot.plot(x0, y0, '.', label='Phasor distributions')
plot.plot(x1, y1, '-', color='0.5', label='Universal semicircle')
plot.plot(x2, y2, '.', color='0.5')
plot.show()
Projection on principal plane

For single harmonic input, the projected, reoriented coordinates match the original, single harmonics phasor coordinates (compare to the first figure):

x0, y0, transformation_matrix = phasor_to_principal_plane(real[:1], imag[:1])
x1, y1 = numpy.dot(
    transformation_matrix,
    numpy.vstack(phasor_from_lifetime(frequency[0], lifetimes, keepdims=True)),
)
x2, y2 = numpy.dot(
    transformation_matrix,
    numpy.vstack(
        phasor_from_lifetime(
            frequency[0], [0.5, 1.0, 2.0, 4.0, 8.0], keepdims=True
        )
    ),
)

plot = PhasorPlot(
    title=f'Projection on principal plane ({frequency[0]} MHz)',
    grid=False,
    xlabel='x',
    ylabel='y',
)
plot.plot(x0, y0, '.', label='Phasor distributions')
plot.plot(x1, y1, '-', color='0.5', label='Universal semicircle')
plot.plot(x2, y2, '.', color='0.5')
plot.show()
Projection on principal plane (80 MHz)

TODO: demonstrate on real data that linearity is preserved and visualization by cursors is applicable.

sphinx_gallery_thumbnail_number = -2 mypy: allow-untyped-defs, allow-untyped-calls mypy: disable-error-code=”arg-type”

Total running time of the script: (0 minutes 0.202 seconds)

Gallery generated by Sphinx-Gallery