phasorpy 0.5__cp313-cp313-win_arm64.whl → 0.6__cp313-cp313-win_arm64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- phasorpy/__init__.py +2 -3
- phasorpy/_phasorpy.cp313-win_arm64.pyd +0 -0
- phasorpy/_phasorpy.pyx +185 -2
- phasorpy/_utils.py +121 -9
- phasorpy/cli.py +56 -3
- phasorpy/cluster.py +42 -6
- phasorpy/components.py +226 -55
- phasorpy/experimental.py +312 -0
- phasorpy/io/__init__.py +137 -0
- phasorpy/io/_flimlabs.py +350 -0
- phasorpy/io/_leica.py +329 -0
- phasorpy/io/_ometiff.py +445 -0
- phasorpy/io/_other.py +782 -0
- phasorpy/io/_simfcs.py +627 -0
- phasorpy/phasor.py +307 -1
- phasorpy/plot/__init__.py +27 -0
- phasorpy/plot/_functions.py +717 -0
- phasorpy/plot/_lifetime_plots.py +553 -0
- phasorpy/plot/_phasorplot.py +1119 -0
- phasorpy/plot/_phasorplot_fret.py +559 -0
- phasorpy/utils.py +84 -296
- {phasorpy-0.5.dist-info → phasorpy-0.6.dist-info}/METADATA +2 -2
- phasorpy-0.6.dist-info/RECORD +34 -0
- {phasorpy-0.5.dist-info → phasorpy-0.6.dist-info}/WHEEL +1 -1
- phasorpy/_io.py +0 -2655
- phasorpy/io.py +0 -9
- phasorpy/plot.py +0 -2318
- phasorpy/version.py +0 -80
- phasorpy-0.5.dist-info/RECORD +0 -26
- {phasorpy-0.5.dist-info → phasorpy-0.6.dist-info}/entry_points.txt +0 -0
- {phasorpy-0.5.dist-info → phasorpy-0.6.dist-info}/licenses/LICENSE.txt +0 -0
- {phasorpy-0.5.dist-info → phasorpy-0.6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,717 @@
|
|
1
|
+
"""Higher level plot functions."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
__all__ = [
|
6
|
+
'plot_histograms',
|
7
|
+
'plot_image',
|
8
|
+
'plot_phasor',
|
9
|
+
'plot_phasor_image',
|
10
|
+
'plot_polar_frequency',
|
11
|
+
'plot_signal_image',
|
12
|
+
]
|
13
|
+
|
14
|
+
import warnings
|
15
|
+
from collections.abc import Sequence
|
16
|
+
from typing import TYPE_CHECKING
|
17
|
+
|
18
|
+
if TYPE_CHECKING:
|
19
|
+
from .._typing import Any, ArrayLike, NDArray, Literal
|
20
|
+
|
21
|
+
from matplotlib.axes import Axes
|
22
|
+
from matplotlib.image import AxesImage
|
23
|
+
|
24
|
+
import numpy
|
25
|
+
from matplotlib import pyplot
|
26
|
+
from matplotlib.gridspec import GridSpec
|
27
|
+
|
28
|
+
from .._utils import parse_kwargs, parse_signal_axis, update_kwargs
|
29
|
+
from ._phasorplot import PhasorPlot
|
30
|
+
|
31
|
+
|
32
|
+
def plot_phasor(
|
33
|
+
real: ArrayLike,
|
34
|
+
imag: ArrayLike,
|
35
|
+
/,
|
36
|
+
*,
|
37
|
+
style: Literal['plot', 'hist2d', 'contour'] | None = None,
|
38
|
+
allquadrants: bool | None = None,
|
39
|
+
frequency: float | None = None,
|
40
|
+
show: bool = True,
|
41
|
+
**kwargs: Any,
|
42
|
+
) -> None:
|
43
|
+
"""Plot phasor coordinates.
|
44
|
+
|
45
|
+
A simplified interface to the :py:class:`PhasorPlot` class.
|
46
|
+
|
47
|
+
Parameters
|
48
|
+
----------
|
49
|
+
real : array_like
|
50
|
+
Real component of phasor coordinates.
|
51
|
+
imag : array_like
|
52
|
+
Imaginary component of phasor coordinates.
|
53
|
+
Must be of same shape as `real`.
|
54
|
+
style : {'plot', 'hist2d', 'contour'}, optional
|
55
|
+
Method used to plot phasor coordinates.
|
56
|
+
By default, if the number of coordinates are less than 65536
|
57
|
+
and the arrays are less than three-dimensional, `'plot'` style is used,
|
58
|
+
else `'hist2d'`.
|
59
|
+
allquadrants : bool, optional
|
60
|
+
Show all quadrants of phasor space.
|
61
|
+
By default, only the first quadrant is shown.
|
62
|
+
frequency : float, optional
|
63
|
+
Frequency of phasor plot.
|
64
|
+
If provided, the universal semicircle is labeled with reference
|
65
|
+
lifetimes.
|
66
|
+
show : bool, optional, default: True
|
67
|
+
Display figure.
|
68
|
+
**kwargs
|
69
|
+
Additional parguments passed to :py:class:`PhasorPlot`,
|
70
|
+
:py:meth:`PhasorPlot.plot`, :py:meth:`PhasorPlot.hist2d`, or
|
71
|
+
:py:meth:`PhasorPlot.contour` depending on `style`.
|
72
|
+
|
73
|
+
See Also
|
74
|
+
--------
|
75
|
+
phasorpy.plot.PhasorPlot
|
76
|
+
:ref:`sphx_glr_tutorials_api_phasorpy_phasorplot.py`
|
77
|
+
|
78
|
+
"""
|
79
|
+
init_kwargs = parse_kwargs(
|
80
|
+
kwargs,
|
81
|
+
'ax',
|
82
|
+
'title',
|
83
|
+
'xlabel',
|
84
|
+
'ylabel',
|
85
|
+
'xlim',
|
86
|
+
'ylim',
|
87
|
+
'xticks',
|
88
|
+
'yticks',
|
89
|
+
'grid',
|
90
|
+
)
|
91
|
+
|
92
|
+
real = numpy.asanyarray(real)
|
93
|
+
imag = numpy.asanyarray(imag)
|
94
|
+
plot = PhasorPlot(
|
95
|
+
frequency=frequency, allquadrants=allquadrants, **init_kwargs
|
96
|
+
)
|
97
|
+
if style is None:
|
98
|
+
style = 'plot' if real.size < 65536 and real.ndim < 3 else 'hist2d'
|
99
|
+
if style == 'plot':
|
100
|
+
plot.plot(real, imag, **kwargs)
|
101
|
+
elif style == 'hist2d':
|
102
|
+
plot.hist2d(real, imag, **kwargs)
|
103
|
+
elif style == 'contour':
|
104
|
+
plot.contour(real, imag, **kwargs)
|
105
|
+
else:
|
106
|
+
raise ValueError(f'invalid {style=}')
|
107
|
+
if show:
|
108
|
+
plot.show()
|
109
|
+
|
110
|
+
|
111
|
+
def plot_phasor_image(
|
112
|
+
mean: ArrayLike | None,
|
113
|
+
real: ArrayLike,
|
114
|
+
imag: ArrayLike,
|
115
|
+
*,
|
116
|
+
harmonics: int | None = None,
|
117
|
+
percentile: float | None = None,
|
118
|
+
title: str | None = None,
|
119
|
+
show: bool = True,
|
120
|
+
**kwargs: Any,
|
121
|
+
) -> None:
|
122
|
+
"""Plot phasor coordinates as images.
|
123
|
+
|
124
|
+
Preview phasor coordinates from time-resolved or hyperspectral
|
125
|
+
image stacks as returned by :py:func:`phasorpy.phasor.phasor_from_signal`.
|
126
|
+
|
127
|
+
The last two axes are assumed to be the image axes.
|
128
|
+
Harmonics, if any, are in the first axes of `real` and `imag`.
|
129
|
+
Other axes are averaged for display.
|
130
|
+
|
131
|
+
Parameters
|
132
|
+
----------
|
133
|
+
mean : array_like
|
134
|
+
Image average. Must be two or more dimensional, or None.
|
135
|
+
real : array_like
|
136
|
+
Image of real component of phasor coordinates.
|
137
|
+
The last dimensions must match shape of `mean`.
|
138
|
+
imag : array_like
|
139
|
+
Image of imaginary component of phasor coordinates.
|
140
|
+
Must be same shape as `real`.
|
141
|
+
harmonics : int, optional
|
142
|
+
Number of harmonics to display.
|
143
|
+
If `mean` is None, a nonzero value indicates the presence of harmonics
|
144
|
+
in the first axes of `mean` and `real`. Else, the presence of harmonics
|
145
|
+
is determined from the shapes of `mean` and `real`.
|
146
|
+
By default, up to 4 harmonics are displayed.
|
147
|
+
percentile : float, optional
|
148
|
+
The (q, 100-q) percentiles of image data are covered by colormaps.
|
149
|
+
By default, the complete value range of `mean` is covered,
|
150
|
+
for `real` and `imag` the range [-1, 1].
|
151
|
+
title : str, optional
|
152
|
+
Figure title.
|
153
|
+
show : bool, optional, default: True
|
154
|
+
Display figure.
|
155
|
+
**kwargs
|
156
|
+
Additional arguments passed to :func:`matplotlib.pyplot.imshow`.
|
157
|
+
|
158
|
+
Raises
|
159
|
+
------
|
160
|
+
ValueError
|
161
|
+
The shapes of `mean`, `real`, and `image` do not match.
|
162
|
+
Percentile is out of range.
|
163
|
+
|
164
|
+
"""
|
165
|
+
update_kwargs(kwargs, interpolation='nearest')
|
166
|
+
cmap = kwargs.pop('cmap', None)
|
167
|
+
shape = None
|
168
|
+
|
169
|
+
if mean is not None:
|
170
|
+
mean = numpy.asarray(mean)
|
171
|
+
if mean.ndim < 2:
|
172
|
+
raise ValueError(f'not an image {mean.ndim=} < 2')
|
173
|
+
shape = mean.shape
|
174
|
+
mean = mean.reshape(-1, *mean.shape[-2:])
|
175
|
+
if mean.shape[0] == 1:
|
176
|
+
mean = mean[0]
|
177
|
+
else:
|
178
|
+
mean = numpy.nanmean(mean, axis=0)
|
179
|
+
|
180
|
+
real = numpy.asarray(real)
|
181
|
+
imag = numpy.asarray(imag)
|
182
|
+
if real.shape != imag.shape:
|
183
|
+
raise ValueError(f'{real.shape=} != {imag.shape=}')
|
184
|
+
if real.ndim < 2:
|
185
|
+
raise ValueError(f'not an image {real.ndim=} < 2')
|
186
|
+
|
187
|
+
if (shape is not None and real.shape[1:] == shape) or (
|
188
|
+
shape is None and harmonics
|
189
|
+
):
|
190
|
+
# first image dimension contains harmonics
|
191
|
+
if real.ndim < 3:
|
192
|
+
raise ValueError(f'not a multi-harmonic image {real.shape=}')
|
193
|
+
nh = real.shape[0] # number harmonics
|
194
|
+
elif shape is None or shape == real.shape:
|
195
|
+
# single harmonic
|
196
|
+
nh = 1
|
197
|
+
else:
|
198
|
+
raise ValueError(f'shape mismatch {real.shape[1:]=} != {shape}')
|
199
|
+
|
200
|
+
real = real.reshape(nh, -1, *real.shape[-2:])
|
201
|
+
imag = imag.reshape(nh, -1, *imag.shape[-2:])
|
202
|
+
if real.shape[1] == 1:
|
203
|
+
real = real[:, 0]
|
204
|
+
imag = imag[:, 0]
|
205
|
+
else:
|
206
|
+
real = numpy.nanmean(real, axis=1)
|
207
|
+
imag = numpy.nanmean(imag, axis=1)
|
208
|
+
|
209
|
+
# for MyPy
|
210
|
+
assert isinstance(mean, numpy.ndarray) or mean is None
|
211
|
+
assert isinstance(real, numpy.ndarray)
|
212
|
+
assert isinstance(imag, numpy.ndarray)
|
213
|
+
|
214
|
+
# limit number of displayed harmonics
|
215
|
+
nh = min(4 if harmonics is None else harmonics, nh)
|
216
|
+
|
217
|
+
# create figure with size depending on image aspect and number of harmonics
|
218
|
+
fig = pyplot.figure(layout='constrained')
|
219
|
+
w, h = fig.get_size_inches()
|
220
|
+
aspect = min(1.0, max(0.5, real.shape[-2] / real.shape[-1]))
|
221
|
+
fig.set_size_inches(w, h * 0.4 * aspect * nh + h * 0.25 * aspect)
|
222
|
+
gs = GridSpec(nh, 2 if mean is None else 3, figure=fig)
|
223
|
+
if title:
|
224
|
+
fig.suptitle(title)
|
225
|
+
|
226
|
+
if mean is not None:
|
227
|
+
_imshow(
|
228
|
+
fig.add_subplot(gs[0, 0]),
|
229
|
+
mean,
|
230
|
+
percentile=percentile,
|
231
|
+
vmin=None,
|
232
|
+
vmax=None,
|
233
|
+
cmap=cmap,
|
234
|
+
axis=True,
|
235
|
+
title='mean',
|
236
|
+
**kwargs,
|
237
|
+
)
|
238
|
+
|
239
|
+
if percentile is None:
|
240
|
+
vmin = -1.0
|
241
|
+
vmax = 1.0
|
242
|
+
if cmap is None:
|
243
|
+
cmap = 'coolwarm_r'
|
244
|
+
else:
|
245
|
+
vmin = None
|
246
|
+
vmax = None
|
247
|
+
|
248
|
+
for h in range(nh):
|
249
|
+
axs = []
|
250
|
+
ax = fig.add_subplot(gs[h, -2])
|
251
|
+
axs.append(ax)
|
252
|
+
_imshow(
|
253
|
+
ax,
|
254
|
+
real[h],
|
255
|
+
percentile=percentile,
|
256
|
+
vmin=vmin,
|
257
|
+
vmax=vmax,
|
258
|
+
cmap=cmap,
|
259
|
+
axis=mean is None and h == 0,
|
260
|
+
colorbar=percentile is not None,
|
261
|
+
title=None if h else 'G, real',
|
262
|
+
**kwargs,
|
263
|
+
)
|
264
|
+
|
265
|
+
ax = fig.add_subplot(gs[h, -1])
|
266
|
+
axs.append(ax)
|
267
|
+
pos = _imshow(
|
268
|
+
ax,
|
269
|
+
imag[h],
|
270
|
+
percentile=percentile,
|
271
|
+
vmin=vmin,
|
272
|
+
vmax=vmax,
|
273
|
+
cmap=cmap,
|
274
|
+
axis=False,
|
275
|
+
colorbar=percentile is not None,
|
276
|
+
title=None if h else 'S, imag',
|
277
|
+
**kwargs,
|
278
|
+
)
|
279
|
+
if percentile is None and h == 0:
|
280
|
+
fig.colorbar(pos, ax=axs, shrink=0.4, location='bottom')
|
281
|
+
|
282
|
+
if show:
|
283
|
+
pyplot.show()
|
284
|
+
|
285
|
+
|
286
|
+
def plot_signal_image(
|
287
|
+
signal: ArrayLike,
|
288
|
+
/,
|
289
|
+
*,
|
290
|
+
axis: int | str | None = None,
|
291
|
+
percentile: float | Sequence[float] | None = None,
|
292
|
+
title: str | None = None,
|
293
|
+
xlabel: str | None = None,
|
294
|
+
show: bool = True,
|
295
|
+
**kwargs: Any,
|
296
|
+
) -> None:
|
297
|
+
"""Plot average image and signal along axis.
|
298
|
+
|
299
|
+
Preview time-resolved or hyperspectral image stacks to be anayzed with
|
300
|
+
:py:func:`phasorpy.phasor.phasor_from_signal`.
|
301
|
+
|
302
|
+
The last two axes, excluding `axis`, are assumed to be the image axes.
|
303
|
+
Other axes are averaged for image display.
|
304
|
+
|
305
|
+
Parameters
|
306
|
+
----------
|
307
|
+
signal : array_like
|
308
|
+
Image stack. Must be three or more dimensional.
|
309
|
+
axis : int or str, optional
|
310
|
+
Axis over which phasor coordinates would be computed.
|
311
|
+
By default, the 'H' or 'C' axes if signal contains such dimension
|
312
|
+
names, else the last axis (-1).
|
313
|
+
percentile : float or [float, float], optional
|
314
|
+
The [q, 100-q] percentiles of image data are covered by colormaps.
|
315
|
+
By default, the complete value range of `mean` is covered,
|
316
|
+
for `real` and `imag` the range [-1, 1].
|
317
|
+
title : str, optional
|
318
|
+
Figure title.
|
319
|
+
xlabel : str, optional
|
320
|
+
Label of axis over which phasor coordinates would be computed.
|
321
|
+
show : bool, optional, default: True
|
322
|
+
Display figure.
|
323
|
+
**kwargs
|
324
|
+
Additional arguments passed to :func:`matplotlib.pyplot.imshow`.
|
325
|
+
|
326
|
+
Raises
|
327
|
+
------
|
328
|
+
ValueError
|
329
|
+
Signal is not an image stack.
|
330
|
+
Percentile is out of range.
|
331
|
+
|
332
|
+
"""
|
333
|
+
# TODO: add option to separate channels?
|
334
|
+
# TODO: add option to plot non-images?
|
335
|
+
|
336
|
+
axis, axis_label = parse_signal_axis(signal, axis)
|
337
|
+
if (
|
338
|
+
axis_label
|
339
|
+
and hasattr(signal, 'coords')
|
340
|
+
and axis_label in signal.coords
|
341
|
+
):
|
342
|
+
axis_coords = signal.coords[axis_label]
|
343
|
+
else:
|
344
|
+
axis_coords = None
|
345
|
+
|
346
|
+
update_kwargs(kwargs, interpolation='nearest')
|
347
|
+
signal = numpy.asarray(signal)
|
348
|
+
if signal.ndim < 3:
|
349
|
+
raise ValueError(f'not an image stack {signal.ndim=} < 3')
|
350
|
+
|
351
|
+
axis %= signal.ndim
|
352
|
+
|
353
|
+
# for MyPy
|
354
|
+
assert isinstance(signal, numpy.ndarray)
|
355
|
+
|
356
|
+
fig = pyplot.figure(layout='constrained')
|
357
|
+
if title:
|
358
|
+
fig.suptitle(title)
|
359
|
+
w, h = fig.get_size_inches()
|
360
|
+
fig.set_size_inches(w, h * 0.7)
|
361
|
+
gs = GridSpec(1, 2, figure=fig, width_ratios=(1, 1))
|
362
|
+
|
363
|
+
# histogram
|
364
|
+
axes = list(range(signal.ndim))
|
365
|
+
del axes[axis]
|
366
|
+
ax = fig.add_subplot(gs[0, 1])
|
367
|
+
|
368
|
+
if axis_coords is not None:
|
369
|
+
ax.set_title(f'{axis=} {axis_label!r}')
|
370
|
+
ax.plot(axis_coords, numpy.nanmean(signal, axis=tuple(axes)))
|
371
|
+
else:
|
372
|
+
ax.set_title(f'{axis=}')
|
373
|
+
ax.plot(numpy.nanmean(signal, axis=tuple(axes)))
|
374
|
+
|
375
|
+
ax.set_ylim(kwargs.get('vmin', None), kwargs.get('vmax', None))
|
376
|
+
|
377
|
+
if xlabel is not None:
|
378
|
+
ax.set_xlabel(xlabel)
|
379
|
+
|
380
|
+
# image
|
381
|
+
axes = list(sorted(axes[:-2] + [axis]))
|
382
|
+
ax = fig.add_subplot(gs[0, 0])
|
383
|
+
_imshow(
|
384
|
+
ax,
|
385
|
+
numpy.nanmean(signal, axis=tuple(axes)),
|
386
|
+
percentile=percentile,
|
387
|
+
shrink=0.5,
|
388
|
+
title='mean',
|
389
|
+
**kwargs,
|
390
|
+
)
|
391
|
+
|
392
|
+
if show:
|
393
|
+
pyplot.show()
|
394
|
+
|
395
|
+
|
396
|
+
def plot_image(
|
397
|
+
*images: ArrayLike,
|
398
|
+
percentile: float | None = None,
|
399
|
+
columns: int | None = None,
|
400
|
+
title: str | None = None,
|
401
|
+
labels: Sequence[str | None] | None = None,
|
402
|
+
show: bool = True,
|
403
|
+
**kwargs: Any,
|
404
|
+
) -> None:
|
405
|
+
"""Plot images.
|
406
|
+
|
407
|
+
Parameters
|
408
|
+
----------
|
409
|
+
*images : array_like
|
410
|
+
Images to be plotted. Must be two or more dimensional.
|
411
|
+
The last two axes are assumed to be the image axes.
|
412
|
+
Other axes are averaged for display.
|
413
|
+
Three-dimensional images with last axis size of three or four
|
414
|
+
are plotted as RGB(A) images.
|
415
|
+
percentile : float, optional
|
416
|
+
The (q, 100-q) percentiles of image data are covered by colormaps.
|
417
|
+
By default, the complete value range is covered.
|
418
|
+
Does not apply to RGB images.
|
419
|
+
columns : int, optional
|
420
|
+
Number of columns in figure.
|
421
|
+
By default, up to four columns are used.
|
422
|
+
title : str, optional
|
423
|
+
Figure title.
|
424
|
+
labels : sequence of str, optional
|
425
|
+
Labels for each image.
|
426
|
+
show : bool, optional, default: True
|
427
|
+
Display figure.
|
428
|
+
**kwargs
|
429
|
+
Additional arguments passed to :func:`matplotlib.pyplot.imshow`.
|
430
|
+
|
431
|
+
Raises
|
432
|
+
------
|
433
|
+
ValueError
|
434
|
+
Percentile is out of range.
|
435
|
+
|
436
|
+
"""
|
437
|
+
update_kwargs(
|
438
|
+
kwargs, interpolation='nearest', location='right', shrink=0.5
|
439
|
+
)
|
440
|
+
cmap = kwargs.pop('cmap', None)
|
441
|
+
figsize = kwargs.pop('figsize', None)
|
442
|
+
subplot_kw = kwargs.pop('subplot_kw', {})
|
443
|
+
location = kwargs['location']
|
444
|
+
allrgb = True
|
445
|
+
|
446
|
+
arrays = []
|
447
|
+
shape = [1, 1]
|
448
|
+
for image in images:
|
449
|
+
image = numpy.asarray(image)
|
450
|
+
if image.ndim < 2:
|
451
|
+
raise ValueError(f'not an image {image.ndim=} < 2')
|
452
|
+
if image.ndim == 3 and image.shape[2] in {3, 4}:
|
453
|
+
# RGB(A)
|
454
|
+
pass
|
455
|
+
else:
|
456
|
+
allrgb = False
|
457
|
+
image = image.reshape(-1, *image.shape[-2:])
|
458
|
+
if image.shape[0] == 1:
|
459
|
+
image = image[0]
|
460
|
+
else:
|
461
|
+
with warnings.catch_warnings():
|
462
|
+
warnings.filterwarnings('ignore', category=RuntimeWarning)
|
463
|
+
image = numpy.nanmean(image, axis=0)
|
464
|
+
assert isinstance(image, numpy.ndarray)
|
465
|
+
for i in (-1, -2):
|
466
|
+
if image.shape[i] > shape[i]:
|
467
|
+
shape[i] = image.shape[i]
|
468
|
+
arrays.append(image)
|
469
|
+
|
470
|
+
if columns is None:
|
471
|
+
n = len(arrays)
|
472
|
+
if n < 3:
|
473
|
+
columns = n
|
474
|
+
elif n < 5:
|
475
|
+
columns = 2
|
476
|
+
elif n < 7:
|
477
|
+
columns = 3
|
478
|
+
else:
|
479
|
+
columns = 4
|
480
|
+
rows = int(numpy.ceil(len(arrays) / columns))
|
481
|
+
|
482
|
+
vmin = None
|
483
|
+
vmax = None
|
484
|
+
if percentile is None:
|
485
|
+
vmin = kwargs.pop('vmin', None)
|
486
|
+
vmax = kwargs.pop('vmax', None)
|
487
|
+
if vmin is None:
|
488
|
+
vmin = numpy.inf
|
489
|
+
for image in images:
|
490
|
+
vmin = min(vmin, numpy.nanmin(image))
|
491
|
+
if vmin == numpy.inf:
|
492
|
+
vmin = None
|
493
|
+
if vmax is None:
|
494
|
+
vmax = -numpy.inf
|
495
|
+
for image in images:
|
496
|
+
vmax = max(vmax, numpy.nanmax(image))
|
497
|
+
if vmax == -numpy.inf:
|
498
|
+
vmax = None
|
499
|
+
|
500
|
+
# create figure with size depending on image aspect
|
501
|
+
fig = pyplot.figure(layout='constrained', figsize=figsize)
|
502
|
+
if figsize is None:
|
503
|
+
# TODO: find optimal figure height as a function of
|
504
|
+
# number of rows and columns, image shapes, labels, and colorbar
|
505
|
+
# presence and placements.
|
506
|
+
if allrgb:
|
507
|
+
hadd = 0.0
|
508
|
+
elif location == 'right':
|
509
|
+
hadd = 0.5
|
510
|
+
else:
|
511
|
+
hadd = 1.2
|
512
|
+
if labels is not None:
|
513
|
+
hadd += 0.3 * rows
|
514
|
+
w, h = fig.get_size_inches()
|
515
|
+
aspect = min(1.0, max(0.5, shape[0] / shape[1]))
|
516
|
+
fig.set_size_inches(
|
517
|
+
w, h * 0.9 / columns * aspect * rows + h * 0.1 * aspect + hadd
|
518
|
+
)
|
519
|
+
gs = GridSpec(rows, columns, figure=fig)
|
520
|
+
if title:
|
521
|
+
fig.suptitle(title)
|
522
|
+
|
523
|
+
axs = []
|
524
|
+
for i, image in enumerate(arrays):
|
525
|
+
ax = fig.add_subplot(gs[i // columns, i % columns], **subplot_kw)
|
526
|
+
ax.set_anchor('C')
|
527
|
+
axs.append(ax)
|
528
|
+
pos = _imshow(
|
529
|
+
ax,
|
530
|
+
image,
|
531
|
+
percentile=percentile,
|
532
|
+
vmin=vmin,
|
533
|
+
vmax=vmax,
|
534
|
+
cmap=cmap,
|
535
|
+
colorbar=percentile is not None,
|
536
|
+
axis=i == 0 and not subplot_kw,
|
537
|
+
title=None if labels is None else labels[i],
|
538
|
+
**kwargs,
|
539
|
+
)
|
540
|
+
if not allrgb and percentile is None:
|
541
|
+
fig.colorbar(pos, ax=axs, shrink=kwargs['shrink'], location=location)
|
542
|
+
|
543
|
+
if show:
|
544
|
+
pyplot.show()
|
545
|
+
|
546
|
+
|
547
|
+
def plot_polar_frequency(
|
548
|
+
frequency: ArrayLike,
|
549
|
+
phase: ArrayLike,
|
550
|
+
modulation: ArrayLike,
|
551
|
+
*,
|
552
|
+
ax: Axes | None = None,
|
553
|
+
title: str | None = None,
|
554
|
+
show: bool = True,
|
555
|
+
**kwargs: Any,
|
556
|
+
) -> None:
|
557
|
+
"""Plot phase and modulation verus frequency.
|
558
|
+
|
559
|
+
Parameters
|
560
|
+
----------
|
561
|
+
frequency : array_like, shape (n, )
|
562
|
+
Laser pulse or modulation frequency in MHz.
|
563
|
+
phase : array_like
|
564
|
+
Angular component of polar coordinates in radians.
|
565
|
+
modulation : array_like
|
566
|
+
Radial component of polar coordinates.
|
567
|
+
ax : matplotlib axes, optional
|
568
|
+
Matplotlib axes used for plotting.
|
569
|
+
By default, a new subplot axes is created.
|
570
|
+
title : str, optional
|
571
|
+
Figure title. The default is "Multi-frequency plot".
|
572
|
+
show : bool, optional, default: True
|
573
|
+
Display figure.
|
574
|
+
**kwargs
|
575
|
+
Additional arguments passed to :py:func:`matplotlib.pyplot.plot`.
|
576
|
+
|
577
|
+
"""
|
578
|
+
# TODO: make this customizable: labels, colors, ...
|
579
|
+
if ax is None:
|
580
|
+
ax = pyplot.subplots()[1]
|
581
|
+
if title is None:
|
582
|
+
title = 'Multi-frequency plot'
|
583
|
+
if title:
|
584
|
+
ax.set_title(title)
|
585
|
+
ax.set_xscale('log', base=10)
|
586
|
+
ax.set_xlabel('Frequency (MHz)')
|
587
|
+
|
588
|
+
phase = numpy.asarray(phase)
|
589
|
+
if phase.ndim < 2:
|
590
|
+
phase = phase.reshape(-1, 1)
|
591
|
+
modulation = numpy.asarray(modulation)
|
592
|
+
if modulation.ndim < 2:
|
593
|
+
modulation = modulation.reshape(-1, 1)
|
594
|
+
|
595
|
+
ax.set_ylabel('Phase (°)', color='tab:blue')
|
596
|
+
ax.set_yticks([0.0, 30.0, 60.0, 90.0])
|
597
|
+
for phi in phase.T:
|
598
|
+
ax.plot(frequency, numpy.rad2deg(phi), color='tab:blue', **kwargs)
|
599
|
+
ax = ax.twinx()
|
600
|
+
|
601
|
+
ax.set_ylabel('Modulation (%)', color='tab:red')
|
602
|
+
ax.set_yticks([0.0, 25.0, 50.0, 75.0, 100.0])
|
603
|
+
for mod in modulation.T:
|
604
|
+
ax.plot(frequency, mod * 100, color='tab:red', **kwargs)
|
605
|
+
if show:
|
606
|
+
pyplot.show()
|
607
|
+
|
608
|
+
|
609
|
+
def plot_histograms(
|
610
|
+
*data: ArrayLike,
|
611
|
+
title: str | None = None,
|
612
|
+
xlabel: str | None = None,
|
613
|
+
ylabel: str | None = None,
|
614
|
+
labels: Sequence[str] | None = None,
|
615
|
+
show: bool = True,
|
616
|
+
**kwargs: Any,
|
617
|
+
) -> None:
|
618
|
+
"""Plot histograms of flattened data arrays.
|
619
|
+
|
620
|
+
Parameters
|
621
|
+
----------
|
622
|
+
data: array_like
|
623
|
+
Data arrays to be plotted as histograms.
|
624
|
+
title : str, optional
|
625
|
+
Figure title.
|
626
|
+
xlabel : str, optional
|
627
|
+
Label for x-axis.
|
628
|
+
ylabel : str, optional
|
629
|
+
Label for y-axis.
|
630
|
+
labels: sequence of str, optional
|
631
|
+
Labels for each data array.
|
632
|
+
show : bool, optional, default: True
|
633
|
+
Display figure.
|
634
|
+
**kwargs
|
635
|
+
Additional arguments passed to :func:`matplotlib.pyplot.hist`.
|
636
|
+
|
637
|
+
"""
|
638
|
+
ax = pyplot.subplots()[1]
|
639
|
+
if kwargs.get('alpha') is None:
|
640
|
+
ax.hist(
|
641
|
+
[numpy.asarray(d).flatten() for d in data], label=labels, **kwargs
|
642
|
+
)
|
643
|
+
else:
|
644
|
+
for d, label in zip(
|
645
|
+
data, [None] * len(data) if labels is None else labels
|
646
|
+
):
|
647
|
+
ax.hist(numpy.asarray(d).flatten(), label=label, **kwargs)
|
648
|
+
if title is not None:
|
649
|
+
ax.set_title(title)
|
650
|
+
if xlabel is not None:
|
651
|
+
ax.set_xlabel(xlabel)
|
652
|
+
if ylabel is not None:
|
653
|
+
ax.set_ylabel(ylabel)
|
654
|
+
if labels is not None:
|
655
|
+
ax.legend()
|
656
|
+
pyplot.tight_layout()
|
657
|
+
if show:
|
658
|
+
pyplot.show()
|
659
|
+
|
660
|
+
|
661
|
+
def _imshow(
|
662
|
+
ax: Axes,
|
663
|
+
image: NDArray[Any],
|
664
|
+
/,
|
665
|
+
*,
|
666
|
+
percentile: float | Sequence[float] | None = None,
|
667
|
+
vmin: float | None = None,
|
668
|
+
vmax: float | None = None,
|
669
|
+
colorbar: bool = True,
|
670
|
+
shrink: float | None = None,
|
671
|
+
axis: bool = True,
|
672
|
+
title: str | None = None,
|
673
|
+
**kwargs: Any,
|
674
|
+
) -> AxesImage:
|
675
|
+
"""Plot image array.
|
676
|
+
|
677
|
+
Convenience wrapper around :py:func:`matplotlib.pyplot.imshow`.
|
678
|
+
|
679
|
+
"""
|
680
|
+
update_kwargs(kwargs, interpolation='none')
|
681
|
+
location = kwargs.pop('location', 'bottom')
|
682
|
+
if image.ndim == 3 and image.shape[2] in {3, 4}:
|
683
|
+
# RGB(A)
|
684
|
+
vmin = None
|
685
|
+
vmax = None
|
686
|
+
percentile = None
|
687
|
+
colorbar = False
|
688
|
+
if percentile is not None:
|
689
|
+
if isinstance(percentile, Sequence):
|
690
|
+
percentile = percentile[0], percentile[1]
|
691
|
+
else:
|
692
|
+
# percentile = max(0.0, min(50, percentile))
|
693
|
+
percentile = percentile, 100.0 - percentile
|
694
|
+
if (
|
695
|
+
percentile[0] >= percentile[1]
|
696
|
+
or percentile[0] < 0
|
697
|
+
or percentile[1] > 100
|
698
|
+
):
|
699
|
+
raise ValueError(f'{percentile=} out of range')
|
700
|
+
vmin, vmax = numpy.nanpercentile(image, percentile)
|
701
|
+
pos = ax.imshow(image, vmin=vmin, vmax=vmax, **kwargs)
|
702
|
+
if colorbar:
|
703
|
+
if percentile is not None and vmin is not None and vmax is not None:
|
704
|
+
ticks = vmin, vmax
|
705
|
+
else:
|
706
|
+
ticks = None
|
707
|
+
fig = ax.get_figure()
|
708
|
+
if fig is not None:
|
709
|
+
if shrink is None:
|
710
|
+
shrink = 0.8
|
711
|
+
fig.colorbar(pos, shrink=shrink, location=location, ticks=ticks)
|
712
|
+
if title:
|
713
|
+
ax.set_title(title)
|
714
|
+
if not axis:
|
715
|
+
ax.set_axis_off()
|
716
|
+
# ax.set_anchor('C')
|
717
|
+
return pos
|