Skip to content

Commit 17f6024

Browse files
committed
1 parent 4e9f922 commit 17f6024

375 files changed

Lines changed: 107441 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/v0.8/.buildinfo

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Sphinx build info version 1
2+
# This file records the configuration used when building these files. When it is not found, a full rebuild will be done.
3+
config: 9c22ae365d00f43f098c93e3a47a0b47
4+
tags: d77d1c0d9ca2f4c8421862c7c5a0d620
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"\n# Cursors\n\nAn introduction to selecting phasor coordinates using cursors.\n"
8+
]
9+
},
10+
{
11+
"cell_type": "markdown",
12+
"metadata": {},
13+
"source": [
14+
"Import required modules, functions, and classes:\n\n"
15+
]
16+
},
17+
{
18+
"cell_type": "code",
19+
"execution_count": null,
20+
"metadata": {
21+
"collapsed": false
22+
},
23+
"outputs": [],
24+
"source": [
25+
"from phasorpy.color import CATEGORICAL\nfrom phasorpy.cursor import (\n mask_from_circular_cursor,\n mask_from_elliptic_cursor,\n mask_from_polar_cursor,\n pseudo_color,\n)\nfrom phasorpy.datasets import fetch\nfrom phasorpy.filter import phasor_threshold\nfrom phasorpy.io import signal_from_lsm\nfrom phasorpy.phasor import phasor_from_signal\nfrom phasorpy.plot import PhasorPlot, plot_image"
26+
]
27+
},
28+
{
29+
"cell_type": "markdown",
30+
"metadata": {},
31+
"source": [
32+
"Open a hyperspectral dataset used throughout this tutorial:\n\n"
33+
]
34+
},
35+
{
36+
"cell_type": "code",
37+
"execution_count": null,
38+
"metadata": {
39+
"collapsed": false
40+
},
41+
"outputs": [],
42+
"source": [
43+
"signal = signal_from_lsm(fetch('paramecium.lsm'))\nmean, real, imag = phasor_from_signal(signal, axis=0)\n\n# remove coordinates with zero intensity\nmean_thresholded, real, imag = phasor_threshold(mean, real, imag, mean_min=1)"
44+
]
45+
},
46+
{
47+
"cell_type": "markdown",
48+
"metadata": {},
49+
"source": [
50+
"## Circular cursors\n\nUse circular cursors to mask regions of interest in the phasor space:\n\n"
51+
]
52+
},
53+
{
54+
"cell_type": "code",
55+
"execution_count": null,
56+
"metadata": {
57+
"collapsed": false
58+
},
59+
"outputs": [],
60+
"source": [
61+
"cursor_real = [-0.33, 0.54]\ncursor_imag = [-0.72, -0.74]\nradius = [0.2, 0.22]\n\ncircular_mask = mask_from_circular_cursor(\n real, imag, cursor_real, cursor_imag, radius=radius\n)"
62+
]
63+
},
64+
{
65+
"cell_type": "markdown",
66+
"metadata": {},
67+
"source": [
68+
"Show the circular cursors in a phasor plot:\n\n"
69+
]
70+
},
71+
{
72+
"cell_type": "code",
73+
"execution_count": null,
74+
"metadata": {
75+
"collapsed": false
76+
},
77+
"outputs": [],
78+
"source": [
79+
"plot = PhasorPlot(allquadrants=True, title='Circular cursors')\nplot.hist2d(real, imag, cmap='Greys')\nplot.cursor(\n cursor_real,\n cursor_imag,\n radius=radius,\n color=CATEGORICAL[:2],\n label=['cursor 0', 'cursor 1'],\n)\nplot.show()"
80+
]
81+
},
82+
{
83+
"cell_type": "markdown",
84+
"metadata": {},
85+
"source": [
86+
"The cursor masks can be blended to produce a pseudo-colored image:\n\n"
87+
]
88+
},
89+
{
90+
"cell_type": "code",
91+
"execution_count": null,
92+
"metadata": {
93+
"collapsed": false
94+
},
95+
"outputs": [],
96+
"source": [
97+
"pseudo_color_image = pseudo_color(*circular_mask)\n\nplot_image(\n pseudo_color_image, title='Pseudo-color image from circular cursors'\n)"
98+
]
99+
},
100+
{
101+
"cell_type": "markdown",
102+
"metadata": {},
103+
"source": [
104+
"## Elliptic cursors\n\nUse elliptic cursors to mask more defined regions of interest in the\nphasor space:\n\n"
105+
]
106+
},
107+
{
108+
"cell_type": "code",
109+
"execution_count": null,
110+
"metadata": {
111+
"collapsed": false
112+
},
113+
"outputs": [],
114+
"source": [
115+
"radius = [0.1, 0.06]\nradius_minor = [0.3, 0.25]\n\nelliptic_mask = mask_from_elliptic_cursor(\n real,\n imag,\n cursor_real,\n cursor_imag,\n radius=radius,\n radius_minor=radius_minor,\n)"
116+
]
117+
},
118+
{
119+
"cell_type": "markdown",
120+
"metadata": {},
121+
"source": [
122+
"Show the elliptic cursors in a phasor plot:\n\n"
123+
]
124+
},
125+
{
126+
"cell_type": "code",
127+
"execution_count": null,
128+
"metadata": {
129+
"collapsed": false
130+
},
131+
"outputs": [],
132+
"source": [
133+
"plot = PhasorPlot(allquadrants=True, title='Elliptic cursors')\nplot.hist2d(real, imag, cmap='Greys')\nplot.cursor(\n cursor_real,\n cursor_imag,\n radius=radius,\n radius_minor=radius_minor,\n color=CATEGORICAL[:2],\n label=['cursor 0', 'cursor 1'],\n)\nplot.show()"
134+
]
135+
},
136+
{
137+
"cell_type": "markdown",
138+
"metadata": {},
139+
"source": [
140+
"The mean intensity image can be used as a base layer to overlay\nthe masks from the elliptic cursors:\n\n"
141+
]
142+
},
143+
{
144+
"cell_type": "code",
145+
"execution_count": null,
146+
"metadata": {
147+
"collapsed": false
148+
},
149+
"outputs": [],
150+
"source": [
151+
"pseudo_color_image = pseudo_color(*elliptic_mask, intensity=mean)\n\nplot_image(\n pseudo_color_image,\n title='Pseudo-color image from elliptic cursors and intensity',\n)"
152+
]
153+
},
154+
{
155+
"cell_type": "markdown",
156+
"metadata": {},
157+
"source": [
158+
"## Polar cursors\n\nCreate a mask with two ranges of phase and modulation values:\n\n"
159+
]
160+
},
161+
{
162+
"cell_type": "code",
163+
"execution_count": null,
164+
"metadata": {
165+
"collapsed": false
166+
},
167+
"outputs": [],
168+
"source": [
169+
"phase_min = [-2.27, -1.22]\nphase_max = [-1.57, -0.70]\nmodulation_min = [0.7, 0.8]\nmodulation_max = [0.9, 1.0]\n\npolar_mask = mask_from_polar_cursor(\n real, imag, phase_min, phase_max, modulation_min, modulation_max\n)"
170+
]
171+
},
172+
{
173+
"cell_type": "markdown",
174+
"metadata": {},
175+
"source": [
176+
"Show the polar cursors in a phasor plot:\n\n"
177+
]
178+
},
179+
{
180+
"cell_type": "code",
181+
"execution_count": null,
182+
"metadata": {
183+
"collapsed": false
184+
},
185+
"outputs": [],
186+
"source": [
187+
"plot = PhasorPlot(allquadrants=True, title='Polar cursors')\nplot.hist2d(real, imag, cmap='Greys')\nplot.polar_cursor(\n phase=phase_min,\n phase_limit=phase_max,\n modulation=modulation_min,\n modulation_limit=modulation_max,\n color=CATEGORICAL[2:4],\n label=['cursor 0', 'cursor 1'],\n)\nplot.show()"
188+
]
189+
},
190+
{
191+
"cell_type": "markdown",
192+
"metadata": {},
193+
"source": [
194+
"The thresholded mean intensity image can be used as a base layer to\noverlay the masks from the polar cursors. Values below the threshold are\ntransparent (white):\n\n"
195+
]
196+
},
197+
{
198+
"cell_type": "code",
199+
"execution_count": null,
200+
"metadata": {
201+
"collapsed": false
202+
},
203+
"outputs": [],
204+
"source": [
205+
"pseudo_color_image = pseudo_color(\n *polar_mask, intensity=mean_thresholded, colors=CATEGORICAL[2:]\n)\n\nplot_image(\n pseudo_color_image,\n title='Pseudo-color image from\\npolar cursors and thresholded intensity',\n)"
206+
]
207+
},
208+
{
209+
"cell_type": "markdown",
210+
"metadata": {},
211+
"source": [
212+
"sphinx_gallery_thumbnail_number = 1\nmypy: allow-untyped-defs, allow-untyped-calls\nmypy: disable-error-code=\"arg-type\"\n\n"
213+
]
214+
}
215+
],
216+
"metadata": {
217+
"kernelspec": {
218+
"display_name": "Python 3",
219+
"language": "python",
220+
"name": "python3"
221+
},
222+
"language_info": {
223+
"codemirror_mode": {
224+
"name": "ipython",
225+
"version": 3
226+
},
227+
"file_extension": ".py",
228+
"mimetype": "text/x-python",
229+
"name": "python",
230+
"nbconvert_exporter": "python",
231+
"pygments_lexer": "ipython3",
232+
"version": "3.14.0"
233+
}
234+
},
235+
"nbformat": 4,
236+
"nbformat_minor": 0
237+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"\n# Benchmark phasor_from_signal\n\nBenchmark the ``phasor_from_signal`` function.\n\nThe :py:func:`phasorpy.phasor.phasor_from_signal` function to calculate phasor\ncoordinates from time-resolved or spectral signals can operate in two modes:\n\n- using an internal Cython function optimized for calculating a small number\n of harmonics, optionally using multiple threads.\n\n- using a real forward Fast Fourier Transform (FFT), ``numpy.fft.rfft`` or\n a drop-in replacement function like ``scipy.fft.rfft``\n or ``mkl_fft.interfaces.numpy_fft.rfft``.\n\nThis tutorial compares the performance of the two modes.\n\nImport required modules and functions:\n"
8+
]
9+
},
10+
{
11+
"cell_type": "code",
12+
"execution_count": null,
13+
"metadata": {
14+
"collapsed": false
15+
},
16+
"outputs": [],
17+
"source": [
18+
"from timeit import timeit\n\nimport numpy\nfrom numpy.fft import rfft as numpy_fft # noqa\n\nfrom phasorpy.phasor import phasor_from_signal # noqa\nfrom phasorpy.utils import number_threads\n\ntry:\n from scipy.fft import rfft as scipy_fft\nexcept ImportError:\n scipy_fft = None\n\ntry:\n from mkl_fft.interfaces.numpy_fft import rfft as mkl_fft\nexcept ImportError:\n mkl_fft = None"
19+
]
20+
},
21+
{
22+
"cell_type": "markdown",
23+
"metadata": {},
24+
"source": [
25+
"## Run benchmark\n\nCreate a random signal with a size and dtype similar to real world data:\n\n"
26+
]
27+
},
28+
{
29+
"cell_type": "code",
30+
"execution_count": null,
31+
"metadata": {
32+
"collapsed": false
33+
},
34+
"outputs": [],
35+
"source": [
36+
"signal = numpy.random.default_rng(1).random((384, 384, 384))\nsignal += 1.1\nsignal *= 3723 # ~12 bit\nsignal = signal.astype(numpy.uint16) # 108 MB\nsignal[signal < 0.05] = 0.0 # 5% no signal"
37+
]
38+
},
39+
{
40+
"cell_type": "markdown",
41+
"metadata": {},
42+
"source": [
43+
"Print execution times depending on FFT function, axis, number of harmonics,\nand number of threads:\n\n"
44+
]
45+
},
46+
{
47+
"cell_type": "code",
48+
"execution_count": null,
49+
"metadata": {
50+
"collapsed": false
51+
},
52+
"outputs": [],
53+
"source": [
54+
"statement = \"\"\"\nphasor_from_signal(signal, axis=axis, harmonic=harmonic, **kwargs)\n\"\"\"\nnumber = 1 # how many times to execute statement\nref = None # reference duration\n\n\ndef print_(descr, t):\n print(f' {descr:20s}{t / number:>6.3f}s {t / ref:>6.2f}')\n\n\nfor harmonic in ([1], [1, 2, 3, 4, 5, 6, 7, 8]):\n print(f'harmonics {len(harmonic)}')\n for axis in (-1, 0, 2):\n print(f' axis {axis}')\n kwargs = {'use_fft': False, 'num_threads': 1}\n t = timeit(statement, number=number, globals=globals())\n if ref is None:\n ref = t\n print_('not_fft', t)\n\n num_threads = number_threads(0, 6)\n if num_threads > 1:\n kwargs = {'use_fft': False, 'num_threads': num_threads}\n t = timeit(statement, number=number, globals=globals())\n print_(f'not_fft ({num_threads} threads)', t)\n\n for fft_name in ('numpy_fft', 'scipy_fft', 'mkl_fft'):\n fft_func = globals()[fft_name]\n if fft_func is None:\n continue\n kwargs = {'use_fft': True, 'rfft': fft_func}\n t = timeit(statement, number=number, globals=globals())\n print_(f'{fft_name}', t)"
55+
]
56+
},
57+
{
58+
"cell_type": "markdown",
59+
"metadata": {},
60+
"source": [
61+
"For reference, the results on a Core i7-14700K CPU, Windows 11,\nPython 3.13.3, numpy 2.2.6, scipy 1.15.3, mkl-fft 1.3.14::\n\n harmonics 1\n axis -1\n not_fft 0.036s 1.00\n not_fft (6 threads) 0.006s 0.17\n numpy_fft 0.270s 7.60\n scipy_fft 0.236s 6.64\n mkl_fft 0.114s 3.20\n axis 0\n not_fft 0.139s 3.90\n not_fft (6 threads) 0.028s 0.78\n numpy_fft 0.591s 16.63\n scipy_fft 0.509s 14.32\n mkl_fft 0.148s 4.18\n axis 2\n not_fft 0.037s 1.03\n not_fft (6 threads) 0.006s 0.16\n numpy_fft 0.265s 7.47\n scipy_fft 0.239s 6.72\n mkl_fft 0.117s 3.29\n harmonics 8\n axis -1\n not_fft 0.284s 8.00\n not_fft (6 threads) 0.040s 1.13\n numpy_fft 0.279s 7.84\n scipy_fft 0.247s 6.94\n mkl_fft 0.129s 3.64\n axis 0\n not_fft 1.120s 31.52\n not_fft (6 threads) 0.280s 7.88\n numpy_fft 0.679s 19.11\n scipy_fft 0.525s 14.78\n mkl_fft 0.165s 4.65\n axis 2\n not_fft 0.285s 8.03\n not_fft (6 threads) 0.039s 1.10\n numpy_fft 0.278s 7.84\n scipy_fft 0.243s 6.85\n mkl_fft 0.131s 3.68\n\n"
62+
]
63+
},
64+
{
65+
"cell_type": "markdown",
66+
"metadata": {},
67+
"source": [
68+
"## Results\n\n- Using the Cython implementation is significantly faster than using the\n ``numpy.fft`` based implementation for single harmonics.\n- Using multiple threads can significantly speed up the Cython mode.\n- The FFT functions from ``scipy`` and ``mkl_fft`` outperform numpy.fft.\n Specifically, ``mkl_fft`` is very performant.\n- Using FFT becomes more competitive when calculating larger number of\n harmonics.\n- Computing over the last axis is significantly faster compared to the first\n axis. That is because the samples in the last dimension are contiguous,\n closer together in memory.\n\nNote that these results were obtained on a single dataset of random numbers.\n\n"
69+
]
70+
},
71+
{
72+
"cell_type": "markdown",
73+
"metadata": {},
74+
"source": [
75+
"## Conclusions\n\nUsing the Cython implementation is a reasonable default when calculating\na few harmonics. Using FFT is a better choice when computing large number\nof harmonics, especially with an optimized FFT function.\n\n"
76+
]
77+
},
78+
{
79+
"cell_type": "code",
80+
"execution_count": null,
81+
"metadata": {
82+
"collapsed": false
83+
},
84+
"outputs": [],
85+
"source": [
86+
"# mypy: allow-untyped-defs, allow-untyped-calls\n# mypy: disable-error-code=\"arg-type\""
87+
]
88+
}
89+
],
90+
"metadata": {
91+
"kernelspec": {
92+
"display_name": "Python 3",
93+
"language": "python",
94+
"name": "python3"
95+
},
96+
"language_info": {
97+
"codemirror_mode": {
98+
"name": "ipython",
99+
"version": 3
100+
},
101+
"file_extension": ".py",
102+
"mimetype": "text/x-python",
103+
"name": "python",
104+
"nbconvert_exporter": "python",
105+
"pygments_lexer": "ipython3",
106+
"version": "3.14.0"
107+
}
108+
},
109+
"nbformat": 4,
110+
"nbformat_minor": 0
111+
}

0 commit comments

Comments
 (0)