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