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

Various additions and improvements to the phasor module #32

Merged
merged 11 commits into from
Feb 16, 2024

Conversation

cgohlke
Copy link
Member

@cgohlke cgohlke commented Feb 4, 2024

Description

This PR proposes various additions and improvements to the phasorpy.phasor module and the build infrastructure:

  • add Cython as a build dependency
  • add workflow to build binary wheels with cibuildwheel
  • complete phasor_from_lifetime, including speedups written in Cython
  • add simple tutorial for phasor_from_lifetime
  • add phasor_from_signal_f1 function, including speedups in Cython. Alternative to Add phasor_from_signal function #20 phasor_from_signal
  • add phasor_from_polar function. Replaces Lifetime module branch #13 phasor_coordinates
  • add phasor_semicircle helper function
  • use Sphinx :math: for formulas
  • fix minor docstring issues

The following are being postponed to a later PR:

  • update "Publish to PyPI" workflow to cibuildwheel.
  • implement phasor_from_apparent_lifetime and phasor_to_apparent_lifetime.

A Python compatible C compiler is now required for developing the PhasorPy library.

Release note

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

Various additions and improvements to the phasor module

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 Feb 4, 2024
@cgohlke cgohlke self-assigned this Feb 4, 2024
@bruno-pannunzio
Copy link
Contributor

Hi @cgohlke, thanks for improving the docstrings of the functions that had some errors. I don't think this overlaps with anything I am working on so no problem with that. I like the idea very much of starting to implement some thing in cython to speed up the processing, maybe we can talk about it today in the meeting.

I also believe that the tests should be improved as there are many cases that are not testes, we can focus on improving this on the following weeks.

Regarding the phasor_from_signal implementation, I believe Bruno has been researching on ways to improve it and also to accept different fft functions, so maybe we can also discuss today this aspect.

@cgohlke
Copy link
Member Author

cgohlke commented Feb 5, 2024

Hi @bruno-pannunzio . Thanks for the feedback.

