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 cursors module #48

Open
wants to merge 31 commits into
base: main
Choose a base branch
from
Open

Add cursors module #48

wants to merge 31 commits into from

Conversation

schutyb
Copy link

@schutyb schutyb commented Mar 26, 2024

Description

Cursors module to select component in regions
1- For circular selection
2- For to selec component in linear ranges
...

Release note

Summarize the changes in the code block below to be included in the
release notes:

...

Checklist

  • The pull request title, summary, and description are concise.
  • Related issues are linked in the description.
  • New dependencies are explained.
  • The source code and documentation can be distributed under the MIT license.
  • The source code adheres to code standards.
  • New classes, functions, and features are thoroughly tested.
  • New, user-facing classes, functions, and features are documented.
  • New features are covered in tutorials.
  • No files other than source code, documentation, and project settings are added to the repository.

@schutyb schutyb added the enhancement New feature or request label Mar 26, 2024
@schutyb
Copy link
Author

schutyb commented Mar 26, 2024

Hi @cgohlke I have this error with:

Run python -m black --check cursors.py
would reformat /home/runner/work/phasorpy/phasorpy/src/phasorpy/cursors.py

Oh no! 💥 💔 💥
1 file would be reformatted, 19 files would be left unchanged.
Error: Process completed with exit code 1.

And I thoght it was due to: single quotes and lines up to 79 characters are allowed, but I checked all the lines and there is no line over 79 characters.

I there anything am I missing?

Thanks

@cgohlke
Copy link
Contributor

cgohlke commented Mar 26, 2024

Just run python -m black . in your local branch before committing.

@cgohlke cgohlke changed the title Cursors module Add cursors module Mar 27, 2024
@cgohlke
Copy link
Contributor

cgohlke commented Mar 27, 2024

Let us know when this PR is ready to be reviewed.

@schutyb
Copy link
Author

schutyb commented Mar 27, 2024

Just run python -m black . in your local branch before committing.

Thanks, It worked now, so without "check" the "black" command correct the mistakes, right?

@schutyb
Copy link
Author

schutyb commented Mar 27, 2024

Let us know when this PR is ready to be reviewed.

You can do it now

src/phasorpy/cursors.py Outdated Show resolved Hide resolved
src/phasorpy/cursors.py Outdated Show resolved Hide resolved
- :py:func:`circular_cursor`
- :py:func:`range_cursor`


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe give a preview of what other functions/functionalities are going to be in this module?

src/phasorpy/cursors.py Outdated Show resolved Hide resolved
src/phasorpy/cursors.py Outdated Show resolved Hide resolved
src/phasorpy/cursors.py Outdated Show resolved Hide resolved
center = numpy.array([[-0.5, -0.5], [-0.5, 0.5], [0.5, -0.5], [0.5, 0.5]])
radius = [0.1, 0.1, 0.1, 0.1]
mask = circular_cursor(real, imag, center, radius=radius, components=4)
assert_array_equal(mask, [1., 2., 3., 4.])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file does not seem to be formatted with black?

- use cursors to select region of interest in the phasor:

- :py:func:`circular_cursor`
- :py:func:`range_cursor`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

range_cursor does not seem to operate on phasor coordinates (?)


Examples
--------
Compute the range cursor:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add blank line after.

src/phasorpy/cursors.py Outdated Show resolved Hide resolved
@cgohlke
Copy link
Contributor

cgohlke commented Apr 11, 2024

In this case we obtained a LUT with dim=mxnx3

In what sense is this a lookup table? This kind of data is usually encoded as a bit-field...

It looks like none of us has a clear idea of what (and why) is the best "cursors" data structure for phasor analysis and visualization. Lets discuss on Monday. So far the options are labels and bit-fields. A 2-dimensional LUT (indexed with s and g) is another option.

I propose to finish this PR (address all comments and pass tests) so it can be used already. Since we might want to add other data structures and shapes later, maybe rename the functions to something like label_from_phasor_circular and label_from_ranges.

@schutyb
Copy link
Author

schutyb commented Apr 12, 2024

In this case we obtained a LUT with dim=mxnx3

In what sense is this a lookup table? This kind of data is usually encoded as a bit-field...

I didn´t know about bit-field, I read about it now, and you are right. Let talk on monday about it!

