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 phasor_to_signal function #86

Merged
merged 7 commits into from
Aug 1, 2024

Conversation

cgohlke
Copy link
Member

@cgohlke cgohlke commented Jul 30, 2024

Description

This PR adds a phasor_to_signal function and improves phasor_from_signal and phasor_from_signal_fft functions:

  • Unify handling of harmonic parameters across the functions.
  • Use harmonic='all' to return all harmonics for signal samples along axis.
  • Use harmonic=[scalar] to keep a length-one first axis in the phasor coordinates.
  • Use rfft function in phasor_to_signal_fft, improving efficiency.
  • Update phasorpy_phasor_from_signal benchmark tutorial.
  • Use numpy.nanmean in plot module. Some plots were silently failing when encountering NaN values (see also Add median filtering function #85).
  • Add phasor_to_signal function to reconstruct signal from phasor coordinates. While this function works perfectly for roundtrips like phasor_to_signal(*phasor_from_signal(signal)) == signal, it does not work well for synthesizing signals from calculated phasor coordinates such as phasor_from_lifetime or phasor_calibrate. I was originally hoping to use this function to calculate exponential decays from lifetime distributions, but that did not work in practice. Hence, I have not used the function in a tutorial. In any case, it's nice to have a function that does the inverse of phasor_from_signal.
  • Add reverse parameter to phasor_calibrate, which turned out useful to debug some artifacts related to phasor_to_signal.

Question: should we unify phasor_from_signal and phasor_from_signal_fft into a single function?

Release note

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

Add phasor_to_signal function

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.

@cgohlke cgohlke added the enhancement New feature or request label Jul 30, 2024
@cgohlke cgohlke requested a review from bruno-pannunzio July 30, 2024 21:09
@cgohlke cgohlke self-assigned this Jul 30, 2024
@cgohlke
Copy link
Member Author

cgohlke commented Jul 30, 2024

does not work well for synthesizing signals from calculated phasor coordinates such as phasor_from_lifetime or phasor_calibrate

Here's an example that demonstrates the issue: the uncalibrated signal is reconstructed perfectly (ignoring any NaN), while the signal reconstructed from the calibrated phasor coordinates shows strong ripples. I think those could be due to the reconstructed signal having a sharp discontinuity at origin, while the uncalibrated signal is convoluted with an IRF and does not have any sharp discontinuity (?).

import numpy
import tifffile

from phasorpy.datasets import fetch
from phasorpy.phasor import (
    phasor_calibrate,
    phasor_from_signal,
    phasor_to_signal,
)
from phasorpy.plot import plot_signal_image

# read signal
axis = 0
signal = tifffile.imread(fetch('Embryo.tif'))
plot_signal_image(signal, axis=axis)

# reconstruct signal from phasor coordinates
mean, real, imag = phasor_from_signal(signal, axis=axis, harmonic=0)
signal1 = phasor_to_signal(
    mean, real, imag, samples=signal.shape[0], axis=axis, harmonic=0
)
numpy.testing.assert_allclose(signal1, signal, atol=1e-3)
plot_signal_image(signal1, axis=axis)

# reconstruct signal from calibrated phasor coordinates
harmonics = real.shape[0]
frequency = numpy.arange(1, harmonics + 1) * 10.0

reference_signal = tifffile.imread(fetch('Fluorescein_Embryo.tif'))
reference_mean, reference_real, reference_imag = phasor_from_signal(
    reference_signal, axis=axis, harmonic=0
)
real, imag = phasor_calibrate(
    real,
    imag,
    reference_real,
    reference_imag,
    frequency=frequency,
    lifetime=4.2,
    skip_axis=0,
)
signal2 = phasor_to_signal(
    mean, real, imag, samples=signal.shape[0], axis=axis, harmonic=0
)
plot_signal_image(signal2, axis=0)

Original signal:
Figure_1
Reconstructed signal:
Figure_2
Reconstructed calibrated signal:
Figure_3

@bruno-pannunzio
Copy link
Contributor

Hi @cgohlke! These modification and functionalities added all seem very good to me.

  • Use harmonic=0 to return all harmonics for signal samples along axis. Is this too confusing?

I think this is okay. I don't know if harmonic=-1 makes more sense, considering also similar parameters in the library like frames=-1, although in that case it's an average of all frames. In any case I think both are good options and as long as it is well documented (as it is the case now) it is clear the case use.

  • Use rfft function in phasor_to_signal_fft, improving efficiency.
  • Update phasorpy_phasor_from_signal benchmark tutorial.

These are both very nice and useful additions.

  • it does not work well for synthesizing signals from calculated phasor coordinates such as phasor_from_lifetime or phasor_calibrate.

The phasor_to_signal is also a very nice addition and I think it can be useful in some cases. What happens if you "uncalibrate" the coordinates using phasor_transform to go back to original coordinates? Also, does it make sense to implement an "uncalibrate" function? I can't think now of specific use cases, but it should take much to implement.

Question: should we unify phasor_from_signal and phasor_from_signal_fft into a single function?

Yes! I think this makes much more sense than having two separate function for calculating the phasors. I think it will make documentation clearer and more easy to understand which function to use for creating the phasors, which the most important function of the whole library in my opinion. So it would be nice it's as direct as possible.

@cgohlke
Copy link
Member Author

cgohlke commented Jul 31, 2024

I don't know if harmonic=-1 makes more sense

You have a point. An alternative, more explicit, would be to allow harmonic='all' using typing harmonic: int | Literal['all'] = 1.

What happens if you "uncalibrate" the coordinates using phasor_transform to go back to original coordinates?

Good question. I'll try.

I think this makes much more sense than having two separate function for calculating the phasors.

OK. I prefer to implement it in another PR unless there are larger changes necessary in this PR.

@cgohlke
Copy link
Member Author

cgohlke commented Jul 31, 2024

What happens if you "uncalibrate" the coordinates using phasor_transform to go back to original coordinates?

Yes, that works as expected.

Also, does it make sense to implement an "uncalibrate" function?

How about adding a reverse: bool = False parameter to phasor_calibrate? It's a trivial change, keeping all of the current implementation. It "just" has to be documented and tested.

@bruno-pannunzio
Copy link
Contributor

You have a point. An alternative, more explicit, would be to allow harmonic='all' using typing harmonic: int | Literal['all'] = 1.

I think that makes sense also and is quite direct to understand. I was also thinking that the problem with harmonic=0 is that can lead to some confusion since I believe harmonic=0 can also be used to refer to the average signal along all axis? Although I am not entirely sure of this.

How about adding a reverse: bool = False parameter to phasor_calibrate? It's a trivial change, keeping all of the current implementation. It "just" has to be documented and tested.

Excelent! I see you already implemented this and looks great and also a great addition.

@cgohlke
Copy link
Member Author

cgohlke commented Aug 1, 2024

I believe harmonic=0 can also be used to refer to the average signal along all axis

The real component of the 0th harmonic is returned as the mean from the functions (the imaginary component is all zeros for real valued input). So to avoid any possible confusion we'll go with harmonic='all'.

@bruno-pannunzio bruno-pannunzio merged commit 78bbfaf into phasorpy:main Aug 1, 2024
16 checks passed
@cgohlke cgohlke deleted the phasor_to_signal branch August 1, 2024 13:47
@cgohlke cgohlke mentioned this pull request Aug 2, 2024
13 tasks
schutyb pushed a commit to schutyb/phasorpy that referenced this pull request Aug 9, 2024
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.

2 participants