phasorpy 0.5__cp311-cp311-win_amd64.whl → 0.6__cp311-cp311-win_amd64.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.cp311-win_amd64.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
phasorpy/plot.py
DELETED
@@ -1,2318 +0,0 @@
|
|
1
|
-
"""Plot phasor coordinates and related data.
|
2
|
-
|
3
|
-
The ``phasorpy.plot`` module provides functions and classes to visualize
|
4
|
-
phasor coordinates and related data using the
|
5
|
-
`matplotlib <https://matplotlib.org/>`_ library.
|
6
|
-
|
7
|
-
"""
|
8
|
-
|
9
|
-
from __future__ import annotations
|
10
|
-
|
11
|
-
__all__ = [
|
12
|
-
'PhasorPlot',
|
13
|
-
'PhasorPlotFret',
|
14
|
-
'plot_histograms',
|
15
|
-
'plot_image',
|
16
|
-
'plot_phasor',
|
17
|
-
'plot_phasor_image',
|
18
|
-
'plot_polar_frequency',
|
19
|
-
'plot_signal_image',
|
20
|
-
]
|
21
|
-
|
22
|
-
import math
|
23
|
-
import os
|
24
|
-
import warnings
|
25
|
-
from collections.abc import Sequence
|
26
|
-
from typing import TYPE_CHECKING
|
27
|
-
|
28
|
-
if TYPE_CHECKING:
|
29
|
-
from ._typing import Any, ArrayLike, NDArray, Literal, IO
|
30
|
-
|
31
|
-
from matplotlib.axes import Axes
|
32
|
-
from matplotlib.image import AxesImage
|
33
|
-
from matplotlib.figure import Figure
|
34
|
-
|
35
|
-
import numpy
|
36
|
-
from matplotlib import pyplot
|
37
|
-
from matplotlib.font_manager import FontProperties
|
38
|
-
from matplotlib.gridspec import GridSpec
|
39
|
-
from matplotlib.lines import Line2D
|
40
|
-
from matplotlib.patches import Arc, Circle, Ellipse, Polygon
|
41
|
-
from matplotlib.path import Path
|
42
|
-
from matplotlib.patheffects import AbstractPathEffect
|
43
|
-
from matplotlib.widgets import Slider
|
44
|
-
|
45
|
-
from ._phasorpy import _intersection_circle_circle, _intersection_circle_line
|
46
|
-
from ._utils import (
|
47
|
-
dilate_coordinates,
|
48
|
-
parse_kwargs,
|
49
|
-
parse_signal_axis,
|
50
|
-
phasor_from_polar_scalar,
|
51
|
-
phasor_to_polar_scalar,
|
52
|
-
sort_coordinates,
|
53
|
-
update_kwargs,
|
54
|
-
)
|
55
|
-
from .phasor import (
|
56
|
-
phasor_from_fret_acceptor,
|
57
|
-
phasor_from_fret_donor,
|
58
|
-
phasor_from_lifetime,
|
59
|
-
phasor_semicircle,
|
60
|
-
phasor_to_apparent_lifetime,
|
61
|
-
phasor_to_polar,
|
62
|
-
phasor_transform,
|
63
|
-
)
|
64
|
-
|
65
|
-
GRID_COLOR = '0.5'
|
66
|
-
GRID_LINESTYLE = ':'
|
67
|
-
GRID_LINESTYLE_MAJOR = '-'
|
68
|
-
GRID_LINEWIDH = 1.0
|
69
|
-
GRID_LINEWIDH_MINOR = 0.5
|
70
|
-
GRID_FILL = False
|
71
|
-
|
72
|
-
|
73
|
-
class PhasorPlot:
|
74
|
-
"""Phasor plot.
|
75
|
-
|
76
|
-
Create publication quality visualizations of phasor coordinates.
|
77
|
-
|
78
|
-
Parameters
|
79
|
-
----------
|
80
|
-
allquadrants : bool, optional
|
81
|
-
Show all quadrants of phasor space.
|
82
|
-
By default, only the first quadrant with universal semicircle is shown.
|
83
|
-
ax : matplotlib axes, optional
|
84
|
-
Matplotlib axes used for plotting.
|
85
|
-
By default, a new subplot axes is created.
|
86
|
-
frequency : float, optional
|
87
|
-
Laser pulse or modulation frequency in MHz.
|
88
|
-
grid : bool, optional, default: True
|
89
|
-
Display polar grid or universal semicircle.
|
90
|
-
**kwargs
|
91
|
-
Additional properties to set on `ax`.
|
92
|
-
|
93
|
-
See Also
|
94
|
-
--------
|
95
|
-
phasorpy.plot.plot_phasor
|
96
|
-
:ref:`sphx_glr_tutorials_api_phasorpy_phasorplot.py`
|
97
|
-
|
98
|
-
"""
|
99
|
-
|
100
|
-
_ax: Axes
|
101
|
-
"""Matplotlib axes."""
|
102
|
-
|
103
|
-
_limits: tuple[tuple[float, float], tuple[float, float]]
|
104
|
-
"""Axes limits (xmin, xmax), (ymin, ymax)."""
|
105
|
-
|
106
|
-
_full: bool
|
107
|
-
"""Show all quadrants of phasor space."""
|
108
|
-
|
109
|
-
_semicircle_ticks: SemicircleTicks | None
|
110
|
-
"""Last SemicircleTicks instance created."""
|
111
|
-
|
112
|
-
_frequency: float
|
113
|
-
"""Laser pulse or modulation frequency in MHz."""
|
114
|
-
|
115
|
-
def __init__(
|
116
|
-
self,
|
117
|
-
/,
|
118
|
-
allquadrants: bool | None = None,
|
119
|
-
ax: Axes | None = None,
|
120
|
-
*,
|
121
|
-
frequency: float | None = None,
|
122
|
-
grid: bool = True,
|
123
|
-
**kwargs: Any,
|
124
|
-
) -> None:
|
125
|
-
# initialize empty phasor plot
|
126
|
-
self._ax = pyplot.subplots()[1] if ax is None else ax
|
127
|
-
self._ax.format_coord = ( # type: ignore[method-assign]
|
128
|
-
self._on_format_coord
|
129
|
-
)
|
130
|
-
|
131
|
-
self._semicircle_ticks = None
|
132
|
-
|
133
|
-
self._full = bool(allquadrants)
|
134
|
-
if self._full:
|
135
|
-
xlim = (-1.05, 1.05)
|
136
|
-
ylim = (-1.05, 1.05)
|
137
|
-
xticks: tuple[float, ...] = (-1.0, -0.5, 0.0, 0.5, 1.0)
|
138
|
-
yticks: tuple[float, ...] = (-1.0, -0.5, 0.0, 0.5, 1.0)
|
139
|
-
if grid:
|
140
|
-
self.polar_grid()
|
141
|
-
else:
|
142
|
-
xlim = (-0.05, 1.05)
|
143
|
-
ylim = (-0.05, 0.7)
|
144
|
-
xticks = (0.0, 0.2, 0.4, 0.6, 0.8, 1.0)
|
145
|
-
yticks = (0.0, 0.2, 0.4, 0.6)
|
146
|
-
if grid:
|
147
|
-
self.semicircle(frequency=frequency)
|
148
|
-
|
149
|
-
title = 'Phasor plot'
|
150
|
-
if frequency is not None:
|
151
|
-
self._frequency = float(frequency)
|
152
|
-
title += f' ({frequency:g} MHz)'
|
153
|
-
else:
|
154
|
-
self._frequency = 0.0
|
155
|
-
|
156
|
-
update_kwargs(
|
157
|
-
kwargs,
|
158
|
-
title=title,
|
159
|
-
xlabel='G, real',
|
160
|
-
ylabel='S, imag',
|
161
|
-
aspect='equal',
|
162
|
-
xlim=xlim,
|
163
|
-
ylim=ylim,
|
164
|
-
xticks=xticks,
|
165
|
-
yticks=yticks,
|
166
|
-
)
|
167
|
-
self._limits = (kwargs['xlim'], kwargs['ylim'])
|
168
|
-
self._ax.set(**kwargs)
|
169
|
-
|
170
|
-
@property
|
171
|
-
def ax(self) -> Axes:
|
172
|
-
"""Matplotlib :py:class:`matplotlib.axes.Axes`."""
|
173
|
-
return self._ax
|
174
|
-
|
175
|
-
@property
|
176
|
-
def fig(self) -> Figure | None:
|
177
|
-
"""Matplotlib :py:class:`matplotlib.figure.Figure`."""
|
178
|
-
try:
|
179
|
-
# matplotlib >= 3.10.0
|
180
|
-
return self._ax.get_figure(root=True)
|
181
|
-
except TypeError:
|
182
|
-
return self._ax.get_figure() # type: ignore[return-value]
|
183
|
-
|
184
|
-
@property
|
185
|
-
def dataunit_to_point(self) -> float:
|
186
|
-
"""Factor to convert data to point unit."""
|
187
|
-
fig = self.fig
|
188
|
-
assert fig is not None
|
189
|
-
length = fig.bbox_inches.height * self._ax.get_position().height * 72.0
|
190
|
-
vrange: float = numpy.diff(self._ax.get_ylim()).item()
|
191
|
-
return length / vrange
|
192
|
-
|
193
|
-
def show(self) -> None:
|
194
|
-
"""Display all open figures. Call :py:func:`matplotlib.pyplot.show`."""
|
195
|
-
# self.fig.show()
|
196
|
-
pyplot.show()
|
197
|
-
|
198
|
-
def save(
|
199
|
-
self,
|
200
|
-
file: str | os.PathLike[Any] | IO[bytes] | None,
|
201
|
-
/,
|
202
|
-
**kwargs: Any,
|
203
|
-
) -> None:
|
204
|
-
"""Save current figure to file.
|
205
|
-
|
206
|
-
Parameters
|
207
|
-
----------
|
208
|
-
file : str, path-like, or binary file-like
|
209
|
-
Path or Python file-like object to write the current figure to.
|
210
|
-
**kwargs
|
211
|
-
Additional keyword arguments passed to
|
212
|
-
:py:func:`matplotlib:pyplot.savefig`.
|
213
|
-
|
214
|
-
"""
|
215
|
-
pyplot.savefig(file, **kwargs)
|
216
|
-
|
217
|
-
def plot(
|
218
|
-
self,
|
219
|
-
real: ArrayLike,
|
220
|
-
imag: ArrayLike,
|
221
|
-
/,
|
222
|
-
fmt: str = 'o',
|
223
|
-
*,
|
224
|
-
label: Sequence[str] | None = None,
|
225
|
-
**kwargs: Any,
|
226
|
-
) -> list[Line2D]:
|
227
|
-
"""Plot imaginary versus real coordinates as markers or lines.
|
228
|
-
|
229
|
-
Parameters
|
230
|
-
----------
|
231
|
-
real : array_like
|
232
|
-
Real component of phasor coordinates.
|
233
|
-
Must be one or two dimensional.
|
234
|
-
imag : array_like
|
235
|
-
Imaginary component of phasor coordinates.
|
236
|
-
Must be of same shape as `real`.
|
237
|
-
fmt : str, optional, default: 'o'
|
238
|
-
Matplotlib style format string.
|
239
|
-
label : sequence of str, optional
|
240
|
-
Plot label.
|
241
|
-
May be a sequence if phasor coordinates are two dimensional arrays.
|
242
|
-
**kwargs
|
243
|
-
Additional parameters passed to
|
244
|
-
:py:meth:`matplotlib.axes.Axes.plot`.
|
245
|
-
|
246
|
-
Returns
|
247
|
-
-------
|
248
|
-
list[matplotlib.lines.Line2D]
|
249
|
-
Lines representing data plotted last.
|
250
|
-
|
251
|
-
"""
|
252
|
-
lines = []
|
253
|
-
if fmt == 'o':
|
254
|
-
if 'marker' in kwargs:
|
255
|
-
fmt = ''
|
256
|
-
if 'linestyle' not in kwargs and 'ls' not in kwargs:
|
257
|
-
kwargs['linestyle'] = ''
|
258
|
-
args = (fmt,) if fmt else ()
|
259
|
-
ax = self._ax
|
260
|
-
if label is not None and (
|
261
|
-
isinstance(label, str) or not isinstance(label, Sequence)
|
262
|
-
):
|
263
|
-
label = (label,)
|
264
|
-
for (
|
265
|
-
i,
|
266
|
-
(re, im),
|
267
|
-
) in enumerate(
|
268
|
-
zip(
|
269
|
-
numpy.atleast_2d(numpy.asarray(real)),
|
270
|
-
numpy.atleast_2d(numpy.asarray(imag)),
|
271
|
-
)
|
272
|
-
):
|
273
|
-
lbl = None
|
274
|
-
if label is not None:
|
275
|
-
try:
|
276
|
-
lbl = label[i]
|
277
|
-
except IndexError:
|
278
|
-
pass
|
279
|
-
lines = ax.plot(re, im, *args, label=lbl, **kwargs)
|
280
|
-
if label is not None:
|
281
|
-
ax.legend()
|
282
|
-
self._reset_limits()
|
283
|
-
return lines
|
284
|
-
|
285
|
-
def _histogram2d(
|
286
|
-
self,
|
287
|
-
real: ArrayLike,
|
288
|
-
imag: ArrayLike,
|
289
|
-
/,
|
290
|
-
**kwargs: Any,
|
291
|
-
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
|
292
|
-
"""Return two-dimensional histogram of imag versus real coordinates."""
|
293
|
-
update_kwargs(kwargs, range=self._limits)
|
294
|
-
(xmin, xmax), (ymin, ymax) = kwargs['range']
|
295
|
-
assert xmax > xmin and ymax > ymin
|
296
|
-
bins = kwargs.get('bins', 128)
|
297
|
-
if isinstance(bins, int):
|
298
|
-
assert bins > 0
|
299
|
-
aspect = (xmax - xmin) / (ymax - ymin)
|
300
|
-
if aspect > 1:
|
301
|
-
bins = (bins, max(int(bins / aspect), 1))
|
302
|
-
else:
|
303
|
-
bins = (max(int(bins * aspect), 1), bins)
|
304
|
-
kwargs['bins'] = bins
|
305
|
-
return numpy.histogram2d(
|
306
|
-
numpy.asanyarray(real).reshape(-1),
|
307
|
-
numpy.asanyarray(imag).reshape(-1),
|
308
|
-
**kwargs,
|
309
|
-
)
|
310
|
-
|
311
|
-
def _reset_limits(self) -> None:
|
312
|
-
"""Reset axes limits."""
|
313
|
-
try:
|
314
|
-
self._ax.set(xlim=self._limits[0], ylim=self._limits[1])
|
315
|
-
except AttributeError:
|
316
|
-
pass
|
317
|
-
|
318
|
-
def hist2d(
|
319
|
-
self,
|
320
|
-
real: ArrayLike,
|
321
|
-
imag: ArrayLike,
|
322
|
-
/,
|
323
|
-
**kwargs: Any,
|
324
|
-
) -> None:
|
325
|
-
"""Plot two-dimensional histogram of imag versus real coordinates.
|
326
|
-
|
327
|
-
Parameters
|
328
|
-
----------
|
329
|
-
real : array_like
|
330
|
-
Real component of phasor coordinates.
|
331
|
-
imag : array_like
|
332
|
-
Imaginary component of phasor coordinates.
|
333
|
-
Must be of same shape as `real`.
|
334
|
-
**kwargs
|
335
|
-
Additional parameters passed to :py:meth:`numpy.histogram2d`
|
336
|
-
and :py:meth:`matplotlib.axes.Axes.pcolormesh`.
|
337
|
-
|
338
|
-
"""
|
339
|
-
kwargs_hist2d = parse_kwargs(
|
340
|
-
kwargs, 'bins', 'range', 'density', 'weights'
|
341
|
-
)
|
342
|
-
h, xedges, yedges = self._histogram2d(real, imag, **kwargs_hist2d)
|
343
|
-
|
344
|
-
update_kwargs(kwargs, cmap='Blues', norm='log')
|
345
|
-
cmin = kwargs.pop('cmin', 1)
|
346
|
-
cmax = kwargs.pop('cmax', None)
|
347
|
-
if cmin is not None:
|
348
|
-
h[h < cmin] = None
|
349
|
-
if cmax is not None:
|
350
|
-
h[h > cmax] = None
|
351
|
-
self._ax.pcolormesh(xedges, yedges, h.T, **kwargs)
|
352
|
-
self._reset_limits()
|
353
|
-
|
354
|
-
def contour(
|
355
|
-
self,
|
356
|
-
real: ArrayLike,
|
357
|
-
imag: ArrayLike,
|
358
|
-
/,
|
359
|
-
**kwargs: Any,
|
360
|
-
) -> None:
|
361
|
-
"""Plot contours of imag versus real coordinates (not implemented).
|
362
|
-
|
363
|
-
Parameters
|
364
|
-
----------
|
365
|
-
real : array_like
|
366
|
-
Real component of phasor coordinates.
|
367
|
-
imag : array_like
|
368
|
-
Imaginary component of phasor coordinates.
|
369
|
-
Must be of same shape as `real`.
|
370
|
-
**kwargs
|
371
|
-
Additional parameters passed to :py:func:`numpy.histogram2d`
|
372
|
-
and :py:meth:`matplotlib.axes.Axes.contour`.
|
373
|
-
|
374
|
-
"""
|
375
|
-
if 'cmap' not in kwargs and 'colors' not in kwargs:
|
376
|
-
kwargs['cmap'] = 'Blues'
|
377
|
-
update_kwargs(kwargs, norm='log')
|
378
|
-
kwargs_hist2d = parse_kwargs(
|
379
|
-
kwargs, 'bins', 'range', 'density', 'weights'
|
380
|
-
)
|
381
|
-
h, xedges, yedges = self._histogram2d(real, imag, **kwargs_hist2d)
|
382
|
-
xedges = xedges[:-1] + ((xedges[1] - xedges[0]) / 2.0)
|
383
|
-
yedges = yedges[:-1] + ((yedges[1] - yedges[0]) / 2.0)
|
384
|
-
self._ax.contour(xedges, yedges, h.T, **kwargs)
|
385
|
-
self._reset_limits()
|
386
|
-
|
387
|
-
def imshow(
|
388
|
-
self,
|
389
|
-
image: ArrayLike,
|
390
|
-
/,
|
391
|
-
**kwargs: Any,
|
392
|
-
) -> None:
|
393
|
-
"""Plot an image, for example, a 2D histogram (not implemented).
|
394
|
-
|
395
|
-
Parameters
|
396
|
-
----------
|
397
|
-
image : array_like
|
398
|
-
Image to display.
|
399
|
-
**kwargs
|
400
|
-
Additional parameters passed to
|
401
|
-
:py:meth:`matplotlib.axes.Axes.imshow`.
|
402
|
-
|
403
|
-
"""
|
404
|
-
raise NotImplementedError
|
405
|
-
|
406
|
-
def components(
|
407
|
-
self,
|
408
|
-
real: ArrayLike,
|
409
|
-
imag: ArrayLike,
|
410
|
-
/,
|
411
|
-
fraction: ArrayLike | None = None,
|
412
|
-
labels: Sequence[str] | None = None,
|
413
|
-
label_offset: float | None = None,
|
414
|
-
**kwargs: Any,
|
415
|
-
) -> None:
|
416
|
-
"""Plot linear combinations of phasor coordinates or ranges thereof.
|
417
|
-
|
418
|
-
Parameters
|
419
|
-
----------
|
420
|
-
real : (N,) array_like
|
421
|
-
Real component of phasor coordinates.
|
422
|
-
imag : (N,) array_like
|
423
|
-
Imaginary component of phasor coordinates.
|
424
|
-
fraction : (N,) array_like, optional
|
425
|
-
Weight associated with each component.
|
426
|
-
If None (default), outline the polygon area of possible linear
|
427
|
-
combinations of components.
|
428
|
-
Else, draw lines from the component coordinates to the weighted
|
429
|
-
average.
|
430
|
-
labels : Sequence of str, optional
|
431
|
-
Text label for each component.
|
432
|
-
label_offset : float, optional
|
433
|
-
Distance of text label to component coordinate.
|
434
|
-
**kwargs
|
435
|
-
Additional parameters passed to
|
436
|
-
:py:class:`matplotlib.patches.Polygon`,
|
437
|
-
:py:class:`matplotlib.lines.Line2D`, or
|
438
|
-
:py:class:`matplotlib.axes.Axes.annotate`
|
439
|
-
|
440
|
-
"""
|
441
|
-
# TODO: use convex hull for outline
|
442
|
-
# TODO: improve automatic placement of labels
|
443
|
-
# TODO: catch more annotate properties?
|
444
|
-
real, imag, indices = sort_coordinates(real, imag)
|
445
|
-
|
446
|
-
label_ = kwargs.pop('label', None)
|
447
|
-
marker = kwargs.pop('marker', None)
|
448
|
-
color = kwargs.pop('color', None)
|
449
|
-
fontsize = kwargs.pop('fontsize', 12)
|
450
|
-
fontweight = kwargs.pop('fontweight', 'bold')
|
451
|
-
horizontalalignment = kwargs.pop('horizontalalignment', 'center')
|
452
|
-
verticalalignment = kwargs.pop('verticalalignment', 'center')
|
453
|
-
if label_offset is None:
|
454
|
-
label_offset = numpy.diff(self._ax.get_xlim()).item() * 0.04
|
455
|
-
|
456
|
-
if labels is not None:
|
457
|
-
if len(labels) != real.size:
|
458
|
-
raise ValueError(
|
459
|
-
f'number labels={len(labels)} != components={real.size}'
|
460
|
-
)
|
461
|
-
labels = [labels[i] for i in indices]
|
462
|
-
textposition = dilate_coordinates(real, imag, label_offset)
|
463
|
-
for label, re, im, x, y in zip(labels, real, imag, *textposition):
|
464
|
-
if not label:
|
465
|
-
continue
|
466
|
-
self._ax.annotate(
|
467
|
-
label,
|
468
|
-
(re, im),
|
469
|
-
xytext=(x, y),
|
470
|
-
color=color,
|
471
|
-
fontsize=fontsize,
|
472
|
-
fontweight=fontweight,
|
473
|
-
horizontalalignment=horizontalalignment,
|
474
|
-
verticalalignment=verticalalignment,
|
475
|
-
)
|
476
|
-
|
477
|
-
if fraction is None:
|
478
|
-
update_kwargs(
|
479
|
-
kwargs,
|
480
|
-
edgecolor=GRID_COLOR if color is None else color,
|
481
|
-
linestyle=GRID_LINESTYLE,
|
482
|
-
linewidth=GRID_LINEWIDH,
|
483
|
-
fill=GRID_FILL,
|
484
|
-
)
|
485
|
-
self._ax.add_patch(Polygon(numpy.vstack((real, imag)).T, **kwargs))
|
486
|
-
if marker is not None:
|
487
|
-
self._ax.plot(
|
488
|
-
real,
|
489
|
-
imag,
|
490
|
-
marker=marker,
|
491
|
-
linestyle='',
|
492
|
-
color=color,
|
493
|
-
label=label_,
|
494
|
-
)
|
495
|
-
if label_ is not None:
|
496
|
-
self._ax.legend()
|
497
|
-
return
|
498
|
-
|
499
|
-
fraction = numpy.asarray(fraction)[indices]
|
500
|
-
update_kwargs(
|
501
|
-
kwargs,
|
502
|
-
color=GRID_COLOR if color is None else color,
|
503
|
-
linestyle=GRID_LINESTYLE,
|
504
|
-
linewidth=GRID_LINEWIDH,
|
505
|
-
)
|
506
|
-
center_re, center_im = numpy.average(
|
507
|
-
numpy.vstack((real, imag)), axis=-1, weights=fraction
|
508
|
-
)
|
509
|
-
for re, im in zip(real, imag):
|
510
|
-
self._ax.add_line(
|
511
|
-
Line2D([center_re, re], [center_im, im], **kwargs)
|
512
|
-
)
|
513
|
-
if marker is not None:
|
514
|
-
self._ax.plot(real, imag, marker=marker, linestyle='', color=color)
|
515
|
-
self._ax.plot(
|
516
|
-
center_re,
|
517
|
-
center_im,
|
518
|
-
marker=marker,
|
519
|
-
linestyle='',
|
520
|
-
color=color,
|
521
|
-
label=label_,
|
522
|
-
)
|
523
|
-
if label_ is not None:
|
524
|
-
self._ax.legend()
|
525
|
-
|
526
|
-
def line(
|
527
|
-
self,
|
528
|
-
real: ArrayLike,
|
529
|
-
imag: ArrayLike,
|
530
|
-
/,
|
531
|
-
**kwargs: Any,
|
532
|
-
) -> list[Line2D]:
|
533
|
-
"""Draw grid line.
|
534
|
-
|
535
|
-
Parameters
|
536
|
-
----------
|
537
|
-
real : array_like, shape (n, )
|
538
|
-
Real components of line start and end coordinates.
|
539
|
-
imag : array_like, shape (n, )
|
540
|
-
Imaginary components of line start and end coordinates.
|
541
|
-
**kwargs
|
542
|
-
Additional parameters passed to
|
543
|
-
:py:class:`matplotlib.lines.Line2D`.
|
544
|
-
|
545
|
-
Returns
|
546
|
-
-------
|
547
|
-
list[matplotlib.lines.Line2D]
|
548
|
-
List containing plotted line.
|
549
|
-
|
550
|
-
"""
|
551
|
-
update_kwargs(
|
552
|
-
kwargs,
|
553
|
-
color=GRID_COLOR,
|
554
|
-
linestyle=GRID_LINESTYLE,
|
555
|
-
linewidth=GRID_LINEWIDH,
|
556
|
-
)
|
557
|
-
return [self._ax.add_line(Line2D(real, imag, **kwargs))]
|
558
|
-
|
559
|
-
def circle(
|
560
|
-
self,
|
561
|
-
real: float,
|
562
|
-
imag: float,
|
563
|
-
/,
|
564
|
-
radius: float,
|
565
|
-
**kwargs: Any,
|
566
|
-
) -> None:
|
567
|
-
"""Draw grid circle of radius around center.
|
568
|
-
|
569
|
-
Parameters
|
570
|
-
----------
|
571
|
-
real : float
|
572
|
-
Real component of circle center coordinate.
|
573
|
-
imag : float
|
574
|
-
Imaginary component of circle center coordinate.
|
575
|
-
radius : float
|
576
|
-
Circle radius.
|
577
|
-
**kwargs
|
578
|
-
Additional parameters passed to
|
579
|
-
:py:class:`matplotlib.patches.Circle`.
|
580
|
-
|
581
|
-
"""
|
582
|
-
update_kwargs(
|
583
|
-
kwargs,
|
584
|
-
color=GRID_COLOR,
|
585
|
-
linestyle=GRID_LINESTYLE,
|
586
|
-
linewidth=GRID_LINEWIDH,
|
587
|
-
fill=GRID_FILL,
|
588
|
-
)
|
589
|
-
self._ax.add_patch(Circle((real, imag), radius, **kwargs))
|
590
|
-
|
591
|
-
def cursor(
|
592
|
-
self,
|
593
|
-
real: float,
|
594
|
-
imag: float,
|
595
|
-
/,
|
596
|
-
real_limit: float | None = None,
|
597
|
-
imag_limit: float | None = None,
|
598
|
-
radius: float | None = None,
|
599
|
-
radius_minor: float | None = None,
|
600
|
-
angle: float | None = None,
|
601
|
-
align_semicircle: bool = False,
|
602
|
-
**kwargs: Any,
|
603
|
-
) -> None:
|
604
|
-
"""Plot phase and modulation grid lines and arcs at phasor coordinates.
|
605
|
-
|
606
|
-
Parameters
|
607
|
-
----------
|
608
|
-
real : float
|
609
|
-
Real component of phasor coordinate.
|
610
|
-
imag : float
|
611
|
-
Imaginary component of phasor coordinate.
|
612
|
-
real_limit : float, optional
|
613
|
-
Real component of limiting phasor coordinate.
|
614
|
-
imag_limit : float, optional
|
615
|
-
Imaginary component of limiting phasor coordinate.
|
616
|
-
radius : float, optional
|
617
|
-
Radius of circle limiting phase and modulation grid lines and arcs.
|
618
|
-
radius_minor : float, optional
|
619
|
-
Radius of elliptic cursor along semi-minor axis.
|
620
|
-
By default, `radius_minor` is equal to `radius`, that is,
|
621
|
-
the ellipse is circular.
|
622
|
-
angle : float, optional
|
623
|
-
Rotation angle of semi-major axis of elliptic cursor in radians.
|
624
|
-
If None (default), orient ellipse cursor according to
|
625
|
-
`align_semicircle`.
|
626
|
-
align_semicircle : bool, optional
|
627
|
-
Determines elliptic cursor orientation if `angle` is not provided.
|
628
|
-
If true, align the minor axis of the ellipse with the closest
|
629
|
-
tangent on the universal semicircle, else align to the unit circle.
|
630
|
-
**kwargs
|
631
|
-
Additional parameters passed to
|
632
|
-
:py:class:`matplotlib.lines.Line2D`,
|
633
|
-
:py:class:`matplotlib.patches.Circle`,
|
634
|
-
:py:class:`matplotlib.patches.Ellipse`, or
|
635
|
-
:py:class:`matplotlib.patches.Arc`.
|
636
|
-
|
637
|
-
See Also
|
638
|
-
--------
|
639
|
-
phasorpy.plot.PhasorPlot.polar_cursor
|
640
|
-
|
641
|
-
"""
|
642
|
-
if real_limit is not None and imag_limit is not None:
|
643
|
-
return self.polar_cursor(
|
644
|
-
*phasor_to_polar_scalar(real, imag),
|
645
|
-
*phasor_to_polar_scalar(real_limit, imag_limit),
|
646
|
-
radius=radius,
|
647
|
-
radius_minor=radius_minor,
|
648
|
-
angle=angle,
|
649
|
-
align_semicircle=align_semicircle,
|
650
|
-
**kwargs,
|
651
|
-
)
|
652
|
-
return self.polar_cursor(
|
653
|
-
*phasor_to_polar_scalar(real, imag),
|
654
|
-
radius=radius,
|
655
|
-
radius_minor=radius_minor,
|
656
|
-
angle=angle,
|
657
|
-
align_semicircle=align_semicircle,
|
658
|
-
# _circle_only=True,
|
659
|
-
**kwargs,
|
660
|
-
)
|
661
|
-
|
662
|
-
def polar_cursor(
|
663
|
-
self,
|
664
|
-
phase: float | None = None,
|
665
|
-
modulation: float | None = None,
|
666
|
-
phase_limit: float | None = None,
|
667
|
-
modulation_limit: float | None = None,
|
668
|
-
radius: float | None = None,
|
669
|
-
radius_minor: float | None = None,
|
670
|
-
angle: float | None = None,
|
671
|
-
align_semicircle: bool = False,
|
672
|
-
**kwargs: Any,
|
673
|
-
) -> None:
|
674
|
-
"""Plot phase and modulation grid lines and arcs.
|
675
|
-
|
676
|
-
Parameters
|
677
|
-
----------
|
678
|
-
phase : float, optional
|
679
|
-
Angular component of polar coordinate in radians.
|
680
|
-
modulation : float, optional
|
681
|
-
Radial component of polar coordinate.
|
682
|
-
phase_limit : float, optional
|
683
|
-
Angular component of limiting polar coordinate (in radians).
|
684
|
-
Modulation grid arcs are drawn between `phase` and `phase_limit`.
|
685
|
-
modulation_limit : float, optional
|
686
|
-
Radial component of limiting polar coordinate.
|
687
|
-
Phase grid lines are drawn from `modulation` to `modulation_limit`.
|
688
|
-
radius : float, optional
|
689
|
-
Radius of circle limiting phase and modulation grid lines and arcs.
|
690
|
-
radius_minor : float, optional
|
691
|
-
Radius of elliptic cursor along semi-minor axis.
|
692
|
-
By default, `radius_minor` is equal to `radius`, that is,
|
693
|
-
the ellipse is circular.
|
694
|
-
angle : float, optional
|
695
|
-
Rotation angle of semi-major axis of elliptic cursor in radians.
|
696
|
-
If None (default), orient ellipse cursor according to
|
697
|
-
`align_semicircle`.
|
698
|
-
align_semicircle : bool, optional
|
699
|
-
Determines elliptic cursor orientation if `angle` is not provided.
|
700
|
-
If true, align the minor axis of the ellipse with the closest
|
701
|
-
tangent on the universal semicircle, else align to the unit circle.
|
702
|
-
**kwargs
|
703
|
-
Additional parameters passed to
|
704
|
-
:py:class:`matplotlib.lines.Line2D`,
|
705
|
-
:py:class:`matplotlib.patches.Circle`,
|
706
|
-
:py:class:`matplotlib.patches.Ellipse`, or
|
707
|
-
:py:class:`matplotlib.patches.Arc`.
|
708
|
-
|
709
|
-
See Also
|
710
|
-
--------
|
711
|
-
phasorpy.plot.PhasorPlot.cursor
|
712
|
-
|
713
|
-
"""
|
714
|
-
update_kwargs(
|
715
|
-
kwargs,
|
716
|
-
color=GRID_COLOR,
|
717
|
-
linestyle=GRID_LINESTYLE,
|
718
|
-
linewidth=GRID_LINEWIDH,
|
719
|
-
fill=GRID_FILL,
|
720
|
-
)
|
721
|
-
_circle_only = kwargs.pop('_circle_only', False)
|
722
|
-
ax = self._ax
|
723
|
-
if radius is not None and phase is not None and modulation is not None:
|
724
|
-
x = modulation * math.cos(phase)
|
725
|
-
y = modulation * math.sin(phase)
|
726
|
-
if radius_minor is not None and radius_minor != radius:
|
727
|
-
if angle is None:
|
728
|
-
if align_semicircle:
|
729
|
-
angle = math.atan2(y, x - 0.5)
|
730
|
-
else:
|
731
|
-
angle = phase
|
732
|
-
angle = math.degrees(angle)
|
733
|
-
ax.add_patch(
|
734
|
-
Ellipse(
|
735
|
-
(x, y),
|
736
|
-
radius * 2,
|
737
|
-
radius_minor * 2,
|
738
|
-
angle=angle,
|
739
|
-
**kwargs,
|
740
|
-
)
|
741
|
-
)
|
742
|
-
# TODO: implement gridlines intersecting with ellipse
|
743
|
-
return None
|
744
|
-
ax.add_patch(Circle((x, y), radius, **kwargs))
|
745
|
-
if _circle_only:
|
746
|
-
return None
|
747
|
-
del kwargs['fill']
|
748
|
-
x0, y0, x1, y1 = _intersection_circle_line(
|
749
|
-
x, y, radius, 0, 0, x, y
|
750
|
-
)
|
751
|
-
ax.add_line(Line2D((x0, x1), (y0, y1), **kwargs))
|
752
|
-
x0, y0, x1, y1 = _intersection_circle_circle(
|
753
|
-
0, 0, modulation, x, y, radius
|
754
|
-
)
|
755
|
-
ax.add_patch(
|
756
|
-
Arc(
|
757
|
-
(0, 0),
|
758
|
-
modulation * 2,
|
759
|
-
modulation * 2,
|
760
|
-
theta1=math.degrees(math.atan2(y0, x0)),
|
761
|
-
theta2=math.degrees(math.atan2(y1, x1)),
|
762
|
-
fill=False,
|
763
|
-
**kwargs,
|
764
|
-
)
|
765
|
-
)
|
766
|
-
return None
|
767
|
-
|
768
|
-
del kwargs['fill']
|
769
|
-
for phi in (phase, phase_limit):
|
770
|
-
if phi is not None:
|
771
|
-
if modulation is not None and modulation_limit is not None:
|
772
|
-
x0 = modulation * math.cos(phi)
|
773
|
-
y0 = modulation * math.sin(phi)
|
774
|
-
x1 = modulation_limit * math.cos(phi)
|
775
|
-
y1 = modulation_limit * math.sin(phi)
|
776
|
-
else:
|
777
|
-
x0 = 0
|
778
|
-
y0 = 0
|
779
|
-
x1 = math.cos(phi)
|
780
|
-
y1 = math.sin(phi)
|
781
|
-
ax.add_line(Line2D((x0, x1), (y0, y1), **kwargs))
|
782
|
-
for mod in (modulation, modulation_limit):
|
783
|
-
if mod is not None:
|
784
|
-
if phase is not None and phase_limit is not None:
|
785
|
-
theta1 = math.degrees(min(phase, phase_limit))
|
786
|
-
theta2 = math.degrees(max(phase, phase_limit))
|
787
|
-
else:
|
788
|
-
theta1 = 0.0
|
789
|
-
theta2 = 360.0 if self._full else 90.0
|
790
|
-
ax.add_patch(
|
791
|
-
Arc(
|
792
|
-
(0, 0),
|
793
|
-
mod * 2,
|
794
|
-
mod * 2,
|
795
|
-
theta1=theta1,
|
796
|
-
theta2=theta2,
|
797
|
-
fill=False, # filling arc objects is not supported
|
798
|
-
**kwargs,
|
799
|
-
)
|
800
|
-
)
|
801
|
-
return None
|
802
|
-
|
803
|
-
def polar_grid(self, **kwargs: Any) -> None:
|
804
|
-
"""Draw polar coordinate system.
|
805
|
-
|
806
|
-
Parameters
|
807
|
-
----------
|
808
|
-
**kwargs
|
809
|
-
Parameters passed to
|
810
|
-
:py:class:`matplotlib.patches.Circle` and
|
811
|
-
:py:class:`matplotlib.lines.Line2D`.
|
812
|
-
|
813
|
-
"""
|
814
|
-
ax = self._ax
|
815
|
-
# major gridlines
|
816
|
-
kwargs_copy = kwargs.copy()
|
817
|
-
update_kwargs(
|
818
|
-
kwargs,
|
819
|
-
color=GRID_COLOR,
|
820
|
-
linestyle=GRID_LINESTYLE_MAJOR,
|
821
|
-
linewidth=GRID_LINEWIDH,
|
822
|
-
# fill=GRID_FILL,
|
823
|
-
)
|
824
|
-
ax.add_line(Line2D([-1, 1], [0, 0], **kwargs))
|
825
|
-
ax.add_line(Line2D([0, 0], [-1, 1], **kwargs))
|
826
|
-
ax.add_patch(Circle((0, 0), 1, fill=False, **kwargs))
|
827
|
-
# minor gridlines
|
828
|
-
kwargs = kwargs_copy
|
829
|
-
update_kwargs(
|
830
|
-
kwargs,
|
831
|
-
color=GRID_COLOR,
|
832
|
-
linestyle=GRID_LINESTYLE,
|
833
|
-
linewidth=GRID_LINEWIDH_MINOR,
|
834
|
-
)
|
835
|
-
for r in (1 / 3, 2 / 3):
|
836
|
-
ax.add_patch(Circle((0, 0), r, fill=False, **kwargs))
|
837
|
-
for a in (3, 6):
|
838
|
-
x = math.cos(math.pi / a)
|
839
|
-
y = math.sin(math.pi / a)
|
840
|
-
ax.add_line(Line2D([-x, x], [-y, y], **kwargs))
|
841
|
-
ax.add_line(Line2D([-x, x], [y, -y], **kwargs))
|
842
|
-
|
843
|
-
def semicircle(
|
844
|
-
self,
|
845
|
-
frequency: float | None = None,
|
846
|
-
*,
|
847
|
-
polar_reference: tuple[float, float] | None = None,
|
848
|
-
phasor_reference: tuple[float, float] | None = None,
|
849
|
-
lifetime: Sequence[float] | None = None,
|
850
|
-
labels: Sequence[str] | None = None,
|
851
|
-
show_circle: bool = True,
|
852
|
-
use_lines: bool = False,
|
853
|
-
**kwargs: Any,
|
854
|
-
) -> list[Line2D]:
|
855
|
-
"""Draw universal semicircle.
|
856
|
-
|
857
|
-
Parameters
|
858
|
-
----------
|
859
|
-
frequency : float, optional
|
860
|
-
Laser pulse or modulation frequency in MHz.
|
861
|
-
polar_reference : (float, float), optional, default: (0, 1)
|
862
|
-
Polar coordinates of zero lifetime.
|
863
|
-
phasor_reference : (float, float), optional, default: (1, 0)
|
864
|
-
Phasor coordinates of zero lifetime.
|
865
|
-
Alternative to `polar_reference`.
|
866
|
-
lifetime : sequence of float, optional
|
867
|
-
Single component lifetimes at which to draw ticks and labels.
|
868
|
-
Only applies when `frequency` is specified.
|
869
|
-
labels : sequence of str, optional
|
870
|
-
Tick labels. By default, the values of `lifetime`.
|
871
|
-
Only applies when `frequency` and `lifetime` are specified.
|
872
|
-
show_circle : bool, optional, default: True
|
873
|
-
Draw universal semicircle.
|
874
|
-
use_lines : bool, optional, default: False
|
875
|
-
Draw universal semicircle using lines instead of arc.
|
876
|
-
**kwargs
|
877
|
-
Additional parameters passed to
|
878
|
-
:py:class:`matplotlib.lines.Line2D` or
|
879
|
-
:py:class:`matplotlib.patches.Arc` and
|
880
|
-
:py:meth:`matplotlib.axes.Axes.plot`.
|
881
|
-
|
882
|
-
Returns
|
883
|
-
-------
|
884
|
-
list[matplotlib.lines.Line2D]
|
885
|
-
Lines representing plotted semicircle and ticks.
|
886
|
-
|
887
|
-
"""
|
888
|
-
if frequency is not None:
|
889
|
-
self._frequency = float(frequency)
|
890
|
-
|
891
|
-
update_kwargs(
|
892
|
-
kwargs,
|
893
|
-
color=GRID_COLOR,
|
894
|
-
linestyle=GRID_LINESTYLE_MAJOR,
|
895
|
-
linewidth=GRID_LINEWIDH,
|
896
|
-
)
|
897
|
-
if phasor_reference is not None:
|
898
|
-
polar_reference = phasor_to_polar_scalar(*phasor_reference)
|
899
|
-
if polar_reference is None:
|
900
|
-
polar_reference = (0.0, 1.0)
|
901
|
-
if phasor_reference is None:
|
902
|
-
phasor_reference = phasor_from_polar_scalar(*polar_reference)
|
903
|
-
ax = self._ax
|
904
|
-
|
905
|
-
lines = []
|
906
|
-
|
907
|
-
if show_circle:
|
908
|
-
if use_lines:
|
909
|
-
lines = [
|
910
|
-
ax.add_line(
|
911
|
-
Line2D(
|
912
|
-
*phasor_transform(
|
913
|
-
*phasor_semicircle(), *polar_reference
|
914
|
-
),
|
915
|
-
**kwargs,
|
916
|
-
)
|
917
|
-
)
|
918
|
-
]
|
919
|
-
else:
|
920
|
-
ax.add_patch(
|
921
|
-
Arc(
|
922
|
-
(phasor_reference[0] / 2, phasor_reference[1] / 2),
|
923
|
-
polar_reference[1],
|
924
|
-
polar_reference[1],
|
925
|
-
theta1=math.degrees(polar_reference[0]),
|
926
|
-
theta2=math.degrees(polar_reference[0]) + 180.0,
|
927
|
-
fill=False,
|
928
|
-
**kwargs,
|
929
|
-
)
|
930
|
-
)
|
931
|
-
|
932
|
-
if frequency is not None and polar_reference == (0.0, 1.0):
|
933
|
-
# draw ticks and labels
|
934
|
-
lifetime, labels = _semicircle_ticks(frequency, lifetime, labels)
|
935
|
-
self._semicircle_ticks = SemicircleTicks(labels=labels)
|
936
|
-
lines.extend(
|
937
|
-
ax.plot(
|
938
|
-
*phasor_transform(
|
939
|
-
*phasor_from_lifetime(frequency, lifetime),
|
940
|
-
*polar_reference,
|
941
|
-
),
|
942
|
-
path_effects=[self._semicircle_ticks],
|
943
|
-
**kwargs,
|
944
|
-
)
|
945
|
-
)
|
946
|
-
self._reset_limits()
|
947
|
-
return lines
|
948
|
-
|
949
|
-
def _on_format_coord(self, x: float, y: float) -> str:
|
950
|
-
"""Callback function to update coordinates displayed in toolbar."""
|
951
|
-
phi, mod = phasor_to_polar_scalar(x, y)
|
952
|
-
ret = [
|
953
|
-
f'[{x:4.2f}, {y:4.2f}]',
|
954
|
-
f'[{math.degrees(phi):.0f}°, {mod * 100:.0f}%]',
|
955
|
-
]
|
956
|
-
if x > 0.0 and y > 0.0 and self._frequency > 0.0:
|
957
|
-
tp, tm = phasor_to_apparent_lifetime(x, y, self._frequency)
|
958
|
-
ret.append(f'[{tp:.2f}, {tm:.2f} ns]')
|
959
|
-
return ' '.join(reversed(ret))
|
960
|
-
|
961
|
-
|
962
|
-
class PhasorPlotFret(PhasorPlot):
|
963
|
-
"""FRET phasor plot.
|
964
|
-
|
965
|
-
Plot Förster Resonance Energy Transfer efficiency trajectories
|
966
|
-
of donor and acceptor channels in phasor space.
|
967
|
-
|
968
|
-
Parameters
|
969
|
-
----------
|
970
|
-
frequency : array_like
|
971
|
-
Laser pulse or modulation frequency in MHz.
|
972
|
-
donor_lifetime : array_like
|
973
|
-
Lifetime of donor without FRET in ns.
|
974
|
-
acceptor_lifetime : array_like
|
975
|
-
Lifetime of acceptor in ns.
|
976
|
-
fret_efficiency : array_like, optional, default 0
|
977
|
-
FRET efficiency in range [0, 1].
|
978
|
-
donor_fretting : array_like, optional, default 1
|
979
|
-
Fraction of donors participating in FRET. Range [0, 1].
|
980
|
-
donor_bleedthrough : array_like, optional, default 0
|
981
|
-
Weight of donor fluorescence in acceptor channel
|
982
|
-
relative to fluorescence of fully sensitized acceptor.
|
983
|
-
A weight of 1 means the fluorescence from donor and fully sensitized
|
984
|
-
acceptor are equal.
|
985
|
-
The background in the donor channel does not bleed through.
|
986
|
-
acceptor_bleedthrough : array_like, optional, default 0
|
987
|
-
Weight of fluorescence from directly excited acceptor
|
988
|
-
relative to fluorescence of fully sensitized acceptor.
|
989
|
-
A weight of 1 means the fluorescence from directly excited acceptor
|
990
|
-
and fully sensitized acceptor are equal.
|
991
|
-
acceptor_background : array_like, optional, default 0
|
992
|
-
Weight of background fluorescence in acceptor channel
|
993
|
-
relative to fluorescence of fully sensitized acceptor.
|
994
|
-
A weight of 1 means the fluorescence of background and fully
|
995
|
-
sensitized acceptor are equal.
|
996
|
-
donor_background : array_like, optional, default 0
|
997
|
-
Weight of background fluorescence in donor channel
|
998
|
-
relative to fluorescence of donor without FRET.
|
999
|
-
A weight of 1 means the fluorescence of background and donor
|
1000
|
-
without FRET are equal.
|
1001
|
-
background_real : array_like, optional, default 0
|
1002
|
-
Real component of background fluorescence phasor coordinate
|
1003
|
-
at `frequency`.
|
1004
|
-
background_imag : array_like, optional, default 0
|
1005
|
-
Imaginary component of background fluorescence phasor coordinate
|
1006
|
-
at `frequency`.
|
1007
|
-
ax : matplotlib axes, optional
|
1008
|
-
Matplotlib axes used for plotting.
|
1009
|
-
By default, a new subplot axes is created.
|
1010
|
-
Cannot be used with `interactive` mode.
|
1011
|
-
interactive : bool, optional, default: False
|
1012
|
-
Use matplotlib slider widgets to interactively control parameters.
|
1013
|
-
**kwargs
|
1014
|
-
Additional parameters passed to :py:class:`phasorpy.plot.PhasorPlot`.
|
1015
|
-
|
1016
|
-
See Also
|
1017
|
-
--------
|
1018
|
-
phasorpy.phasor.phasor_from_fret_donor
|
1019
|
-
phasorpy.phasor.phasor_from_fret_acceptor
|
1020
|
-
:ref:`sphx_glr_tutorials_api_phasorpy_fret.py`
|
1021
|
-
|
1022
|
-
"""
|
1023
|
-
|
1024
|
-
_fret_efficiencies: NDArray[Any]
|
1025
|
-
|
1026
|
-
_frequency_slider: Slider
|
1027
|
-
_donor_lifetime_slider: Slider
|
1028
|
-
_acceptor_lifetime_slider: Slider
|
1029
|
-
_fret_efficiency_slider: Slider
|
1030
|
-
_donor_fretting_slider: Slider
|
1031
|
-
_donor_bleedthrough_slider: Slider
|
1032
|
-
_acceptor_bleedthrough_slider: Slider
|
1033
|
-
_acceptor_background_slider: Slider
|
1034
|
-
_donor_background_slider: Slider
|
1035
|
-
_background_real_slider: Slider
|
1036
|
-
_background_imag_slider: Slider
|
1037
|
-
|
1038
|
-
_donor_line: Line2D
|
1039
|
-
_donor_only_line: Line2D
|
1040
|
-
_donor_fret_line: Line2D
|
1041
|
-
_donor_trajectory_line: Line2D
|
1042
|
-
_donor_semicircle_line: Line2D
|
1043
|
-
_donor_donor_line: Line2D
|
1044
|
-
_donor_background_line: Line2D
|
1045
|
-
_acceptor_line: Line2D
|
1046
|
-
_acceptor_only_line: Line2D
|
1047
|
-
_acceptor_trajectory_line: Line2D
|
1048
|
-
_acceptor_semicircle_line: Line2D
|
1049
|
-
_acceptor_background_line: Line2D
|
1050
|
-
_background_line: Line2D
|
1051
|
-
|
1052
|
-
_donor_semicircle_ticks: SemicircleTicks | None
|
1053
|
-
|
1054
|
-
def __init__(
|
1055
|
-
self,
|
1056
|
-
*,
|
1057
|
-
frequency: float = 60.0,
|
1058
|
-
donor_lifetime: float = 4.2,
|
1059
|
-
acceptor_lifetime: float = 3.0,
|
1060
|
-
fret_efficiency: float = 0.5,
|
1061
|
-
donor_fretting: float = 1.0,
|
1062
|
-
donor_bleedthrough: float = 0.0,
|
1063
|
-
acceptor_bleedthrough: float = 0.0,
|
1064
|
-
acceptor_background: float = 0.0,
|
1065
|
-
donor_background: float = 0.0,
|
1066
|
-
background_real: float = 0.0,
|
1067
|
-
background_imag: float = 0.0,
|
1068
|
-
ax: Axes | None = None,
|
1069
|
-
interactive: bool = False,
|
1070
|
-
**kwargs: Any,
|
1071
|
-
) -> None:
|
1072
|
-
update_kwargs(
|
1073
|
-
kwargs,
|
1074
|
-
title='FRET phasor plot',
|
1075
|
-
xlim=[-0.2, 1.1],
|
1076
|
-
ylim=[-0.1, 0.8],
|
1077
|
-
)
|
1078
|
-
kwargs['allquadrants'] = False
|
1079
|
-
kwargs['grid'] = False
|
1080
|
-
|
1081
|
-
if ax is not None:
|
1082
|
-
interactive = False
|
1083
|
-
else:
|
1084
|
-
fig = pyplot.figure()
|
1085
|
-
ax = fig.add_subplot()
|
1086
|
-
if interactive:
|
1087
|
-
w, h = fig.get_size_inches()
|
1088
|
-
fig.set_size_inches(w, h * 1.66)
|
1089
|
-
fig.subplots_adjust(bottom=0.45)
|
1090
|
-
fcm = fig.canvas.manager
|
1091
|
-
if fcm is not None:
|
1092
|
-
fcm.set_window_title(kwargs['title'])
|
1093
|
-
|
1094
|
-
super().__init__(ax=ax, **kwargs)
|
1095
|
-
|
1096
|
-
self._fret_efficiencies = numpy.linspace(0.0, 1.0, 101)
|
1097
|
-
|
1098
|
-
donor_real, donor_imag = phasor_from_lifetime(
|
1099
|
-
frequency, donor_lifetime
|
1100
|
-
)
|
1101
|
-
donor_fret_real, donor_fret_imag = phasor_from_lifetime(
|
1102
|
-
frequency, donor_lifetime * (1.0 - fret_efficiency)
|
1103
|
-
)
|
1104
|
-
acceptor_real, acceptor_imag = phasor_from_lifetime(
|
1105
|
-
frequency, acceptor_lifetime
|
1106
|
-
)
|
1107
|
-
donor_trajectory_real, donor_trajectory_imag = phasor_from_fret_donor(
|
1108
|
-
frequency,
|
1109
|
-
donor_lifetime,
|
1110
|
-
fret_efficiency=self._fret_efficiencies,
|
1111
|
-
donor_fretting=donor_fretting,
|
1112
|
-
donor_background=donor_background,
|
1113
|
-
background_real=background_real,
|
1114
|
-
background_imag=background_imag,
|
1115
|
-
)
|
1116
|
-
(
|
1117
|
-
acceptor_trajectory_real,
|
1118
|
-
acceptor_trajectory_imag,
|
1119
|
-
) = phasor_from_fret_acceptor(
|
1120
|
-
frequency,
|
1121
|
-
donor_lifetime,
|
1122
|
-
acceptor_lifetime,
|
1123
|
-
fret_efficiency=self._fret_efficiencies,
|
1124
|
-
donor_fretting=donor_fretting,
|
1125
|
-
donor_bleedthrough=donor_bleedthrough,
|
1126
|
-
acceptor_bleedthrough=acceptor_bleedthrough,
|
1127
|
-
acceptor_background=acceptor_background,
|
1128
|
-
background_real=background_real,
|
1129
|
-
background_imag=background_imag,
|
1130
|
-
)
|
1131
|
-
|
1132
|
-
# add plots
|
1133
|
-
lines = self.semicircle(frequency=frequency)
|
1134
|
-
self._donor_semicircle_line = lines[0]
|
1135
|
-
self._donor_semicircle_ticks = self._semicircle_ticks
|
1136
|
-
|
1137
|
-
lines = self.semicircle(
|
1138
|
-
phasor_reference=(float(acceptor_real), float(acceptor_imag)),
|
1139
|
-
use_lines=True,
|
1140
|
-
)
|
1141
|
-
self._acceptor_semicircle_line = lines[0]
|
1142
|
-
|
1143
|
-
if donor_fretting < 1.0 and donor_background == 0.0:
|
1144
|
-
lines = self.line(
|
1145
|
-
[donor_real, donor_fret_real],
|
1146
|
-
[donor_imag, donor_fret_imag],
|
1147
|
-
)
|
1148
|
-
else:
|
1149
|
-
lines = self.line([0.0, 0.0], [0.0, 0.0])
|
1150
|
-
self._donor_donor_line = lines[0]
|
1151
|
-
|
1152
|
-
if acceptor_background > 0.0:
|
1153
|
-
lines = self.line(
|
1154
|
-
[float(acceptor_real), float(background_real)],
|
1155
|
-
[float(acceptor_imag), float(background_imag)],
|
1156
|
-
)
|
1157
|
-
else:
|
1158
|
-
lines = self.line([0.0, 0.0], [0.0, 0.0])
|
1159
|
-
self._acceptor_background_line = lines[0]
|
1160
|
-
|
1161
|
-
if donor_background > 0.0:
|
1162
|
-
lines = self.line(
|
1163
|
-
[float(donor_real), float(background_real)],
|
1164
|
-
[float(donor_imag), float(background_imag)],
|
1165
|
-
)
|
1166
|
-
else:
|
1167
|
-
lines = self.line([0.0, 0.0], [0.0, 0.0])
|
1168
|
-
self._donor_background_line = lines[0]
|
1169
|
-
|
1170
|
-
lines = self.plot(
|
1171
|
-
donor_trajectory_real,
|
1172
|
-
donor_trajectory_imag,
|
1173
|
-
'-',
|
1174
|
-
color='tab:green',
|
1175
|
-
)
|
1176
|
-
self._donor_trajectory_line = lines[0]
|
1177
|
-
|
1178
|
-
lines = self.plot(
|
1179
|
-
acceptor_trajectory_real,
|
1180
|
-
acceptor_trajectory_imag,
|
1181
|
-
'-',
|
1182
|
-
color='tab:red',
|
1183
|
-
)
|
1184
|
-
self._acceptor_trajectory_line = lines[0]
|
1185
|
-
|
1186
|
-
lines = self.plot(
|
1187
|
-
donor_real,
|
1188
|
-
donor_imag,
|
1189
|
-
'.',
|
1190
|
-
color='tab:green',
|
1191
|
-
)
|
1192
|
-
self._donor_only_line = lines[0]
|
1193
|
-
|
1194
|
-
lines = self.plot(
|
1195
|
-
donor_real,
|
1196
|
-
donor_imag,
|
1197
|
-
'.',
|
1198
|
-
color='tab:green',
|
1199
|
-
)
|
1200
|
-
self._donor_fret_line = lines[0]
|
1201
|
-
|
1202
|
-
lines = self.plot(
|
1203
|
-
acceptor_real,
|
1204
|
-
acceptor_imag,
|
1205
|
-
'.',
|
1206
|
-
color='tab:red',
|
1207
|
-
)
|
1208
|
-
self._acceptor_only_line = lines[0]
|
1209
|
-
|
1210
|
-
lines = self.plot(
|
1211
|
-
donor_trajectory_real[int(fret_efficiency * 100.0)],
|
1212
|
-
donor_trajectory_imag[int(fret_efficiency * 100.0)],
|
1213
|
-
'o',
|
1214
|
-
color='tab:green',
|
1215
|
-
label='Donor',
|
1216
|
-
)
|
1217
|
-
self._donor_line = lines[0]
|
1218
|
-
|
1219
|
-
lines = self.plot(
|
1220
|
-
acceptor_trajectory_real[int(fret_efficiency * 100.0)],
|
1221
|
-
acceptor_trajectory_imag[int(fret_efficiency * 100.0)],
|
1222
|
-
'o',
|
1223
|
-
color='tab:red',
|
1224
|
-
label='Acceptor',
|
1225
|
-
)
|
1226
|
-
self._acceptor_line = lines[0]
|
1227
|
-
|
1228
|
-
lines = self.plot(
|
1229
|
-
background_real,
|
1230
|
-
background_imag,
|
1231
|
-
'o',
|
1232
|
-
color='black',
|
1233
|
-
label='Background',
|
1234
|
-
)
|
1235
|
-
self._background_line = lines[0]
|
1236
|
-
|
1237
|
-
if not interactive:
|
1238
|
-
return
|
1239
|
-
|
1240
|
-
# add sliders
|
1241
|
-
axes = []
|
1242
|
-
for i in range(11):
|
1243
|
-
axes.append(fig.add_axes((0.33, 0.05 + i * 0.03, 0.45, 0.01)))
|
1244
|
-
|
1245
|
-
self._frequency_slider = Slider(
|
1246
|
-
ax=axes[10],
|
1247
|
-
label='Frequency ',
|
1248
|
-
valfmt=' %.0f MHz',
|
1249
|
-
valmin=10,
|
1250
|
-
valmax=200,
|
1251
|
-
valstep=1,
|
1252
|
-
valinit=frequency,
|
1253
|
-
)
|
1254
|
-
self._frequency_slider.on_changed(self._on_semicircle_changed)
|
1255
|
-
|
1256
|
-
self._donor_lifetime_slider = Slider(
|
1257
|
-
ax=axes[9],
|
1258
|
-
label='Donor lifetime ',
|
1259
|
-
valfmt=' %.1f ns',
|
1260
|
-
valmin=0.1,
|
1261
|
-
valmax=16.0,
|
1262
|
-
valstep=0.1,
|
1263
|
-
valinit=donor_lifetime,
|
1264
|
-
# facecolor='tab:green',
|
1265
|
-
handle_style={'edgecolor': 'tab:green'},
|
1266
|
-
)
|
1267
|
-
self._donor_lifetime_slider.on_changed(self._on_changed)
|
1268
|
-
|
1269
|
-
self._acceptor_lifetime_slider = Slider(
|
1270
|
-
ax=axes[8],
|
1271
|
-
label='Acceptor lifetime ',
|
1272
|
-
valfmt=' %.1f ns',
|
1273
|
-
valmin=0.1,
|
1274
|
-
valmax=16.0,
|
1275
|
-
valstep=0.1,
|
1276
|
-
valinit=acceptor_lifetime,
|
1277
|
-
# facecolor='tab:red',
|
1278
|
-
handle_style={'edgecolor': 'tab:red'},
|
1279
|
-
)
|
1280
|
-
self._acceptor_lifetime_slider.on_changed(self._on_semicircle_changed)
|
1281
|
-
|
1282
|
-
self._fret_efficiency_slider = Slider(
|
1283
|
-
ax=axes[7],
|
1284
|
-
label='FRET efficiency ',
|
1285
|
-
valfmt=' %.2f',
|
1286
|
-
valmin=0.0,
|
1287
|
-
valmax=1.0,
|
1288
|
-
valstep=0.01,
|
1289
|
-
valinit=fret_efficiency,
|
1290
|
-
)
|
1291
|
-
self._fret_efficiency_slider.on_changed(self._on_changed)
|
1292
|
-
|
1293
|
-
self._donor_fretting_slider = Slider(
|
1294
|
-
ax=axes[6],
|
1295
|
-
label='Donors fretting ',
|
1296
|
-
valfmt=' %.2f',
|
1297
|
-
valmin=0.0,
|
1298
|
-
valmax=1.0,
|
1299
|
-
valstep=0.01,
|
1300
|
-
valinit=donor_fretting,
|
1301
|
-
# facecolor='tab:green',
|
1302
|
-
handle_style={'edgecolor': 'tab:green'},
|
1303
|
-
)
|
1304
|
-
self._donor_fretting_slider.on_changed(self._on_changed)
|
1305
|
-
|
1306
|
-
self._donor_bleedthrough_slider = Slider(
|
1307
|
-
ax=axes[5],
|
1308
|
-
label='Donor bleedthrough ',
|
1309
|
-
valfmt=' %.2f',
|
1310
|
-
valmin=0.0,
|
1311
|
-
valmax=5.0,
|
1312
|
-
valstep=0.01,
|
1313
|
-
valinit=donor_bleedthrough,
|
1314
|
-
# facecolor='tab:red',
|
1315
|
-
handle_style={'edgecolor': 'tab:red'},
|
1316
|
-
)
|
1317
|
-
self._donor_bleedthrough_slider.on_changed(self._on_changed)
|
1318
|
-
|
1319
|
-
self._acceptor_bleedthrough_slider = Slider(
|
1320
|
-
ax=axes[4],
|
1321
|
-
label='Acceptor bleedthrough ',
|
1322
|
-
valfmt=' %.2f',
|
1323
|
-
valmin=0.0,
|
1324
|
-
valmax=5.0,
|
1325
|
-
valstep=0.01,
|
1326
|
-
valinit=acceptor_bleedthrough,
|
1327
|
-
# facecolor='tab:red',
|
1328
|
-
handle_style={'edgecolor': 'tab:red'},
|
1329
|
-
)
|
1330
|
-
self._acceptor_bleedthrough_slider.on_changed(self._on_changed)
|
1331
|
-
|
1332
|
-
self._acceptor_background_slider = Slider(
|
1333
|
-
ax=axes[3],
|
1334
|
-
label='Acceptor background ',
|
1335
|
-
valfmt=' %.2f',
|
1336
|
-
valmin=0.0,
|
1337
|
-
valmax=5.0,
|
1338
|
-
valstep=0.01,
|
1339
|
-
valinit=acceptor_background,
|
1340
|
-
# facecolor='tab:red',
|
1341
|
-
handle_style={'edgecolor': 'tab:red'},
|
1342
|
-
)
|
1343
|
-
self._acceptor_background_slider.on_changed(self._on_changed)
|
1344
|
-
|
1345
|
-
self._donor_background_slider = Slider(
|
1346
|
-
ax=axes[2],
|
1347
|
-
label='Donor background ',
|
1348
|
-
valfmt=' %.2f',
|
1349
|
-
valmin=0.0,
|
1350
|
-
valmax=5.0,
|
1351
|
-
valstep=0.01,
|
1352
|
-
valinit=donor_background,
|
1353
|
-
# facecolor='tab:green',
|
1354
|
-
handle_style={'edgecolor': 'tab:green'},
|
1355
|
-
)
|
1356
|
-
self._donor_background_slider.on_changed(self._on_changed)
|
1357
|
-
|
1358
|
-
self._background_real_slider = Slider(
|
1359
|
-
ax=axes[1],
|
1360
|
-
label='Background real ',
|
1361
|
-
valfmt=' %.2f',
|
1362
|
-
valmin=0.0,
|
1363
|
-
valmax=1.0,
|
1364
|
-
valstep=0.01,
|
1365
|
-
valinit=background_real,
|
1366
|
-
)
|
1367
|
-
self._background_real_slider.on_changed(self._on_changed)
|
1368
|
-
|
1369
|
-
self._background_imag_slider = Slider(
|
1370
|
-
ax=axes[0],
|
1371
|
-
label='Background imag ',
|
1372
|
-
valfmt=' %.2f',
|
1373
|
-
valmin=0.0,
|
1374
|
-
valmax=0.6,
|
1375
|
-
valstep=0.01,
|
1376
|
-
valinit=background_imag,
|
1377
|
-
)
|
1378
|
-
self._background_imag_slider.on_changed(self._on_changed)
|
1379
|
-
|
1380
|
-
def _on_semicircle_changed(self, value: Any) -> None:
|
1381
|
-
"""Callback function to update semicircles."""
|
1382
|
-
self._frequency = frequency = self._frequency_slider.val
|
1383
|
-
acceptor_lifetime = self._acceptor_lifetime_slider.val
|
1384
|
-
if self._donor_semicircle_ticks is not None:
|
1385
|
-
lifetime, labels = _semicircle_ticks(frequency)
|
1386
|
-
self._donor_semicircle_ticks.labels = labels
|
1387
|
-
self._donor_semicircle_line.set_data(
|
1388
|
-
*phasor_transform(*phasor_from_lifetime(frequency, lifetime))
|
1389
|
-
)
|
1390
|
-
self._acceptor_semicircle_line.set_data(
|
1391
|
-
*phasor_transform(
|
1392
|
-
*phasor_semicircle(),
|
1393
|
-
*phasor_to_polar(
|
1394
|
-
*phasor_from_lifetime(frequency, acceptor_lifetime)
|
1395
|
-
),
|
1396
|
-
)
|
1397
|
-
)
|
1398
|
-
self._on_changed(value)
|
1399
|
-
|
1400
|
-
def _on_changed(self, value: Any) -> None:
|
1401
|
-
"""Callback function to update plot with current slider values."""
|
1402
|
-
frequency = self._frequency_slider.val
|
1403
|
-
donor_lifetime = self._donor_lifetime_slider.val
|
1404
|
-
acceptor_lifetime = self._acceptor_lifetime_slider.val
|
1405
|
-
fret_efficiency = self._fret_efficiency_slider.val
|
1406
|
-
donor_fretting = self._donor_fretting_slider.val
|
1407
|
-
donor_bleedthrough = self._donor_bleedthrough_slider.val
|
1408
|
-
acceptor_bleedthrough = self._acceptor_bleedthrough_slider.val
|
1409
|
-
acceptor_background = self._acceptor_background_slider.val
|
1410
|
-
donor_background = self._donor_background_slider.val
|
1411
|
-
background_real = self._background_real_slider.val
|
1412
|
-
background_imag = self._background_imag_slider.val
|
1413
|
-
e = int(self._fret_efficiency_slider.val * 100)
|
1414
|
-
|
1415
|
-
donor_real, donor_imag = phasor_from_lifetime(
|
1416
|
-
frequency, donor_lifetime
|
1417
|
-
)
|
1418
|
-
donor_fret_real, donor_fret_imag = phasor_from_lifetime(
|
1419
|
-
frequency, donor_lifetime * (1.0 - fret_efficiency)
|
1420
|
-
)
|
1421
|
-
acceptor_real, acceptor_imag = phasor_from_lifetime(
|
1422
|
-
frequency, acceptor_lifetime
|
1423
|
-
)
|
1424
|
-
donor_trajectory_real, donor_trajectory_imag = phasor_from_fret_donor(
|
1425
|
-
frequency,
|
1426
|
-
donor_lifetime,
|
1427
|
-
fret_efficiency=self._fret_efficiencies,
|
1428
|
-
donor_fretting=donor_fretting,
|
1429
|
-
donor_background=donor_background,
|
1430
|
-
background_real=background_real,
|
1431
|
-
background_imag=background_imag,
|
1432
|
-
)
|
1433
|
-
(
|
1434
|
-
acceptor_trajectory_real,
|
1435
|
-
acceptor_trajectory_imag,
|
1436
|
-
) = phasor_from_fret_acceptor(
|
1437
|
-
frequency,
|
1438
|
-
donor_lifetime,
|
1439
|
-
acceptor_lifetime,
|
1440
|
-
fret_efficiency=self._fret_efficiencies,
|
1441
|
-
donor_fretting=donor_fretting,
|
1442
|
-
donor_bleedthrough=donor_bleedthrough,
|
1443
|
-
acceptor_bleedthrough=acceptor_bleedthrough,
|
1444
|
-
acceptor_background=acceptor_background,
|
1445
|
-
background_real=background_real,
|
1446
|
-
background_imag=background_imag,
|
1447
|
-
)
|
1448
|
-
|
1449
|
-
if donor_background > 0.0:
|
1450
|
-
self._donor_background_line.set_data(
|
1451
|
-
[float(donor_real), float(background_real)],
|
1452
|
-
[float(donor_imag), float(background_imag)],
|
1453
|
-
)
|
1454
|
-
else:
|
1455
|
-
self._donor_background_line.set_data([0.0, 0.0], [0.0, 0.0])
|
1456
|
-
|
1457
|
-
if donor_fretting < 1.0 and donor_background == 0.0:
|
1458
|
-
self._donor_donor_line.set_data(
|
1459
|
-
[donor_real, donor_fret_real],
|
1460
|
-
[donor_imag, donor_fret_imag],
|
1461
|
-
)
|
1462
|
-
else:
|
1463
|
-
self._donor_donor_line.set_data([0.0, 0.0], [0.0, 0.0])
|
1464
|
-
|
1465
|
-
if acceptor_background > 0.0:
|
1466
|
-
self._acceptor_background_line.set_data(
|
1467
|
-
[float(acceptor_real), float(background_real)],
|
1468
|
-
[float(acceptor_imag), float(background_imag)],
|
1469
|
-
)
|
1470
|
-
else:
|
1471
|
-
self._acceptor_background_line.set_data([0.0, 0.0], [0.0, 0.0])
|
1472
|
-
|
1473
|
-
self._background_line.set_data([background_real], [background_imag])
|
1474
|
-
|
1475
|
-
self._donor_only_line.set_data([donor_real], [donor_imag])
|
1476
|
-
self._donor_fret_line.set_data([donor_fret_real], [donor_fret_imag])
|
1477
|
-
self._donor_trajectory_line.set_data(
|
1478
|
-
donor_trajectory_real, donor_trajectory_imag
|
1479
|
-
)
|
1480
|
-
self._donor_line.set_data(
|
1481
|
-
[donor_trajectory_real[e]], [donor_trajectory_imag[e]]
|
1482
|
-
)
|
1483
|
-
|
1484
|
-
self._acceptor_only_line.set_data([acceptor_real], [acceptor_imag])
|
1485
|
-
self._acceptor_trajectory_line.set_data(
|
1486
|
-
acceptor_trajectory_real, acceptor_trajectory_imag
|
1487
|
-
)
|
1488
|
-
self._acceptor_line.set_data(
|
1489
|
-
[acceptor_trajectory_real[e]], [acceptor_trajectory_imag[e]]
|
1490
|
-
)
|
1491
|
-
|
1492
|
-
|
1493
|
-
class SemicircleTicks(AbstractPathEffect):
|
1494
|
-
"""Draw ticks on universal semicircle.
|
1495
|
-
|
1496
|
-
Parameters
|
1497
|
-
----------
|
1498
|
-
size : float, optional
|
1499
|
-
Length of tick in dots.
|
1500
|
-
The default is ``rcParams['xtick.major.size']``.
|
1501
|
-
labels : sequence of str, optional
|
1502
|
-
Tick labels for each vertex in path.
|
1503
|
-
**kwargs
|
1504
|
-
Extra keywords passed to matplotlib's
|
1505
|
-
:py:meth:`matplotlib.patheffects.AbstractPathEffect._update_gc`.
|
1506
|
-
|
1507
|
-
"""
|
1508
|
-
|
1509
|
-
_size: float # tick length
|
1510
|
-
_labels: tuple[str, ...] # tick labels
|
1511
|
-
_gc: dict[str, Any] # keywords passed to _update_gc
|
1512
|
-
|
1513
|
-
def __init__(
|
1514
|
-
self,
|
1515
|
-
size: float | None = None,
|
1516
|
-
labels: Sequence[str] | None = None,
|
1517
|
-
**kwargs: Any,
|
1518
|
-
) -> None:
|
1519
|
-
super().__init__((0.0, 0.0))
|
1520
|
-
|
1521
|
-
if size is None:
|
1522
|
-
self._size = pyplot.rcParams['xtick.major.size']
|
1523
|
-
else:
|
1524
|
-
self._size = size
|
1525
|
-
if labels is None or not labels:
|
1526
|
-
self._labels = ()
|
1527
|
-
else:
|
1528
|
-
self._labels = tuple(labels)
|
1529
|
-
self._gc = kwargs
|
1530
|
-
|
1531
|
-
@property
|
1532
|
-
def labels(self) -> tuple[str, ...]:
|
1533
|
-
"""Tick labels."""
|
1534
|
-
return self._labels
|
1535
|
-
|
1536
|
-
@labels.setter
|
1537
|
-
def labels(self, value: Sequence[str] | None, /) -> None:
|
1538
|
-
if value is None or not value:
|
1539
|
-
self._labels = ()
|
1540
|
-
else:
|
1541
|
-
self._labels = tuple(value)
|
1542
|
-
|
1543
|
-
def draw_path(
|
1544
|
-
self,
|
1545
|
-
renderer: Any,
|
1546
|
-
gc: Any,
|
1547
|
-
tpath: Any,
|
1548
|
-
affine: Any,
|
1549
|
-
rgbFace: Any = None,
|
1550
|
-
) -> None:
|
1551
|
-
"""Draw path with updated gc."""
|
1552
|
-
gc0 = renderer.new_gc()
|
1553
|
-
gc0.copy_properties(gc)
|
1554
|
-
|
1555
|
-
# TODO: this uses private methods of the base class
|
1556
|
-
gc0 = self._update_gc(gc0, self._gc) # type: ignore[attr-defined]
|
1557
|
-
trans = affine
|
1558
|
-
trans += self._offset_transform(renderer) # type: ignore[attr-defined]
|
1559
|
-
|
1560
|
-
font = FontProperties()
|
1561
|
-
# approximate half size of 'x'
|
1562
|
-
fontsize = renderer.points_to_pixels(font.get_size_in_points()) / 4
|
1563
|
-
size = renderer.points_to_pixels(self._size)
|
1564
|
-
origin = affine.transform([[0.5, 0.0]])
|
1565
|
-
|
1566
|
-
transpath = affine.transform_path(tpath)
|
1567
|
-
polys = transpath.to_polygons(closed_only=False)
|
1568
|
-
|
1569
|
-
for p in polys:
|
1570
|
-
# coordinates of tick ends
|
1571
|
-
t = p - origin
|
1572
|
-
t /= numpy.hypot(t[:, 0], t[:, 1])[:, numpy.newaxis]
|
1573
|
-
d = t.copy()
|
1574
|
-
t *= size
|
1575
|
-
t += p
|
1576
|
-
|
1577
|
-
xyt = numpy.empty((2 * p.shape[0], 2))
|
1578
|
-
xyt[0::2] = p
|
1579
|
-
xyt[1::2] = t
|
1580
|
-
|
1581
|
-
renderer.draw_path(
|
1582
|
-
gc0,
|
1583
|
-
Path(xyt, numpy.tile([Path.MOVETO, Path.LINETO], p.shape[0])),
|
1584
|
-
affine.inverted() + trans,
|
1585
|
-
rgbFace,
|
1586
|
-
)
|
1587
|
-
if not self._labels:
|
1588
|
-
continue
|
1589
|
-
# coordinates of labels
|
1590
|
-
t = d * size * 2.5
|
1591
|
-
t += p
|
1592
|
-
|
1593
|
-
if renderer.flipy():
|
1594
|
-
h = renderer.get_canvas_width_height()[1]
|
1595
|
-
else:
|
1596
|
-
h = 0.0
|
1597
|
-
|
1598
|
-
for s, (x, y), (dx, _) in zip(self._labels, t, d):
|
1599
|
-
# TODO: get rendered text size from matplotlib.text.Text?
|
1600
|
-
# this did not work:
|
1601
|
-
# Text(d[i,0], h - d[i,1], label, ha='center', va='center')
|
1602
|
-
x = x + fontsize * len(s.split()[0]) * (dx - 1.0)
|
1603
|
-
y = h - y + fontsize
|
1604
|
-
renderer.draw_text(gc0, x, y, s, font, 0.0)
|
1605
|
-
|
1606
|
-
gc0.restore()
|
1607
|
-
|
1608
|
-
|
1609
|
-
def plot_phasor(
|
1610
|
-
real: ArrayLike,
|
1611
|
-
imag: ArrayLike,
|
1612
|
-
/,
|
1613
|
-
*,
|
1614
|
-
style: Literal['plot', 'hist2d', 'contour'] | None = None,
|
1615
|
-
allquadrants: bool | None = None,
|
1616
|
-
frequency: float | None = None,
|
1617
|
-
show: bool = True,
|
1618
|
-
**kwargs: Any,
|
1619
|
-
) -> None:
|
1620
|
-
"""Plot phasor coordinates.
|
1621
|
-
|
1622
|
-
A simplified interface to the :py:class:`PhasorPlot` class.
|
1623
|
-
|
1624
|
-
Parameters
|
1625
|
-
----------
|
1626
|
-
real : array_like
|
1627
|
-
Real component of phasor coordinates.
|
1628
|
-
imag : array_like
|
1629
|
-
Imaginary component of phasor coordinates.
|
1630
|
-
Must be of same shape as `real`.
|
1631
|
-
style : {'plot', 'hist2d', 'contour'}, optional
|
1632
|
-
Method used to plot phasor coordinates.
|
1633
|
-
By default, if the number of coordinates are less than 65536
|
1634
|
-
and the arrays are less than three-dimensional, `'plot'` style is used,
|
1635
|
-
else `'hist2d'`.
|
1636
|
-
allquadrants : bool, optional
|
1637
|
-
Show all quadrants of phasor space.
|
1638
|
-
By default, only the first quadrant is shown.
|
1639
|
-
frequency : float, optional
|
1640
|
-
Frequency of phasor plot.
|
1641
|
-
If provided, the universal semicircle is labeled with reference
|
1642
|
-
lifetimes.
|
1643
|
-
show : bool, optional, default: True
|
1644
|
-
Display figure.
|
1645
|
-
**kwargs
|
1646
|
-
Additional parguments passed to :py:class:`PhasorPlot`,
|
1647
|
-
:py:meth:`PhasorPlot.plot`, :py:meth:`PhasorPlot.hist2d`, or
|
1648
|
-
:py:meth:`PhasorPlot.contour` depending on `style`.
|
1649
|
-
|
1650
|
-
See Also
|
1651
|
-
--------
|
1652
|
-
phasorpy.plot.PhasorPlot
|
1653
|
-
:ref:`sphx_glr_tutorials_api_phasorpy_phasorplot.py`
|
1654
|
-
|
1655
|
-
"""
|
1656
|
-
init_kwargs = parse_kwargs(
|
1657
|
-
kwargs,
|
1658
|
-
'ax',
|
1659
|
-
'title',
|
1660
|
-
'xlabel',
|
1661
|
-
'ylabel',
|
1662
|
-
'xlim',
|
1663
|
-
'ylim',
|
1664
|
-
'xticks',
|
1665
|
-
'yticks',
|
1666
|
-
'grid',
|
1667
|
-
)
|
1668
|
-
|
1669
|
-
real = numpy.asanyarray(real)
|
1670
|
-
imag = numpy.asanyarray(imag)
|
1671
|
-
plot = PhasorPlot(
|
1672
|
-
frequency=frequency, allquadrants=allquadrants, **init_kwargs
|
1673
|
-
)
|
1674
|
-
if style is None:
|
1675
|
-
style = 'plot' if real.size < 65536 and real.ndim < 3 else 'hist2d'
|
1676
|
-
if style == 'plot':
|
1677
|
-
plot.plot(real, imag, **kwargs)
|
1678
|
-
elif style == 'hist2d':
|
1679
|
-
plot.hist2d(real, imag, **kwargs)
|
1680
|
-
elif style == 'contour':
|
1681
|
-
plot.contour(real, imag, **kwargs)
|
1682
|
-
else:
|
1683
|
-
raise ValueError(f'invalid {style=}')
|
1684
|
-
if show:
|
1685
|
-
plot.show()
|
1686
|
-
|
1687
|
-
|
1688
|
-
def plot_phasor_image(
|
1689
|
-
mean: ArrayLike | None,
|
1690
|
-
real: ArrayLike,
|
1691
|
-
imag: ArrayLike,
|
1692
|
-
*,
|
1693
|
-
harmonics: int | None = None,
|
1694
|
-
percentile: float | None = None,
|
1695
|
-
title: str | None = None,
|
1696
|
-
show: bool = True,
|
1697
|
-
**kwargs: Any,
|
1698
|
-
) -> None:
|
1699
|
-
"""Plot phasor coordinates as images.
|
1700
|
-
|
1701
|
-
Preview phasor coordinates from time-resolved or hyperspectral
|
1702
|
-
image stacks as returned by :py:func:`phasorpy.phasor.phasor_from_signal`.
|
1703
|
-
|
1704
|
-
The last two axes are assumed to be the image axes.
|
1705
|
-
Harmonics, if any, are in the first axes of `real` and `imag`.
|
1706
|
-
Other axes are averaged for display.
|
1707
|
-
|
1708
|
-
Parameters
|
1709
|
-
----------
|
1710
|
-
mean : array_like
|
1711
|
-
Image average. Must be two or more dimensional, or None.
|
1712
|
-
real : array_like
|
1713
|
-
Image of real component of phasor coordinates.
|
1714
|
-
The last dimensions must match shape of `mean`.
|
1715
|
-
imag : array_like
|
1716
|
-
Image of imaginary component of phasor coordinates.
|
1717
|
-
Must be same shape as `real`.
|
1718
|
-
harmonics : int, optional
|
1719
|
-
Number of harmonics to display.
|
1720
|
-
If `mean` is None, a nonzero value indicates the presence of harmonics
|
1721
|
-
in the first axes of `mean` and `real`. Else, the presence of harmonics
|
1722
|
-
is determined from the shapes of `mean` and `real`.
|
1723
|
-
By default, up to 4 harmonics are displayed.
|
1724
|
-
percentile : float, optional
|
1725
|
-
The (q, 100-q) percentiles of image data are covered by colormaps.
|
1726
|
-
By default, the complete value range of `mean` is covered,
|
1727
|
-
for `real` and `imag` the range [-1, 1].
|
1728
|
-
title : str, optional
|
1729
|
-
Figure title.
|
1730
|
-
show : bool, optional, default: True
|
1731
|
-
Display figure.
|
1732
|
-
**kwargs
|
1733
|
-
Additional arguments passed to :func:`matplotlib.pyplot.imshow`.
|
1734
|
-
|
1735
|
-
Raises
|
1736
|
-
------
|
1737
|
-
ValueError
|
1738
|
-
The shapes of `mean`, `real`, and `image` do not match.
|
1739
|
-
Percentile is out of range.
|
1740
|
-
|
1741
|
-
"""
|
1742
|
-
update_kwargs(kwargs, interpolation='nearest')
|
1743
|
-
cmap = kwargs.pop('cmap', None)
|
1744
|
-
shape = None
|
1745
|
-
|
1746
|
-
if mean is not None:
|
1747
|
-
mean = numpy.asarray(mean)
|
1748
|
-
if mean.ndim < 2:
|
1749
|
-
raise ValueError(f'not an image {mean.ndim=} < 2')
|
1750
|
-
shape = mean.shape
|
1751
|
-
mean = mean.reshape(-1, *mean.shape[-2:])
|
1752
|
-
if mean.shape[0] == 1:
|
1753
|
-
mean = mean[0]
|
1754
|
-
else:
|
1755
|
-
mean = numpy.nanmean(mean, axis=0)
|
1756
|
-
|
1757
|
-
real = numpy.asarray(real)
|
1758
|
-
imag = numpy.asarray(imag)
|
1759
|
-
if real.shape != imag.shape:
|
1760
|
-
raise ValueError(f'{real.shape=} != {imag.shape=}')
|
1761
|
-
if real.ndim < 2:
|
1762
|
-
raise ValueError(f'not an image {real.ndim=} < 2')
|
1763
|
-
|
1764
|
-
if (shape is not None and real.shape[1:] == shape) or (
|
1765
|
-
shape is None and harmonics
|
1766
|
-
):
|
1767
|
-
# first image dimension contains harmonics
|
1768
|
-
if real.ndim < 3:
|
1769
|
-
raise ValueError(f'not a multi-harmonic image {real.shape=}')
|
1770
|
-
nh = real.shape[0] # number harmonics
|
1771
|
-
elif shape is None or shape == real.shape:
|
1772
|
-
# single harmonic
|
1773
|
-
nh = 1
|
1774
|
-
else:
|
1775
|
-
raise ValueError(f'shape mismatch {real.shape[1:]=} != {shape}')
|
1776
|
-
|
1777
|
-
real = real.reshape(nh, -1, *real.shape[-2:])
|
1778
|
-
imag = imag.reshape(nh, -1, *imag.shape[-2:])
|
1779
|
-
if real.shape[1] == 1:
|
1780
|
-
real = real[:, 0]
|
1781
|
-
imag = imag[:, 0]
|
1782
|
-
else:
|
1783
|
-
real = numpy.nanmean(real, axis=1)
|
1784
|
-
imag = numpy.nanmean(imag, axis=1)
|
1785
|
-
|
1786
|
-
# for MyPy
|
1787
|
-
assert isinstance(mean, numpy.ndarray) or mean is None
|
1788
|
-
assert isinstance(real, numpy.ndarray)
|
1789
|
-
assert isinstance(imag, numpy.ndarray)
|
1790
|
-
|
1791
|
-
# limit number of displayed harmonics
|
1792
|
-
nh = min(4 if harmonics is None else harmonics, nh)
|
1793
|
-
|
1794
|
-
# create figure with size depending on image aspect and number of harmonics
|
1795
|
-
fig = pyplot.figure(layout='constrained')
|
1796
|
-
w, h = fig.get_size_inches()
|
1797
|
-
aspect = min(1.0, max(0.5, real.shape[-2] / real.shape[-1]))
|
1798
|
-
fig.set_size_inches(w, h * 0.4 * aspect * nh + h * 0.25 * aspect)
|
1799
|
-
gs = GridSpec(nh, 2 if mean is None else 3, figure=fig)
|
1800
|
-
if title:
|
1801
|
-
fig.suptitle(title)
|
1802
|
-
|
1803
|
-
if mean is not None:
|
1804
|
-
_imshow(
|
1805
|
-
fig.add_subplot(gs[0, 0]),
|
1806
|
-
mean,
|
1807
|
-
percentile=percentile,
|
1808
|
-
vmin=None,
|
1809
|
-
vmax=None,
|
1810
|
-
cmap=cmap,
|
1811
|
-
axis=True,
|
1812
|
-
title='mean',
|
1813
|
-
**kwargs,
|
1814
|
-
)
|
1815
|
-
|
1816
|
-
if percentile is None:
|
1817
|
-
vmin = -1.0
|
1818
|
-
vmax = 1.0
|
1819
|
-
if cmap is None:
|
1820
|
-
cmap = 'coolwarm_r'
|
1821
|
-
else:
|
1822
|
-
vmin = None
|
1823
|
-
vmax = None
|
1824
|
-
|
1825
|
-
for h in range(nh):
|
1826
|
-
axs = []
|
1827
|
-
ax = fig.add_subplot(gs[h, -2])
|
1828
|
-
axs.append(ax)
|
1829
|
-
_imshow(
|
1830
|
-
ax,
|
1831
|
-
real[h],
|
1832
|
-
percentile=percentile,
|
1833
|
-
vmin=vmin,
|
1834
|
-
vmax=vmax,
|
1835
|
-
cmap=cmap,
|
1836
|
-
axis=mean is None and h == 0,
|
1837
|
-
colorbar=percentile is not None,
|
1838
|
-
title=None if h else 'G, real',
|
1839
|
-
**kwargs,
|
1840
|
-
)
|
1841
|
-
|
1842
|
-
ax = fig.add_subplot(gs[h, -1])
|
1843
|
-
axs.append(ax)
|
1844
|
-
pos = _imshow(
|
1845
|
-
ax,
|
1846
|
-
imag[h],
|
1847
|
-
percentile=percentile,
|
1848
|
-
vmin=vmin,
|
1849
|
-
vmax=vmax,
|
1850
|
-
cmap=cmap,
|
1851
|
-
axis=False,
|
1852
|
-
colorbar=percentile is not None,
|
1853
|
-
title=None if h else 'S, imag',
|
1854
|
-
**kwargs,
|
1855
|
-
)
|
1856
|
-
if percentile is None and h == 0:
|
1857
|
-
fig.colorbar(pos, ax=axs, shrink=0.4, location='bottom')
|
1858
|
-
|
1859
|
-
if show:
|
1860
|
-
pyplot.show()
|
1861
|
-
|
1862
|
-
|
1863
|
-
def plot_signal_image(
|
1864
|
-
signal: ArrayLike,
|
1865
|
-
/,
|
1866
|
-
*,
|
1867
|
-
axis: int | str | None = None,
|
1868
|
-
percentile: float | Sequence[float] | None = None,
|
1869
|
-
title: str | None = None,
|
1870
|
-
xlabel: str | None = None,
|
1871
|
-
show: bool = True,
|
1872
|
-
**kwargs: Any,
|
1873
|
-
) -> None:
|
1874
|
-
"""Plot average image and signal along axis.
|
1875
|
-
|
1876
|
-
Preview time-resolved or hyperspectral image stacks to be anayzed with
|
1877
|
-
:py:func:`phasorpy.phasor.phasor_from_signal`.
|
1878
|
-
|
1879
|
-
The last two axes, excluding `axis`, are assumed to be the image axes.
|
1880
|
-
Other axes are averaged for image display.
|
1881
|
-
|
1882
|
-
Parameters
|
1883
|
-
----------
|
1884
|
-
signal : array_like
|
1885
|
-
Image stack. Must be three or more dimensional.
|
1886
|
-
axis : int or str, optional
|
1887
|
-
Axis over which phasor coordinates would be computed.
|
1888
|
-
By default, the 'H' or 'C' axes if signal contains such dimension
|
1889
|
-
names, else the last axis (-1).
|
1890
|
-
percentile : float or [float, float], optional
|
1891
|
-
The [q, 100-q] percentiles of image data are covered by colormaps.
|
1892
|
-
By default, the complete value range of `mean` is covered,
|
1893
|
-
for `real` and `imag` the range [-1, 1].
|
1894
|
-
title : str, optional
|
1895
|
-
Figure title.
|
1896
|
-
xlabel : str, optional
|
1897
|
-
Label of axis over which phasor coordinates would be computed.
|
1898
|
-
show : bool, optional, default: True
|
1899
|
-
Display figure.
|
1900
|
-
**kwargs
|
1901
|
-
Additional arguments passed to :func:`matplotlib.pyplot.imshow`.
|
1902
|
-
|
1903
|
-
Raises
|
1904
|
-
------
|
1905
|
-
ValueError
|
1906
|
-
Signal is not an image stack.
|
1907
|
-
Percentile is out of range.
|
1908
|
-
|
1909
|
-
"""
|
1910
|
-
# TODO: add option to separate channels?
|
1911
|
-
# TODO: add option to plot non-images?
|
1912
|
-
|
1913
|
-
axis, axis_label = parse_signal_axis(signal, axis)
|
1914
|
-
if (
|
1915
|
-
axis_label
|
1916
|
-
and hasattr(signal, 'coords')
|
1917
|
-
and axis_label in signal.coords
|
1918
|
-
):
|
1919
|
-
axis_coords = signal.coords[axis_label]
|
1920
|
-
else:
|
1921
|
-
axis_coords = None
|
1922
|
-
|
1923
|
-
update_kwargs(kwargs, interpolation='nearest')
|
1924
|
-
signal = numpy.asarray(signal)
|
1925
|
-
if signal.ndim < 3:
|
1926
|
-
raise ValueError(f'not an image stack {signal.ndim=} < 3')
|
1927
|
-
|
1928
|
-
axis %= signal.ndim
|
1929
|
-
|
1930
|
-
# for MyPy
|
1931
|
-
assert isinstance(signal, numpy.ndarray)
|
1932
|
-
|
1933
|
-
fig = pyplot.figure(layout='constrained')
|
1934
|
-
if title:
|
1935
|
-
fig.suptitle(title)
|
1936
|
-
w, h = fig.get_size_inches()
|
1937
|
-
fig.set_size_inches(w, h * 0.7)
|
1938
|
-
gs = GridSpec(1, 2, figure=fig, width_ratios=(1, 1))
|
1939
|
-
|
1940
|
-
# histogram
|
1941
|
-
axes = list(range(signal.ndim))
|
1942
|
-
del axes[axis]
|
1943
|
-
ax = fig.add_subplot(gs[0, 1])
|
1944
|
-
|
1945
|
-
if axis_coords is not None:
|
1946
|
-
ax.set_title(f'{axis=} {axis_label!r}')
|
1947
|
-
ax.plot(axis_coords, numpy.nanmean(signal, axis=tuple(axes)))
|
1948
|
-
else:
|
1949
|
-
ax.set_title(f'{axis=}')
|
1950
|
-
ax.plot(numpy.nanmean(signal, axis=tuple(axes)))
|
1951
|
-
|
1952
|
-
ax.set_ylim(kwargs.get('vmin', None), kwargs.get('vmax', None))
|
1953
|
-
|
1954
|
-
if xlabel is not None:
|
1955
|
-
ax.set_xlabel(xlabel)
|
1956
|
-
|
1957
|
-
# image
|
1958
|
-
axes = list(sorted(axes[:-2] + [axis]))
|
1959
|
-
ax = fig.add_subplot(gs[0, 0])
|
1960
|
-
_imshow(
|
1961
|
-
ax,
|
1962
|
-
numpy.nanmean(signal, axis=tuple(axes)),
|
1963
|
-
percentile=percentile,
|
1964
|
-
shrink=0.5,
|
1965
|
-
title='mean',
|
1966
|
-
**kwargs,
|
1967
|
-
)
|
1968
|
-
|
1969
|
-
if show:
|
1970
|
-
pyplot.show()
|
1971
|
-
|
1972
|
-
|
1973
|
-
def plot_image(
|
1974
|
-
*images: ArrayLike,
|
1975
|
-
percentile: float | None = None,
|
1976
|
-
columns: int | None = None,
|
1977
|
-
title: str | None = None,
|
1978
|
-
labels: Sequence[str | None] | None = None,
|
1979
|
-
show: bool = True,
|
1980
|
-
**kwargs: Any,
|
1981
|
-
) -> None:
|
1982
|
-
"""Plot images.
|
1983
|
-
|
1984
|
-
Parameters
|
1985
|
-
----------
|
1986
|
-
*images : array_like
|
1987
|
-
Images to be plotted. Must be two or more dimensional.
|
1988
|
-
The last two axes are assumed to be the image axes.
|
1989
|
-
Other axes are averaged for display.
|
1990
|
-
Three-dimensional images with last axis size of three or four
|
1991
|
-
are plotted as RGB(A) images.
|
1992
|
-
percentile : float, optional
|
1993
|
-
The (q, 100-q) percentiles of image data are covered by colormaps.
|
1994
|
-
By default, the complete value range is covered.
|
1995
|
-
Does not apply to RGB images.
|
1996
|
-
columns : int, optional
|
1997
|
-
Number of columns in figure.
|
1998
|
-
By default, up to four columns are used.
|
1999
|
-
title : str, optional
|
2000
|
-
Figure title.
|
2001
|
-
labels : sequence of str, optional
|
2002
|
-
Labels for each image.
|
2003
|
-
show : bool, optional, default: True
|
2004
|
-
Display figure.
|
2005
|
-
**kwargs
|
2006
|
-
Additional arguments passed to :func:`matplotlib.pyplot.imshow`.
|
2007
|
-
|
2008
|
-
Raises
|
2009
|
-
------
|
2010
|
-
ValueError
|
2011
|
-
Percentile is out of range.
|
2012
|
-
|
2013
|
-
"""
|
2014
|
-
update_kwargs(
|
2015
|
-
kwargs, interpolation='nearest', location='right', shrink=0.5
|
2016
|
-
)
|
2017
|
-
cmap = kwargs.pop('cmap', None)
|
2018
|
-
figsize = kwargs.pop('figsize', None)
|
2019
|
-
subplot_kw = kwargs.pop('subplot_kw', {})
|
2020
|
-
location = kwargs['location']
|
2021
|
-
allrgb = True
|
2022
|
-
|
2023
|
-
arrays = []
|
2024
|
-
shape = [1, 1]
|
2025
|
-
for image in images:
|
2026
|
-
image = numpy.asarray(image)
|
2027
|
-
if image.ndim < 2:
|
2028
|
-
raise ValueError(f'not an image {image.ndim=} < 2')
|
2029
|
-
if image.ndim == 3 and image.shape[2] in {3, 4}:
|
2030
|
-
# RGB(A)
|
2031
|
-
pass
|
2032
|
-
else:
|
2033
|
-
allrgb = False
|
2034
|
-
image = image.reshape(-1, *image.shape[-2:])
|
2035
|
-
if image.shape[0] == 1:
|
2036
|
-
image = image[0]
|
2037
|
-
else:
|
2038
|
-
with warnings.catch_warnings():
|
2039
|
-
warnings.filterwarnings('ignore', category=RuntimeWarning)
|
2040
|
-
image = numpy.nanmean(image, axis=0)
|
2041
|
-
assert isinstance(image, numpy.ndarray)
|
2042
|
-
for i in (-1, -2):
|
2043
|
-
if image.shape[i] > shape[i]:
|
2044
|
-
shape[i] = image.shape[i]
|
2045
|
-
arrays.append(image)
|
2046
|
-
|
2047
|
-
if columns is None:
|
2048
|
-
n = len(arrays)
|
2049
|
-
if n < 3:
|
2050
|
-
columns = n
|
2051
|
-
elif n < 5:
|
2052
|
-
columns = 2
|
2053
|
-
elif n < 7:
|
2054
|
-
columns = 3
|
2055
|
-
else:
|
2056
|
-
columns = 4
|
2057
|
-
rows = int(numpy.ceil(len(arrays) / columns))
|
2058
|
-
|
2059
|
-
vmin = None
|
2060
|
-
vmax = None
|
2061
|
-
if percentile is None:
|
2062
|
-
vmin = kwargs.pop('vmin', None)
|
2063
|
-
vmax = kwargs.pop('vmax', None)
|
2064
|
-
if vmin is None:
|
2065
|
-
vmin = numpy.inf
|
2066
|
-
for image in images:
|
2067
|
-
vmin = min(vmin, numpy.nanmin(image))
|
2068
|
-
if vmin == numpy.inf:
|
2069
|
-
vmin = None
|
2070
|
-
if vmax is None:
|
2071
|
-
vmax = -numpy.inf
|
2072
|
-
for image in images:
|
2073
|
-
vmax = max(vmax, numpy.nanmax(image))
|
2074
|
-
if vmax == -numpy.inf:
|
2075
|
-
vmax = None
|
2076
|
-
|
2077
|
-
# create figure with size depending on image aspect
|
2078
|
-
fig = pyplot.figure(layout='constrained', figsize=figsize)
|
2079
|
-
if figsize is None:
|
2080
|
-
# TODO: find optimal figure height as a function of
|
2081
|
-
# number of rows and columns, image shapes, labels, and colorbar
|
2082
|
-
# presence and placements.
|
2083
|
-
if allrgb:
|
2084
|
-
hadd = 0.0
|
2085
|
-
elif location == 'right':
|
2086
|
-
hadd = 0.5
|
2087
|
-
else:
|
2088
|
-
hadd = 1.2
|
2089
|
-
if labels is not None:
|
2090
|
-
hadd += 0.3 * rows
|
2091
|
-
w, h = fig.get_size_inches()
|
2092
|
-
aspect = min(1.0, max(0.5, shape[0] / shape[1]))
|
2093
|
-
fig.set_size_inches(
|
2094
|
-
w, h * 0.9 / columns * aspect * rows + h * 0.1 * aspect + hadd
|
2095
|
-
)
|
2096
|
-
gs = GridSpec(rows, columns, figure=fig)
|
2097
|
-
if title:
|
2098
|
-
fig.suptitle(title)
|
2099
|
-
|
2100
|
-
axs = []
|
2101
|
-
for i, image in enumerate(arrays):
|
2102
|
-
ax = fig.add_subplot(gs[i // columns, i % columns], **subplot_kw)
|
2103
|
-
ax.set_anchor('C')
|
2104
|
-
axs.append(ax)
|
2105
|
-
pos = _imshow(
|
2106
|
-
ax,
|
2107
|
-
image,
|
2108
|
-
percentile=percentile,
|
2109
|
-
vmin=vmin,
|
2110
|
-
vmax=vmax,
|
2111
|
-
cmap=cmap,
|
2112
|
-
colorbar=percentile is not None,
|
2113
|
-
axis=i == 0 and not subplot_kw,
|
2114
|
-
title=None if labels is None else labels[i],
|
2115
|
-
**kwargs,
|
2116
|
-
)
|
2117
|
-
if not allrgb and percentile is None:
|
2118
|
-
fig.colorbar(pos, ax=axs, shrink=kwargs['shrink'], location=location)
|
2119
|
-
|
2120
|
-
if show:
|
2121
|
-
pyplot.show()
|
2122
|
-
|
2123
|
-
|
2124
|
-
def plot_polar_frequency(
|
2125
|
-
frequency: ArrayLike,
|
2126
|
-
phase: ArrayLike,
|
2127
|
-
modulation: ArrayLike,
|
2128
|
-
*,
|
2129
|
-
ax: Axes | None = None,
|
2130
|
-
title: str | None = None,
|
2131
|
-
show: bool = True,
|
2132
|
-
**kwargs: Any,
|
2133
|
-
) -> None:
|
2134
|
-
"""Plot phase and modulation verus frequency.
|
2135
|
-
|
2136
|
-
Parameters
|
2137
|
-
----------
|
2138
|
-
frequency : array_like, shape (n, )
|
2139
|
-
Laser pulse or modulation frequency in MHz.
|
2140
|
-
phase : array_like
|
2141
|
-
Angular component of polar coordinates in radians.
|
2142
|
-
modulation : array_like
|
2143
|
-
Radial component of polar coordinates.
|
2144
|
-
ax : matplotlib axes, optional
|
2145
|
-
Matplotlib axes used for plotting.
|
2146
|
-
By default, a new subplot axes is created.
|
2147
|
-
title : str, optional
|
2148
|
-
Figure title. The default is "Multi-frequency plot".
|
2149
|
-
show : bool, optional, default: True
|
2150
|
-
Display figure.
|
2151
|
-
**kwargs
|
2152
|
-
Additional arguments passed to :py:func:`matplotlib.pyplot.plot`.
|
2153
|
-
|
2154
|
-
"""
|
2155
|
-
# TODO: make this customizable: labels, colors, ...
|
2156
|
-
if ax is None:
|
2157
|
-
ax = pyplot.subplots()[1]
|
2158
|
-
if title is None:
|
2159
|
-
title = 'Multi-frequency plot'
|
2160
|
-
if title:
|
2161
|
-
ax.set_title(title)
|
2162
|
-
ax.set_xscale('log', base=10)
|
2163
|
-
ax.set_xlabel('Frequency (MHz)')
|
2164
|
-
|
2165
|
-
phase = numpy.asarray(phase)
|
2166
|
-
if phase.ndim < 2:
|
2167
|
-
phase = phase.reshape(-1, 1)
|
2168
|
-
modulation = numpy.asarray(modulation)
|
2169
|
-
if modulation.ndim < 2:
|
2170
|
-
modulation = modulation.reshape(-1, 1)
|
2171
|
-
|
2172
|
-
ax.set_ylabel('Phase (°)', color='tab:blue')
|
2173
|
-
ax.set_yticks([0.0, 30.0, 60.0, 90.0])
|
2174
|
-
for phi in phase.T:
|
2175
|
-
ax.plot(frequency, numpy.rad2deg(phi), color='tab:blue', **kwargs)
|
2176
|
-
ax = ax.twinx()
|
2177
|
-
|
2178
|
-
ax.set_ylabel('Modulation (%)', color='tab:red')
|
2179
|
-
ax.set_yticks([0.0, 25.0, 50.0, 75.0, 100.0])
|
2180
|
-
for mod in modulation.T:
|
2181
|
-
ax.plot(frequency, mod * 100, color='tab:red', **kwargs)
|
2182
|
-
if show:
|
2183
|
-
pyplot.show()
|
2184
|
-
|
2185
|
-
|
2186
|
-
def plot_histograms(
|
2187
|
-
*data: ArrayLike,
|
2188
|
-
title: str | None = None,
|
2189
|
-
xlabel: str | None = None,
|
2190
|
-
ylabel: str | None = None,
|
2191
|
-
labels: Sequence[str] | None = None,
|
2192
|
-
show: bool = True,
|
2193
|
-
**kwargs: Any,
|
2194
|
-
) -> None:
|
2195
|
-
"""Plot histograms of flattened data arrays.
|
2196
|
-
|
2197
|
-
Parameters
|
2198
|
-
----------
|
2199
|
-
data: array_like
|
2200
|
-
Data arrays to be plotted as histograms.
|
2201
|
-
title : str, optional
|
2202
|
-
Figure title.
|
2203
|
-
xlabel : str, optional
|
2204
|
-
Label for x-axis.
|
2205
|
-
ylabel : str, optional
|
2206
|
-
Label for y-axis.
|
2207
|
-
labels: sequence of str, optional
|
2208
|
-
Labels for each data array.
|
2209
|
-
show : bool, optional, default: True
|
2210
|
-
Display figure.
|
2211
|
-
**kwargs
|
2212
|
-
Additional arguments passed to :func:`matplotlib.pyplot.hist`.
|
2213
|
-
|
2214
|
-
"""
|
2215
|
-
ax = pyplot.subplots()[1]
|
2216
|
-
if kwargs.get('alpha') is None:
|
2217
|
-
ax.hist(
|
2218
|
-
[numpy.asarray(d).flatten() for d in data], label=labels, **kwargs
|
2219
|
-
)
|
2220
|
-
else:
|
2221
|
-
for d, label in zip(
|
2222
|
-
data, [None] * len(data) if labels is None else labels
|
2223
|
-
):
|
2224
|
-
ax.hist(numpy.asarray(d).flatten(), label=label, **kwargs)
|
2225
|
-
if title is not None:
|
2226
|
-
ax.set_title(title)
|
2227
|
-
if xlabel is not None:
|
2228
|
-
ax.set_xlabel(xlabel)
|
2229
|
-
if ylabel is not None:
|
2230
|
-
ax.set_ylabel(ylabel)
|
2231
|
-
if labels is not None:
|
2232
|
-
ax.legend()
|
2233
|
-
pyplot.tight_layout()
|
2234
|
-
if show:
|
2235
|
-
pyplot.show()
|
2236
|
-
|
2237
|
-
|
2238
|
-
def _imshow(
|
2239
|
-
ax: Axes,
|
2240
|
-
image: NDArray[Any],
|
2241
|
-
/,
|
2242
|
-
*,
|
2243
|
-
percentile: float | Sequence[float] | None = None,
|
2244
|
-
vmin: float | None = None,
|
2245
|
-
vmax: float | None = None,
|
2246
|
-
colorbar: bool = True,
|
2247
|
-
shrink: float | None = None,
|
2248
|
-
axis: bool = True,
|
2249
|
-
title: str | None = None,
|
2250
|
-
**kwargs: Any,
|
2251
|
-
) -> AxesImage:
|
2252
|
-
"""Plot image array.
|
2253
|
-
|
2254
|
-
Convenience wrapper around :py:func:`matplotlib.pyplot.imshow`.
|
2255
|
-
|
2256
|
-
"""
|
2257
|
-
update_kwargs(kwargs, interpolation='none')
|
2258
|
-
location = kwargs.pop('location', 'bottom')
|
2259
|
-
if image.ndim == 3 and image.shape[2] in {3, 4}:
|
2260
|
-
# RGB(A)
|
2261
|
-
vmin = None
|
2262
|
-
vmax = None
|
2263
|
-
percentile = None
|
2264
|
-
colorbar = False
|
2265
|
-
if percentile is not None:
|
2266
|
-
if isinstance(percentile, Sequence):
|
2267
|
-
percentile = percentile[0], percentile[1]
|
2268
|
-
else:
|
2269
|
-
# percentile = max(0.0, min(50, percentile))
|
2270
|
-
percentile = percentile, 100.0 - percentile
|
2271
|
-
if (
|
2272
|
-
percentile[0] >= percentile[1]
|
2273
|
-
or percentile[0] < 0
|
2274
|
-
or percentile[1] > 100
|
2275
|
-
):
|
2276
|
-
raise ValueError(f'{percentile=} out of range')
|
2277
|
-
vmin, vmax = numpy.nanpercentile(image, percentile)
|
2278
|
-
pos = ax.imshow(image, vmin=vmin, vmax=vmax, **kwargs)
|
2279
|
-
if colorbar:
|
2280
|
-
if percentile is not None and vmin is not None and vmax is not None:
|
2281
|
-
ticks = vmin, vmax
|
2282
|
-
else:
|
2283
|
-
ticks = None
|
2284
|
-
fig = ax.get_figure()
|
2285
|
-
if fig is not None:
|
2286
|
-
if shrink is None:
|
2287
|
-
shrink = 0.8
|
2288
|
-
fig.colorbar(pos, shrink=shrink, location=location, ticks=ticks)
|
2289
|
-
if title:
|
2290
|
-
ax.set_title(title)
|
2291
|
-
if not axis:
|
2292
|
-
ax.set_axis_off()
|
2293
|
-
# ax.set_anchor('C')
|
2294
|
-
return pos
|
2295
|
-
|
2296
|
-
|
2297
|
-
def _semicircle_ticks(
|
2298
|
-
frequency: float,
|
2299
|
-
lifetime: Sequence[float] | None = None,
|
2300
|
-
labels: Sequence[str] | None = None,
|
2301
|
-
) -> tuple[tuple[float, ...], tuple[str, ...]]:
|
2302
|
-
"""Return semicircle tick lifetimes and labels at frequency."""
|
2303
|
-
if lifetime is None:
|
2304
|
-
lifetime = [0.0] + [
|
2305
|
-
2**t
|
2306
|
-
for t in range(-8, 32)
|
2307
|
-
if phasor_from_lifetime(frequency, 2**t)[1] >= 0.18
|
2308
|
-
]
|
2309
|
-
unit = 'ns'
|
2310
|
-
else:
|
2311
|
-
unit = ''
|
2312
|
-
if labels is None:
|
2313
|
-
labels = [f'{tau:g}' for tau in lifetime]
|
2314
|
-
try:
|
2315
|
-
labels[2] = f'{labels[2]} {unit}'
|
2316
|
-
except IndexError:
|
2317
|
-
pass
|
2318
|
-
return tuple(lifetime), tuple(labels)
|