It looks like none of us has a clear idea of what (and why) is the best "cursors" data structure for phasor analysis and visualization. Lets discuss on Monday. So far the options are labels and bit-fields. A 2-dimensional LUT (indexed with s and g) is another option.

I propose to finish this PR (address all comments and pass tests) so it can be used already. Since we might want to add other data structures and shapes later, maybe rename the functions to something like label_from_phasor_circular and label_from_ranges.

Great, I´ll work on monday after we talk, we have the workshop next week at the unit and I still need to finish some things for my presentation this weekend.

@cgohlke cgohlke mentioned this pull request Apr 29, 2024
13 tasks
@schutyb
Copy link
Author

schutyb commented Apr 29, 2024

@cgohlke all tests passed, it is ready for review

@cgohlke
Copy link
Contributor

cgohlke commented Apr 29, 2024

Can you merge the main branch and mark open conversations as resolved if they were addressed?

@schutyb
Copy link
Author

schutyb commented Apr 29, 2024

Can you merge the main branch and mark open conversations as resolved if they were addressed?

I cannot merge, bc I need one approving review

@cgohlke
Copy link
Contributor

cgohlke commented Apr 29, 2024

What I meant was to merge/pull the current main branch (which contains over a month of changes) into your cursors branch.

@cgohlke
Copy link
Contributor

cgohlke commented Apr 29, 2024

Several of the previous comments have not been addressed and the tutorial needs to be tidied. See the patch below for suggested changes.

I strongly recommend to exclude upper range values from the ranges. It is the default in Python and numpy, and no warning is printed in the tutorial.

The types of the returned arrays should be uint8 or uint16 based on the number of labels. More labels should probably raise an error or use uint32/uint64 if you think there is a possible use case.

Both cursor function are inefficient. This can be changed later. I still think the range cursor could be implemented using numpy.digitize. Did you try?

diff --git a/docs/api/index.rst b/docs/api/index.rst
index d9314d6..c20663c 100644
--- a/docs/api/index.rst
+++ b/docs/api/index.rst
@@ -14,6 +14,7 @@ PhasorPy library version |version|.
 
     phasorpy
     phasor
+    cursors
     components
     plot
     io
@@ -21,4 +22,3 @@ PhasorPy library version |version|.
     datasets
     utils
     cli
-    cursors
diff --git a/src/phasorpy/cursors.py b/src/phasorpy/cursors.py
index 1a51a13..728260f 100644
--- a/src/phasorpy/cursors.py
+++ b/src/phasorpy/cursors.py
@@ -1,10 +1,11 @@
-""" Select phasor coordinates.
-    The ``phasorpy.cursors`` module provides functions to:
+"""Select phasor coordinates.
+
+The ``phasorpy.cursors`` module provides functions to:
 
 - create labels for region of interests in the phasor space:
 
-  - :py:func:`circular_cursor`
-  - :py:func:`range_cursor`
+  - :py:func:`label_from_phasor_circular`
+  - :py:func:`label_from_ranges`
 
 """
 
@@ -30,12 +31,14 @@ import numpy
 
 
 def label_from_phasor_circular(
-    real,
-    imag,
+    real: ArrayLike,
+    imag: ArrayLike,
+    /,
     center: ArrayLike,
     radius: ArrayLike,
 ) -> NDArray[Any]:
     r"""Return indices of circle to which each phasor coordinate belongs.
+
     Phasor coordinates that do not fall in a circle have an index of zero.
 
     Parameters