OK, I will remove phasor_from_signal from this PR (assuming #20 is imminent) but leave phasor_from_signal_f1.

Besides the tests marked with TODO:, any other test cases need to be improved in this PR?

@bruno-pannunzio
Copy link
Contributor

I think those are ok. I think they also apply to the tests from my previous PR because not all functions cover all those test cases you added to the TODO. So I will improve those tests also when possible.

@cgohlke
Copy link
Member Author

cgohlke commented Feb 5, 2024

Regarding today's discussion about using plain SI units: should I integrate it into this PR as a commit? Then we'll see how things look and if we don't like can just revert the commit...

@bruno-pannunzio
Copy link
Contributor

That would be great. If you need I can also do some work on it but may be caotic if we both do stuff at the same time. Let me know If you want me to work on something related.

Also, before I forget although is not related to this PR. I was reading some papers and some terms used for transforming the coordinates are 'phase shift' and 'modulation ratio'. Could it be a good idea to rename phi0 and mod0 to phase_shift and mod_ratio or maybe leave it that way? I think it sounds better but I don't know if it is more easy to understand.

@cgohlke
Copy link
Member Author

cgohlke commented Feb 5, 2024

Yes, I like phase_shift and modulation_ratio as input for phasor_calibrate and returns for polar_from_reference. It still needs to be communicated that it's regarding the reference/calibration? For acquired data, phase and modulation are always phase shifts and modulation ratios of some kind...

@bruno-pannunzio
Copy link
Contributor

You are right, maybe its something that we can discuss if it make sense or if it can lead to misunderstandings. I will get feedback on this topic.

@cgohlke
Copy link
Member Author

cgohlke commented Feb 5, 2024

Regarding today's discussion about using plain SI units: should I integrate it into this PR as a commit? Then we'll see how things look and if we don't like can just revert the commit...

OK, that did not go too well. I now think we should not handle/store lifetimes and frequencies in s and Hz. The precision of 32-bit floating point float32 is not enough to accurately represent lifetimes in ns. It is enough for frequencies in MHz, but not GHz. 64-bit floating point (double) is good enough to store these values in s and Hz, but precision still might become an issue when moving to the ps/THz range...

Anyway, I think the current implementation of phasor_from_lifetime does not strictly require users to pass frequencies and lifetimes in MHz and ns units. They might as well pass them as, for example, THz and ps or Hz and ms (phosphorescence). The function could also accept an optional conversion factor (1e-3 by default) as a function parameter, so users don't have to covert their lifetime or frequency values before passing them. Similar for the phasor_to_apparent_lifetime function...

@cgohlke cgohlke mentioned this pull request Feb 7, 2024
9 tasks
@cgohlke
Copy link
Member Author

cgohlke commented Feb 7, 2024

Interesting benchmark results: the phasor_from_signal_f1 function is 10 to 100 times faster than the phasor_from_signal reference numpy implementation on a 256x256x256 uint8 stack (lifetime_cat.tif). Maybe this computation is extremely memory limited or numpy converts the stack to double precision?

timeit.timeit('phasor_from_signal(stack, axis=0)', number=10, globals=globals())
6.033
timeit.timeit('phasor_from_signal_f1(stack, axis=0)', number=10, globals=globals())
0.524
timeit.timeit('phasor_from_signal_f1(stack, axis=0, numthreads=28)', number=10, globals=globals())
0.063

@cgohlke cgohlke changed the title WIP: improve the phasor module Various additions and improvements to the phasor module Feb 8, 2024
@cgohlke
Copy link
Member Author

cgohlke commented Feb 8, 2024

Hi @bruno-pannunzio. I think this is ready for review.

You can download the docs, including formulas and tutorial as a build artifact at https://github.com/phasorpy/phasorpy/actions/runs/7825054348?pr=32

Never mind the long phasor_plot function in the tutorial. The plan is to move it to a phasorpy.plot module.

@cgohlke
Copy link
Member Author

cgohlke commented Feb 8, 2024

I see the following issues with the phasor_from_signal_f1 function:

  1. A copy of the input signal array is made if it is not C contiguous. That is necessary because the array is reshaped to a 3D memoryview. This could be avoided by using the numpy iterator C API, but the implementation would be less straightforward/readable/maintainable and not compatible with OpenMP (I think).
  2. On macOS, Xcode does not include OpenMP libraries. There's a workaround using clang, but that could break anytime and might conflict with workarounds used in other Python libraries.
  3. Related to that, a slower than necessary code path is used when setting num_threads > 1 on builds without OpenMP.
  4. Only the phasor coordinates of the fundamental frequency is calculated.

@bruno-pannunzio
Copy link
Contributor

Hi Christoph, sorry I didn't reply before, I was on a trip until yesterday. I will go through your comments and will get back to you on the aspects you pointed out.

@bruno-pannunzio
Copy link
Contributor

Hi @cgohlke, I tested the phasor_from_f1 and phasor_from_lifetime functions and both work great. I tried all the pipeline for calibration with real images (fbd and ptu) and it works as expected using both. Also I timed the phasor calculation from signal of ptu files which are quite large. Your implementation with cython is much faster than using numpy fft. Using phasor_from_signal_f1 took just 2.8 seconds, while the numpy fft implementation took 13.5 seconds, so this is a great improvement.

I also tested the num_threads parameters but had not much of an impact with different numbers tested. Maybe as I am running with macOS, this is not compatible as you stated before.

The tutorial looks really good and easy to understand. The only thing I see is a bit distracting is the code for phasor_plot at the beginning, but as you said before, when that is implemented in phasorpy.plot, I'm guessing it will disappear from the tutorial and it will be cleaner.

Comment on lines +163 to +172
# pure numpy implementation for reference:
# shape = [1] * signal.ndim
# shape[axis] = sample_phase.size
# sample_phase = sample_phase.reshape(shape) # make broadcastable
# mean = numpy.mean(signal, axis=axis)
# real = numpy.mean(signal * numpy.cos(sample_phase), axis=axis)
# real /= mean
# imag = numpy.mean(signal * numpy.sin(sample_phase), axis=axis)
# imag /= mean
# return mean, real, imag
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we keep this commented implementation?

Copy link
Member Author

Choose a reason for hiding this comment

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

I was thinking to move it into test_phasor.py or phasorpy.testing_utils.py, but then the test function would need to be tested too. Also, the plan is to test phasor_from_signal_f1 against phasor_from_signal once implemented, or to merge those functions into one...

Copy link
Contributor

Choose a reason for hiding this comment

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

You are right, it can be a good a idea to keep that implementation somewhere to compare. The proposed phasor_from_signal is also different from that implementation, so maybe we can keep and compare the three of them?

Copy link
Member Author

Choose a reason for hiding this comment

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

For now there is no need for this in the tests, which use known values. So I think to leave it as a comment in this PR. Another option would be to implement it as a reference in a tutorial/benchmark that compares various implementations.

@cgohlke
Copy link
Member Author

cgohlke commented Feb 15, 2024

Thanks for the feedback.

I also tested the num_threads parameters but had not much of an impact with different numbers tested. Maybe as I am running with macOS, this is not compatible as you stated before.

Yes, unfortunately there's no official support for OpenMP on macOS/Xcode. It's unbelievable. I applied the workaround in the lfdfiles wheels, which ship with their own copy of OpenMP. I 'm not sure it's the right solution for this project.

The only thing I see is a bit distracting is the code for phasor_plot at the beginning, but as you said before, when that is implemented in phasorpy.plot, I'm guessing it will disappear from the tutorial and it will be cleaner.

Exactly, that function will be moved to phasorpy.plot. I have done some work on that. It will be in another PR.

src/phasorpy/phasor.py Show resolved Hide resolved
src/phasorpy/phasor.py Outdated Show resolved Hide resolved
src/phasorpy/phasor.py Outdated Show resolved Hide resolved
@cgohlke
Copy link
Member Author

cgohlke commented Feb 16, 2024

Hi @bruno-pannunzio Do you think this can be merged?

@bruno-pannunzio
Copy link
Contributor

@cgohlke I was just going to comment that I believe this can be merged and we can keep working/discussing the points were we can have more input.

@bruno-pannunzio bruno-pannunzio self-requested a review February 16, 2024 15:58
@cgohlke cgohlke merged commit 66e8af2 into phasorpy:main Feb 16, 2024
13 checks passed
@cgohlke cgohlke deleted the phasor_from_lifetime branch February 16, 2024 16:10
@cgohlke
Copy link
Member Author

cgohlke commented Feb 16, 2024

Btw, wheels for the main branch are available at https://github.com/phasorpy/phasorpy/actions/workflows/build_wheels.yml. Just follow the latest workflow run and go to artifacts, for example, https://github.com/phasorpy/phasorpy/actions/runs/7933102707#artifacts

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