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

Add components module #59

Merged
merged 27 commits into from
Apr 25, 2024
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b23889e
Added phasorpy_dev virtual environment to gitignore
bruno-pannunzio Jan 16, 2024
f70575b
Merge remote-tracking branch 'upstream/main'
bruno-pannunzio Feb 2, 2024
eb49467
Merge branch 'main' of https://github.com/bruno-pannunzio/phasorpy
bruno-pannunzio Feb 26, 2024
a89411c
Merge branch 'main' of https://github.com/bruno-pannunzio/phasorpy
bruno-pannunzio Feb 26, 2024
ace367c
Merge branch 'main' of https://github.com/bruno-pannunzio/phasorpy
bruno-pannunzio Mar 27, 2024
8f86c20
First version of projection to line between components
bruno-pannunzio Apr 3, 2024
1259734
Divided functions into modules.`project_phasor_to_line` implemented i…
bruno-pannunzio Apr 3, 2024
f7f7326
Update on linear fraction functions
bruno-pannunzio Apr 8, 2024
634b22f
Update fractions function
bruno-pannunzio Apr 16, 2024
d1beda5
update fractional intensities function
bruno-pannunzio Apr 16, 2024
24f161c
update fraction function
bruno-pannunzio Apr 18, 2024
635b69c
Update tutorial
bruno-pannunzio Apr 22, 2024
61c6fc3
Merge branch 'phasorpy:main' into main
bruno-pannunzio Apr 22, 2024
d7d8868
Merge branch 'main' of https://github.com/bruno-pannunzio/phasorpy in…
bruno-pannunzio Apr 22, 2024
6153e99
Update to `project_phasor_to_line` and add tests for `project_phasor_…
bruno-pannunzio Apr 22, 2024
4f472d1
Discard `plot` changes
bruno-pannunzio Apr 22, 2024
2e4be72
Discard test files
bruno-pannunzio Apr 22, 2024
917bdcf
Revert "Discard `plot` changes"
bruno-pannunzio Apr 22, 2024
788f43e
Discard `plot` changes
bruno-pannunzio Apr 22, 2024
8e6e6fe
Update `components` module description
bruno-pannunzio Apr 22, 2024
342cd3c
Apply code standards
bruno-pannunzio Apr 22, 2024
a2a46cf
Minor fixes mainly to documentation of tutorial and components functions
bruno-pannunzio Apr 23, 2024
4eaa807
Minor fixes to `phasorpy_components` tutorial
bruno-pannunzio Apr 23, 2024
c115a41
Modified check for equal components in `two_fractions_from_phasor`. A…
bruno-pannunzio Apr 25, 2024
0d08625
Applied code standards.
bruno-pannunzio Apr 25, 2024
6cd8c54
Replace `math.sqrt` with `numpy.hypot` in `two_fractions_from_phasor`…
bruno-pannunzio Apr 25, 2024
c5c3a6c
Update `numpy.hypot` with `math.hypot`
bruno-pannunzio Apr 25, 2024
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
5 changes: 5 additions & 0 deletions docs/api/components.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
phasorpy.components
-------------------

.. automodule:: phasorpy.components
:members:
1 change: 1 addition & 0 deletions docs/api/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ PhasorPy library version |version|.

phasorpy
phasor
components
plot
io
color
Expand Down
60 changes: 60 additions & 0 deletions src/phasorpy/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
'phasor_from_polar_scalar',
'circle_line_intersection',
'circle_circle_intersection',
'project_phasor_to_line',
]

import math
Expand Down Expand Up @@ -260,3 +261,62 @@ def circle_line_intersection(
y + (-dd * dx - abs(dy) * rdd) / dr,
),
)


def project_phasor_to_line(
real: ArrayLike,
imag: ArrayLike,
real_components: ArrayLike,
imag_components: ArrayLike,
/,
*,
clip: bool = True,
axis: int = -1,
) -> tuple[NDArray, NDArray]:
"""Return projected phasor coordinates to the line that joins two phasors.

By default, the points are clipped to the line segment between components
and the projection is done into the last axis.

>>> project_phasor_to_line(
... [0.6, 0.5, 0.4], [0.4, 0.3, 0.2], [0.2, 0.9], [0.4, 0.3]
... ) # doctest: +NUMBER
(array([0.592, 0.508, 0.424]), array([0.344, 0.356, 0.368]))

"""
real = numpy.copy(real)
imag = numpy.copy(imag)
real_components = numpy.asarray(real_components)
imag_components = numpy.asarray(imag_components)
if real_components.shape != (2,):
raise ValueError(f'{real_components.shape=} != (2,)')
if imag_components.shape != (2,):
raise ValueError(f'{imag_components.shape=} != (2,)')
first_component_phasor = numpy.array(
[real_components[0], imag_components[0]]
)
second_component_phasor = numpy.array(
[real_components[1], imag_components[1]]
)
total_distance_between_components = math.sqrt(
(second_component_phasor[0] - first_component_phasor[0]) ** 2
+ (second_component_phasor[1] - first_component_phasor[1]) ** 2
)
cgohlke marked this conversation as resolved.
Show resolved Hide resolved
if math.isclose(total_distance_between_components, 0, abs_tol=1e-6):
raise ValueError('components must have different coordinates')
line_vector = second_component_phasor - first_component_phasor
line_length = numpy.linalg.norm(line_vector)
line_direction = line_vector / line_length
projected_points = (
numpy.stack((real, imag), axis=axis) - first_component_phasor
)
projection_lengths = numpy.dot(projected_points, line_direction)
if clip:
projection_lengths = numpy.clip(projection_lengths, 0, line_length)
projected_points = (
first_component_phasor
+ numpy.expand_dims(projection_lengths, axis=axis) * line_direction
)
projected_points_real = projected_points[..., 0]
projected_points_imag = projected_points[..., 1]
return projected_points_real, projected_points_imag
116 changes: 116 additions & 0 deletions src/phasorpy/components.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"""Component analysis of phasor coordinates.

The ``phasorpy.components`` module provides functions to:

- calculate fractions of two components of known location by projecting to
line between components:

- :py:func:`two_fractions_from_phasor`

- calculate phasor coordinates of second component if only one is
known (not implemented)

- calculate fractions of three or four known components by using higher
harmonic information (not implemented)

- calculate fractions of two or three components of known location by
resolving graphically with histogram (not implemented)

- blindly resolve fractions of n components by using harmonic
information (not implemented)

"""

from __future__ import annotations

__all__ = ['two_fractions_from_phasor']

from typing import TYPE_CHECKING

if TYPE_CHECKING:
from ._typing import Any, ArrayLike, NDArray

import math

import numpy

from ._utils import project_phasor_to_line


def two_fractions_from_phasor(
real: ArrayLike,
imag: ArrayLike,
real_components: ArrayLike,
imag_components: ArrayLike,
/,
) -> tuple[NDArray[Any], NDArray[Any]]:
"""Return fractions of two components from phasor coordinates.

Parameters
----------
real : array_like
Real component of phasor coordinates.
imag : array_like
Imaginary component of phasor coordinates.
real_components: array_like
Real coordinates of the first and second components.
imag_components: array_like
Imaginary coordinates of the first and second components.

Returns
-------
fraction_of_first_component : ndarray
Fractions of the first component.
fraction_of_second_component : ndarray
Fractions of the second component.

Notes
-----
For the moment, 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.

Raises
------
ValueError
If the real and/or imaginary coordinates of the known components are
not of size 2.
If the two components have the same coordinates.

Examples
--------
>>> two_fractions_from_phasor(
... [0.6, 0.5, 0.4], [0.4, 0.3, 0.2], [0.2, 0.9], [0.4, 0.3]
... ) # doctest: +NUMBER
(array([0.44, 0.56, 0.68]), array([0.56, 0.44, 0.32]))

"""
real_components = numpy.asarray(real_components)
imag_components = numpy.asarray(imag_components)
if real_components.shape != (2,):
raise ValueError(f'{real_components.shape=} != (2,)')
if imag_components.shape != (2,):
raise ValueError(f'{imag_components.shape=} != (2,)')
first_component_phasor = numpy.array(
[real_components[0], imag_components[0]]
)
second_component_phasor = numpy.array(
[real_components[1], imag_components[1]]
)
total_distance_between_components = math.sqrt(
(second_component_phasor[0] - first_component_phasor[0]) ** 2
+ (second_component_phasor[1] - first_component_phasor[1]) ** 2
)
cgohlke marked this conversation as resolved.
Show resolved Hide resolved
if math.isclose(total_distance_between_components, 0, abs_tol=1e-6):
raise ValueError('components must have different coordinates')
projected_real, projected_imag = project_phasor_to_line(
real, imag, real_components, imag_components
)
distances_to_first_component = numpy.sqrt(
(numpy.array(projected_real) - first_component_phasor[0]) ** 2
+ (numpy.array(projected_imag) - first_component_phasor[1]) ** 2
)
fraction_of_second_component = (
distances_to_first_component / total_distance_between_components
)
return 1 - fraction_of_second_component, fraction_of_second_component
54 changes: 28 additions & 26 deletions src/phasorpy/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -943,19 +943,20 @@ def __init__(
background_real=background_real,
background_imag=background_imag,
)
acceptor_trajectory_real, acceptor_trajectory_imag = (
phasor_from_fret_acceptor(
frequency,
donor_lifetime,
acceptor_lifetime,
fret_efficiency=self._fret_efficiencies,
donor_freting=donor_freting,
donor_bleedthrough=donor_bleedthrough,
acceptor_bleedthrough=acceptor_bleedthrough,
acceptor_background=acceptor_background,
background_real=background_real,
background_imag=background_imag,
)
(
acceptor_trajectory_real,
acceptor_trajectory_imag,
) = phasor_from_fret_acceptor(
frequency,
donor_lifetime,
acceptor_lifetime,
fret_efficiency=self._fret_efficiencies,
donor_freting=donor_freting,
donor_bleedthrough=donor_bleedthrough,
acceptor_bleedthrough=acceptor_bleedthrough,
acceptor_background=acceptor_background,
background_real=background_real,
background_imag=background_imag,
)
cgohlke marked this conversation as resolved.
Show resolved Hide resolved

# add plots
Expand Down Expand Up @@ -1259,19 +1260,20 @@ def _on_changed(self, value: Any) -> None:
background_real=background_real,
background_imag=background_imag,
)
acceptor_trajectory_real, acceptor_trajectory_imag = (
phasor_from_fret_acceptor(
frequency,
donor_lifetime,
acceptor_lifetime,
fret_efficiency=self._fret_efficiencies,
donor_freting=donor_freting,
donor_bleedthrough=donor_bleedthrough,
acceptor_bleedthrough=acceptor_bleedthrough,
acceptor_background=acceptor_background,
background_real=background_real,
background_imag=background_imag,
)
(
acceptor_trajectory_real,
acceptor_trajectory_imag,
) = phasor_from_fret_acceptor(
frequency,
donor_lifetime,
acceptor_lifetime,
fret_efficiency=self._fret_efficiencies,
donor_freting=donor_freting,
donor_bleedthrough=donor_bleedthrough,
acceptor_bleedthrough=acceptor_bleedthrough,
acceptor_background=acceptor_background,
background_real=background_real,
background_imag=background_imag,
cgohlke marked this conversation as resolved.
Show resolved Hide resolved
)

if donor_background > 0.0:
Expand Down
34 changes: 34 additions & 0 deletions tests/test__utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
parse_kwargs,
phasor_from_polar_scalar,
phasor_to_polar_scalar,
project_phasor_to_line,
scale_matrix,
sort_coordinates,
update_kwargs,
Expand Down Expand Up @@ -132,3 +133,36 @@ def test_circle_circle_intersection():
1e-3,
)
assert not circle_line_intersection(0.6, 0.4, 0.2, 0.0, 0.0, 0.6, 0.1)


def test_project_phasor_to_line():
"""Test project_phasor_to_line function."""
assert_allclose(
project_phasor_to_line(
[0.7, 0.5, 0.3], [0.3, 0.4, 0.3], [0.2, 0.9], [0.4, 0.3]
),
(
[0.704, 0.494, 0.312],
[0.328, 0.358, 0.384],
),
)
assert_allclose(
project_phasor_to_line([0.1, 1.0], [0.5, 0.5], [0.2, 0.9], [0.4, 0.3]),
([0.2, 0.9], [0.4, 0.3]),
)
assert_allclose(
project_phasor_to_line(
[0.1, 1.0], [0.5, 0.5], [0.2, 0.9], [0.4, 0.3], clip=False
),
([0.088, 0.97], [0.416, 0.29]),
)
with pytest.raises(ValueError):
project_phasor_to_line([0], [0], [0.1, 0.1], [0.2, 0.2])
with pytest.raises(ValueError):
project_phasor_to_line([0], [0], [0.3], [0.1, 0.2])
with pytest.raises(ValueError):
project_phasor_to_line([0], [0], [0.1, 0.2], [0.3])
with pytest.raises(ValueError):
project_phasor_to_line([0], [0], [0.1], [0.3])
with pytest.raises(ValueError):
project_phasor_to_line([0], [0], [0.1, 0.1, 0, 1], [0.1, 0, 2])
61 changes: 61 additions & 0 deletions tests/test_components.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""Tests for the phasorpy.components module."""

import pytest
from numpy.testing import assert_allclose

from phasorpy.components import two_fractions_from_phasor


def test_two_fractions_from_phasor():
"""Test two_fractions_from_phasor function."""
assert_allclose(
two_fractions_from_phasor(
[0.2, 0.5, 0.7],
[0.2, 0.4, 0.3],
[0.0582399, 0.79830002],
[0.23419652, 0.40126936],
),
(
[0.82766281, 0.38389704, 0.15577992],
[0.17233719, 0.61610296, 0.84422008],
),
)
assert_allclose(
two_fractions_from_phasor(
[0.0, 0.5, 0.9],
[0.4, 0.4, 0.6],
[0.0582399, 0.79830002],
[0.23419652, 0.40126936],
),
(
[1.0, 0.38389704, 0.0],
[0.0, 0.61610296, 1.0],
),
)
with pytest.raises(ValueError):
two_fractions_from_phasor([0], [0], [0.1, 0.1], [0.2, 0.2])
with pytest.raises(ValueError):
two_fractions_from_phasor([0], [0], [0.3], [0.1, 0.2])
with pytest.raises(ValueError):
two_fractions_from_phasor([0], [0], [0.1, 0.2], [0.3])
with pytest.raises(ValueError):
two_fractions_from_phasor([0], [0], [0.1], [0.3])
with pytest.raises(ValueError):
two_fractions_from_phasor([0], [0], [0.1, 0.1, 0, 1], [0.1, 0, 2])


@pytest.mark.xfail
def test_two_fractions_from_phasor_channels():
"""Test two_fractions_from_phasor function for multiple channels."""
assert_allclose(
two_fractions_from_phasor(
[[[0.1, 0.2, 0.3]]],
[[[0.1, 0.2, 0.3]]],
[[0.2, 0.2, 0.2], [0.9, 0.9, 0.9]],
[[0.4, 0.4, 0.4], [0.3, 0.3, 0.3]],
),
(
[[[1.0, 0.96, 0.84]]],
[[[0.0, 0.04, 0.16]]],
),
)
Loading