@@ -64,11 +67,14 @@ def label_from_phasor_circular(
     --------
     Compute label array for four circles:
 
-    >>> label_from_phasor_circular(numpy.array([-0.5, -0.5, 0.5, 0.5]),
-    ...     numpy.array([-0.5, 0.5, -0.5, 0.5]),
-    ...     numpy.array([[-0.5, -0.5], [-0.5, 0.5], [0.5, -0.5], [0.5, 0.5]]),
-    ...     radius=[0.1, 0.1, 0.1, 0.1])
-    array([1, 2, 3, 4])
+    >>> label_from_phasor_circular(
+    ...     [-0.5, -0.5, 0.5, 0.5],
+    ...     [-0.5, 0.5, -0.5, 0.5],
+    ...     center=[[-0.5, -0.5], [-0.5, 0.5], [0.5, -0.5], [0.5, 0.5]],
+    ...     radius=[0.1, 0.1, 0.1, 0.1]
+    ... )
+    array([1, 2, 3, 4], dtype=uint8)
+
     """
     real = numpy.asarray(real)
     imag = numpy.asarray(imag)
@@ -77,29 +83,37 @@ def label_from_phasor_circular(
 
     if real.shape != imag.shape:
         raise ValueError(f'{real.shape=} != {imag.shape=}')
+    if center.ndim != 2 or center.shape[1] != 2:
+        raise ValueError(f'invalid {center.shape=}')
+    if radius.ndim != 1 or radius.shape != (center.shape[0],):
+        raise ValueError(f'invalid {radius.shape=}')
     if numpy.any(radius < 0):
         raise ValueError('radius is < 0')
-    label = numpy.zeros(real.shape, dtype=numpy.int8)
+
+    dtype = numpy.uint8 if len(center) < 256 else numpy.uint16
+    label = numpy.zeros(real.shape, dtype=dtype)
     for i in range(len(center)):
         condition = (
-            (real - center[i][0]) ** 2
-            + (imag - center[i][1]) ** 2
-            - radius[i] ** 2
+            numpy.square(real - center[i][0])
+            + numpy.square(imag - center[i][1])
+            - numpy.square(radius[i])
         )
         label = numpy.where(
-            condition > 0, label, numpy.ones(label.shape) * (i + 1)
+            condition > 0, label, numpy.full(label.shape, i + 1, dtype=dtype)
         )
     return label
 
 
-def label_from_ranges(values, ranges: ArrayLike) -> NDArray[Any]:
+def label_from_ranges(values: ArrayLike, /, ranges: ArrayLike) -> NDArray[Any]:
     r"""Return indices of range to which each value belongs.
+
     Values that do not fall in any range have an index of zero.
 
     Parameters
     ----------
-    values : array_like, shape (M, 2)
-    ranges : array_like
+    values : array_like
+        Values to be labeled.
+    ranges : array_like, shape (M, 2)
         Start and stop values of ranges.
 
     Returns
@@ -114,52 +128,60 @@ def label_from_ranges(values, ranges: ArrayLike) -> NDArray[Any]:
 
     Examples
     --------
-    Compute the range cursor:
+    Compute label array for three ranges:
+
+    >>> label_from_ranges(
+    ...     [[3.3, 6, 8], [15, 20, 7]], ranges=[(2, 8), (10, 15), (20, 25)]
+    ... )
+    array([[1, 1, 0], [0, 3, 1]], dtype=uint8)
 
-    >>> label_from_ranges(numpy.array([[3.3, 6, 8], [15, 20, 7]]),
-    ...     numpy.array([(2, 8), (10, 15), (20, 25)]))
-    array([[1, 1, 1], [2, 3, 1]])
     """
     values = numpy.asarray(values)
     ranges = numpy.asarray(ranges)
+    if ranges.ndim != 2 or ranges.shape[1] != 2:
+        raise ValueError(f'invalid {ranges.shape=}')
 
     if _overlapping_ranges(ranges):
-        warnings.warn("Overlapping ranges", UserWarning)
-    label = numpy.zeros_like(values, dtype=int)
+        warnings.warn('Overlapping ranges', UserWarning)
+
+    dtype = numpy.uint8 if ranges.shape[0] < 256 else numpy.uint16
+    label = numpy.zeros_like(values, dtype=dtype)
     # Iterate over each value in the array
     for index, value in numpy.ndenumerate(values):
         # Iterate over each range
         for range_index, (start, end) in enumerate(ranges):
             # Check if the value falls within the current range
-            if start <= value <= end:
+            if start <= value < end:
                 # Set the index of the current range
                 label[index] = range_index + 1
                 break
     return label
 
 
-def _overlapping_ranges(ranges) -> bool:
+def _overlapping_ranges(ranges: ArrayLike) -> bool:
     r"""Check if there are overlapping ranges in an array of ranges.
 
     Parameters
     ----------
-        ranges : array_like
-            Start and stop values of ranges.
+    ranges : array_like
+        Start and stop values of ranges.
 
     Returns
     -------
-        bool: True if there are overlapping ranges, False otherwise.
+    bool: True if there are overlapping ranges, False otherwise.
 
     Example
     -------
-    Compute for some range with overlapping.
-        >>> _overlapping_ranges([(1, 5), (3, 8), (6, 10), (9, 12)])
-        True
+    Compute for some range with overlapping:
+
+    >>> _overlapping_ranges([(1, 5), (3, 8), (6, 10), (9, 12)])
+    True
+
     """
     ranges = numpy.asarray(ranges)
     for i in range(len(ranges)):
         for j in range(i + 1, len(ranges)):
             # Check if the ranges overlap
-            if ranges[i][0] <= ranges[j][1] and ranges[j][0] <= ranges[i][1]:
+            if ranges[i][0] < ranges[j][1] and ranges[j][0] < ranges[i][1]:
                 return True  # Ranges overlap
     return False  # No overlaps found
diff --git a/tests/test_cursors.py b/tests/test_cursors.py
index 3339ad8..1e27354 100644
--- a/tests/test_cursors.py
+++ b/tests/test_cursors.py
@@ -13,11 +13,12 @@ def test_label_from_phasor_circular():
     imag = numpy.array([-0.5, 0.5, -0.5, 0.5])
     center = numpy.array([[-0.5, -0.5], [-0.5, 0.5], [0.5, -0.5], [0.5, 0.5]])
     radius = [0.1, 0.1, 0.1, 0.1]
-    mask = label_from_phasor_circular(real, imag, center, radius=radius)
-    assert_array_equal(mask, [1.0, 2.0, 3.0, 4.0])
+    labels = label_from_phasor_circular(real, imag, center, radius=radius)
+    assert labels.dtype == 'uint8'
+    assert_array_equal(labels, [1.0, 2.0, 3.0, 4.0])
 
 
-def test_label_from_phasor_circular_ValueErros():
+def test_label_from_phasor_circular_errors():
     # Test ValueErrors
     real = numpy.array([-0.5, -0.5, 0.5, 0.5])
     imag = numpy.array([-0.5, 0.5, -0.5, 0.5])
@@ -37,5 +38,6 @@ def test_label_from_ranges():
     # Test label from ranges
     values = numpy.array([[3.3, 6, 8], [15, 20, 7]])
     ranges = numpy.array([(2, 8), (10, 15), (20, 25)])
-    mask = label_from_ranges(values, ranges)
-    assert_array_equal(mask, [[1, 1, 1], [2, 3, 1]])
+    labels = label_from_ranges(values, ranges)
+    assert labels.dtype == 'uint8'
+    assert_array_equal(labels, [[1, 1, 0], [0, 3, 1]])
diff --git a/tutorials/phasorpy_cursors.py b/tutorials/phasorpy_cursors.py
index 2c782f8..2bf59ab 100644
--- a/tutorials/phasorpy_cursors.py
+++ b/tutorials/phasorpy_cursors.py
@@ -1,8 +1,8 @@
 """
-Phasor Cursors selectors
-========================
+Phasor cursors
+==============
 
-An introduction to cursors module.
+An introduction to selecting phasor coordinates using cursors.
 
 """
 
@@ -11,8 +11,8 @@ An introduction to cursors module.
 
 import math
 
-import numpy
 import tifffile
+from matplotlib import pyplot
 
 from phasorpy.cursors import label_from_phasor_circular, label_from_ranges
 from phasorpy.datasets import fetch
@@ -20,80 +20,83 @@ from phasorpy.phasor import phasor_from_signal, phasor_to_polar
 from phasorpy.plot import PhasorPlot
 
 # %%
-# Using circular cursors
-# ----------------------
+# Circular cursors
+# ----------------
 #
-# Use circular cursors to select regions of interest.
+# Use circular cursors in the phasor space to segment the images:
 
 signal = tifffile.imread(fetch('paramecium.lsm'))
 mean, real, imag = phasor_from_signal(signal, axis=0)
 
-label = label_from_phasor_circular(
-    real,
-    imag,
-    numpy.array(
-        [[-0.48, -0.65], [-0.22, -0.75], [0.40, -0.80], [0.66, -0.68]]
-    ),
-    radius=[0.15, 0.15, 0.15, 0.15],
-)
+center = [(-0.48, -0.65), (-0.22, -0.75), (0.4, -0.8), (0.66, -0.68)]
+radius = [0.15, 0.15, 0.15, 0.15]
 
+label = label_from_phasor_circular(real, imag, center, radius)
 
 # %%
-# Circular cursors
-# ----------------
-#
+# Show the circular cursors in the phasor plot:
 
 mask = mean > 1
-real = real[mask]
-imag = imag[mask]
-
-import matplotlib.pyplot as plt
-
-
-def cart2pol(x, y):
-    rho = numpy.sqrt(x**2 + y**2)
-    phi = numpy.arctan2(y, x)
-    return (phi, rho)
-
-
-# components centers in polars
-t1, r1 = cart2pol(-0.48, -0.65)
-t2, r2 = cart2pol(-0.22, -0.75)
-t3, r3 = cart2pol(0.40, -0.80)
-t4, r4 = cart2pol(0.66, -0.68)
 
-plotcursors = True
-if plotcursors:
-    plot = PhasorPlot(allquadrants=True, title='Test cursors selection')
-    plot.polar_cursor(t1, r1, radius=0.15)
-    plot.polar_cursor(t2, r2, radius=0.15)
-    plot.polar_cursor(t3, r3, radius=0.15)
-    plot.polar_cursor(t4, r4, radius=0.15)
-    plot.hist2d(real, imag, cmap='Blues')
+plot = PhasorPlot(allquadrants=True, title='Circular cursors')
+plot.hist2d(real[mask], imag[mask])
+plot.cursor(*center[0], radius=radius[0], color='tab:orange', linestyle='-')
+plot.cursor(*center[1], radius=radius[1], color='tab:green', linestyle='-')
+plot.cursor(*center[2], radius=radius[2], color='tab:red', linestyle='-')
+plot.cursor(*center[3], radius=radius[3], color='tab:purple', linestyle='-')
 
-plt.figure()
-plt.imshow(label)
-plt.show()
+# %%
+# Show the label image:
 
+fig, ax = pyplot.subplots()
+ax.set_title('Labels from circular cursors')
+plt = ax.imshow(label, vmin=0, vmax=10, cmap='tab10')
+fig.colorbar(plt)
+pyplot.show()
 
 # %%
 # Range cursors
 # -------------
 #
-# Compute phasor from signal and get the phase values.
+# Create labels from ranges of phase values computed from phasor coordinates:
 
 mean, real, imag = phasor_from_signal(signal, axis=0)
 phase, _ = phasor_to_polar(real, imag)
-rang = numpy.array(
-    [(0, numpy.pi), (-numpy.pi / 2, 0), (-numpy.pi, -numpy.pi / 2)]
-)
-label = label_from_ranges(phase, ranges=rang)
 
+ranges = [(0, math.pi), (-math.pi / 2, 0), (-math.pi, -math.pi / 2)]
 
-plt.figure()
-plt.imshow(label)
+label = label_from_ranges(phase, ranges=ranges)
 
-plot = PhasorPlot(allquadrants=True, title='Raw phasor')
-plot.hist2d(real, imag, cmap='Blues')
+# %%
+# Show the label image:
+
+fig, ax = pyplot.subplots()
+ax.set_title('Labels from phase ranges')
+plt = ax.imshow(label, vmin=0, vmax=10, cmap='tab10')
+fig.colorbar(plt)
+pyplot.show()
 
-plt.show()
+# %%
+# Show the range cursors in the phasor plot:
+
+plot = PhasorPlot(allquadrants=True, title='Phase range cursors')
+plot.hist2d(real, imag, cmap='Blues')
+plot.polar_cursor(
+    phase=ranges[0][0],
+    phase_limit=ranges[0][1],
+    color='tab:orange',
+    linestyle='-',
+)
+plot.polar_cursor(
+    phase=ranges[1][0],
+    phase_limit=ranges[1][1],
+    color='tab:green',
+    linestyle='-',
+)
+plot.polar_cursor(
+    phase=ranges[2][0],
+    phase_limit=ranges[2][1],
+    color='tab:red',
+    linestyle='-',
+)
+plot.show()

@schutyb
Copy link
Author

schutyb commented Apr 29, 2024

thnaks @cgohlke I´ll apply the changes tonight and have a look at numpy.digitize bc I didn´t do it. I update everything tommorrow during the day so we keep interating until it gets ready.

Also I didn´t try to optimize it or improve the functions bc you mention that eventually we could write them in cython. Do you think I should do it now?

@schutyb
Copy link
Author

schutyb commented May 17, 2024

@cgohlke I changed the function label_from_ranges to label_from_lut so for any given lut that is created with some ranges that can overlap we can select ROI´s in the phasor. I add this in the tutorial with some plot using the polar cursors.

Please have a look when you can and give me your feedback.

Comment on lines +105 to +106
#############################################
#############################################
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove

Comment on lines +110 to +113
min_vals1: NDArray,
max_vals1: NDArray,
min_vals2: NDArray,
max_vals2: NDArray,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Array inputs should be ArrayLike, not NDArray.

Comment on lines +120 to +127
- min_vals1: NDArray
Array of minimum values to binarize data1.
- max_vals1: NDArray
Array of maximum values to binarize data1.
- min_vals2: NDArray
Array of minimum values to binarize data2.
- max_vals2: NDArray
Array of maximum values to binarize data2.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow numpy docstring standard.


Returns
-------
- dict
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow numpy docstring standard.

max_vals2: NDArray,
) -> dict:
"""
Create a Lookup Table (LUT) with two pairs of minimum and maximum values.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-> """Return lookup table (LUT) with two pairs of minimum and maximum values.

Comment on lines +162 to +172
- data1: NDArray
The first data array.
- data2: NDArray
The second data array.
- lut: dict
Lookup Table (LUT) mapping input values to binarized output values.

Returns
-------
- label: NDArray:
The binarized array.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow numpy docstring standard.

Raises
------
ValueError
'Input arrays must have same shapes'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove '

Comment on lines +194 to +216
#### EXAMPLES
# Examples
# --------
# Create a LUT based on ranges values:

# >>> create_lut(
# ... min_vals1 = numpy.array([0, 3, 6]),
# ... max_vals1 = numpy.array([2, 5, 8]),
# ... min_vals2 = numpy.array([1, 4, 7]),
# ... max_vals2 = numpy.array([3, 6, 9]))
# {((0, 2), (1, 3)): 1, ((0, 2), (4, 6)): 2, ((0, 2), (7, 9)): 3,
# ... ((3, 5), (1, 3)): 4, ((3, 5), (4, 6)): 5, ((3, 5), (7, 9)): 6,
# ... ((6, 8), (1, 3)): 7, ((6, 8), (4, 6)): 8, ((6, 8), (7, 9)): 9}

# Example
# -------
# >>> arr1 = numpy.array([[1.2, 2.4, 3.5], [4.7, 5.1, 6.9], [7.3, 8.6, 9.0]])
# >>> arr2 = numpy.array([[0.8, 2.1, 3.9], [4.2, 5.7, 6.3],[7.5, 8.2, 9.5]])
# >>> lut = {((0, 2), (1, 3)): 1, ((0, 2), (4, 6)): 2, ((0, 2), (7, 9)): 3,
# ... ((3, 5), (1, 3)): 4, ((3, 5), (4, 6)): 5, ((3, 5), (7, 9)): 6,
# ... ((6, 8), (1, 3)): 7, ((6, 8), (4, 6)): 8, ((6, 8), (7, 9)): 9}
# >>> label = label_from_lut(arr1, arr2, lut)
# array([[0, 0, 0], [5, 0, 0], [9, 0, 0]])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove

------
ValueError
'Input arrays must have same shapes'

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Example is missing.

------
ValueError
'Input array must have same shapes'
"""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Example is missing

@cgohlke
Copy link
Contributor

cgohlke commented May 17, 2024

This PR contains changes which do not belong here. Please update your branch from the main phasorpy branch as discussed before.

I find the use of create_lut and label_from_lut hard to follow, especially for overlapping bins. Can you document how the labels for overlapping bins are determined. It also seems to differ from label_from_phasor_circular, which does not handle overlaps? The implementation looks inefficient, especially for large number of bins. Maybe the non-overlapping case could be implemented/replaced with scipy.stats.binned_statistic_2d? Another option could be to replace the dict LUT with a 2D numpy array.

@@ -0,0 +1,216 @@
""" Select phasor coordinates.

The ``phasorpy.cursors`` module provides functions to:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove indent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants