phasorpy 0.6__cp313-cp313-win_amd64.whl → 0.8__cp313-cp313-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/io/__init__.py CHANGED
@@ -8,6 +8,7 @@ The ``phasorpy.io`` module provides functions to:
8
8
  - :py:func:`signal_from_lif` - Leica LIF and XLEF
9
9
  - :py:func:`signal_from_lsm` - Zeiss LSM
10
10
  - :py:func:`signal_from_ptu` - PicoQuant PTU
11
+ - :py:func:`signal_from_pqbin` - PicoQuant BIN
11
12
  - :py:func:`signal_from_sdt` - Becker & Hickl SDT
12
13
  - :py:func:`signal_from_fbd` - FLIMbox FBD
13
14
  - :py:func:`signal_from_flimlabs_json` - FLIM LABS JSON
@@ -46,7 +47,8 @@ third-party file reader libraries, currently
46
47
  `tifffile <https://github.com/cgohlke/tifffile>`_,
47
48
  `ptufile <https://github.com/cgohlke/ptufile>`_,
48
49
  `liffile <https://github.com/cgohlke/liffile>`_,
49
- `sdtfile <https://github.com/cgohlke/sdtfile>`_, and
50
+ `sdtfile <https://github.com/cgohlke/sdtfile>`_,
51
+ `fbdfile <https://github.com/cgohlke/fbdfile>`_, and
50
52
  `lfdfiles <https://github.com/cgohlke/lfdfiles>`_.
51
53
  For advanced or unsupported use cases, consider using these libraries directly.
52
54
 
phasorpy/io/_flimlabs.py CHANGED
@@ -67,10 +67,13 @@ def phasor_from_flimlabs_json(
67
67
 
68
68
  - ``'dims'`` (tuple of str):
69
69
  :ref:`Axes codes <axes>` for `mean` image dimensions.
70
- - ``'harmonic'`` (int):
71
- Harmonic of `real` and `imag`.
70
+ - ``'samples'`` (int):
71
+ Number of time bins (always 256).
72
+ - ``'harmonic'`` (int or list of int):
73
+ Harmonic(s) of `real` and `imag`.
74
+ Single int if one harmonic, list if multiple harmonics.
72
75
  - ``'frequency'`` (float):
73
- Fundamental frequency of time-resolved phasor coordinates in MHz.
76
+ Laser repetition frequency in MHz.
74
77
  - ``'flimlabs_header'`` (dict):
75
78
  FLIM LABS file header.
76
79
 
@@ -154,9 +157,9 @@ def phasor_from_flimlabs_json(
154
157
 
155
158
  shape: tuple[int, ...] = nharmonics, nchannels, height, width
156
159
  axes: str = 'CYX'
157
- mean = numpy.zeros(shape[1:], dtype)
158
- real = numpy.zeros(shape, dtype)
159
- imag = numpy.zeros(shape, dtype)
160
+ mean = numpy.zeros(shape[1:], dtype=dtype)
161
+ real = numpy.zeros(shape, dtype=dtype)
162
+ imag = numpy.zeros(shape, dtype=dtype)
160
163
 
161
164
  for d in phasor_data:
162
165
  h = d['harmonic']
@@ -170,8 +173,8 @@ def phasor_from_flimlabs_json(
170
173
  else:
171
174
  c = channels.index(d['channel'])
172
175
 
173
- real[h, c] = numpy.asarray(d['g_data'], dtype)
174
- imag[h, c] = numpy.asarray(d['s_data'], dtype)
176
+ real[h, c] = numpy.asarray(d['g_data'], dtype=dtype)
177
+ imag[h, c] = numpy.asarray(d['s_data'], dtype=dtype)
175
178
 
176
179
  if 'intensities_data' in data:
177
180
  from .._phasorpy import _flimlabs_mean
@@ -231,27 +234,34 @@ def signal_from_flimlabs_json(
231
234
  Index of channel to return.
232
235
  By default, return the first channel.
233
236
  If None, return all channels.
234
- dtype : dtype-like, optional, default: uint16
237
+ dtype : dtype_like, optional, default: uint16
235
238
  Unsigned integer type of TCSPC histogram.
236
239
  Increase the bit-depth for high photon counts.
237
240
 
238
241
  Returns
239
242
  -------
240
243
  xarray.DataArray
241
- TCSPC histogram with :ref:`axes codes <axes>` ``'CYXH'`` and
242
- type specified in ``dtype``:
244
+ TCSPC histogram with :ref:`axes codes <axes>` depending on
245
+ `channel` parameter:
246
+
247
+ - Single channel: axes ``'YXH'``
248
+ - Multiple channels: axes ``'CYXH'``
249
+
250
+ Type specified by ``dtype`` parameter.
243
251
 
244
252
  - ``coords['H']``: delay-times of histogram bins in ns.
253
+ - ``coords['C']``: channel indices (if multiple channels).
245
254
  - ``attrs['frequency']``: laser repetition frequency in MHz.
246
255
  - ``attrs['flimlabs_header']``: FLIM LABS file header.
247
256
 
248
257
  Raises
249
258
  ------
250
259
  ValueError
251
- File is not a FLIM LABS JSON file containing TCSPC histogram.
252
- `dtype` is not an unsigned integer.
260
+ If file is not a valid JSON file.
261
+ If file is not a FLIM LABS JSON file containing TCSPC histogram.
262
+ If `dtype` is not an unsigned integer type.
253
263
  IndexError
254
- Channel out of range.
264
+ If `channel` is out of range.
255
265
 
256
266
  See Also
257
267
  --------
@@ -271,7 +281,7 @@ def signal_from_flimlabs_json(
271
281
  >>> signal.coords['H'].data
272
282
  array(...)
273
283
  >>> signal.attrs['frequency'] # doctest: +NUMBER
274
- 40.00
284
+ 40.0
275
285
 
276
286
  """
277
287
  with open(filename, 'rb') as fh:
@@ -317,7 +327,7 @@ def signal_from_flimlabs_json(
317
327
 
318
328
  from .._phasorpy import _flimlabs_signal
319
329
 
320
- signal = numpy.zeros((nchannels, height * width, 256), dtype)
330
+ signal = numpy.zeros((nchannels, height * width, 256), dtype=dtype)
321
331
  _flimlabs_signal(
322
332
  signal,
323
333
  intensities_data,
phasorpy/io/_leica.py CHANGED
@@ -82,7 +82,6 @@ def phasor_from_lif(
82
82
  import liffile
83
83
 
84
84
  image = '' if image is None else f'.*{image}.*/'
85
- samples = 1
86
85
 
87
86
  with liffile.LifFile(filename) as lif:
88
87
  try:
@@ -100,27 +99,9 @@ def phasor_from_lif(
100
99
  ) from exc
101
100
 
102
101
  attrs: dict[str, Any] = {'dims': dims, 'coords': coords}
103
- flim = im.parent_image
104
- if flim is not None and isinstance(flim, liffile.LifFlimImage):
105
- xml = flim.parent.xml_element
106
- frequency = xml.find('.//Dataset/RawData/LaserPulseFrequency')
107
- if frequency is not None and frequency.text is not None:
108
- attrs['frequency'] = float(frequency.text) * 1e-6
109
- clock_period = xml.find('.//Dataset/RawData/ClockPeriod')
110
- if clock_period is not None and clock_period.text is not None:
111
- tmp = float(clock_period.text) * float(frequency.text)
112
- samples = int(round(1.0 / tmp))
113
- attrs['samples'] = samples
114
- channels = []
115
- for channel in xml.findall(
116
- './/Dataset/FlimData/PhasorData/Channels'
117
- ):
118
- ch = liffile.xml2dict(channel)['Channels']
119
- ch.pop('PhasorPlotShapes', None)
120
- channels.append(ch)
121
- attrs['flim_phasor_channels'] = channels
122
- attrs['flim_rawdata'] = flim.attrs.get('RawData', {})
102
+ _flim_metadata(im.parent_image, attrs)
123
103
 
104
+ samples = attrs.get('samples', 1)
124
105
  if samples > 1:
125
106
  mean /= samples
126
107
  return (
@@ -140,6 +121,9 @@ def lifetime_from_lif(
140
121
 
141
122
  Leica image files may contain fluorescence lifetime images and metadata
142
123
  from the analysis of FLIM measurements.
124
+ The lifetimes are average photon arrival times ("Fast FLIM") according to
125
+ the LAS X FLIM/FCS documentation. They are not corrected for the IRF
126
+ position.
143
127
 
144
128
  Parameters
145
129
  ----------
@@ -151,7 +135,7 @@ def lifetime_from_lif(
151
135
  Returns
152
136
  -------
153
137
  lifetime : ndarray
154
- Fluorescence lifetime image in ns.
138
+ Fast FLIM lifetime image in ns.
155
139
  intensity : ndarray
156
140
  Fluorescence intensity image.
157
141
  stddev : ndarray
@@ -183,6 +167,8 @@ def lifetime_from_lif(
183
167
 
184
168
  Examples
185
169
  --------
170
+ Read Fast FLIM lifetime and related images from a Leica image file:
171
+
186
172
  >>> lifetime, intensity, stddev, attrs = lifetime_from_lif(
187
173
  ... fetch('FLIM_testdata.lif')
188
174
  ... )
@@ -193,6 +179,12 @@ def lifetime_from_lif(
193
179
  >>> attrs['frequency']
194
180
  19.505
195
181
 
182
+ Calibrate the Fast FLIM lifetime for IRF position:
183
+
184
+ >>> frequency = attrs['frequency']
185
+ >>> reference = attrs['flim_phasor_channels'][0]['AutomaticReferencePhase']
186
+ >>> lifetime -= math.radians(reference) / (2 * math.pi) / frequency * 1000
187
+
196
188
  """
197
189
  import liffile
198
190
 
@@ -213,18 +205,7 @@ def lifetime_from_lif(
213
205
  ) from exc
214
206
 
215
207
  attrs: dict[str, Any] = {'dims': dims, 'coords': coords}
216
- flim = im.parent_image
217
- if flim is not None and isinstance(flim, liffile.LifFlimImage):
218
- xml = flim.parent.xml_element
219
- frequency = xml.find('.//Dataset/RawData/LaserPulseFrequency')
220
- if frequency is not None and frequency.text is not None:
221
- attrs['frequency'] = float(frequency.text) * 1e-6
222
- clock_period = xml.find('.//Dataset/RawData/ClockPeriod')
223
- if clock_period is not None and clock_period.text is not None:
224
- tmp = float(clock_period.text) * float(frequency.text)
225
- samples = int(round(1.0 / tmp))
226
- attrs['samples'] = samples
227
- attrs['flim_rawdata'] = flim.attrs.get('RawData', {})
208
+ _flim_metadata(im.parent_image, attrs)
228
209
 
229
210
  return (
230
211
  lifetime.astype(numpy.float32),
@@ -234,6 +215,29 @@ def lifetime_from_lif(
234
215
  )
235
216
 
236
217
 
218
+ def _flim_metadata(flim: Any | None, attrs: dict[str, Any]) -> None:
219
+ """Add FLIM metadata to attrs."""
220
+ import liffile
221
+
222
+ if flim is not None and isinstance(flim, liffile.LifFlimImage):
223
+ xml = flim.parent.xml_element
224
+ frequency = xml.find('.//Dataset/RawData/LaserPulseFrequency')
225
+ if frequency is not None and frequency.text is not None:
226
+ attrs['frequency'] = float(frequency.text) * 1e-6
227
+ clock_period = xml.find('.//Dataset/RawData/ClockPeriod')
228
+ if clock_period is not None and clock_period.text is not None:
229
+ tmp = float(clock_period.text) * float(frequency.text)
230
+ samples = int(round(1.0 / tmp))
231
+ attrs['samples'] = samples
232
+ channels = []
233
+ for channel in xml.findall('.//Dataset/FlimData/PhasorData/Channels'):
234
+ ch = liffile.xml2dict(channel)['Channels']
235
+ ch.pop('PhasorPlotShapes', None)
236
+ channels.append(ch)
237
+ attrs['flim_phasor_channels'] = channels
238
+ attrs['flim_rawdata'] = flim.attrs.get('RawData', {})
239
+
240
+
237
241
  def signal_from_lif(
238
242
  filename: str | PathLike[Any],
239
243
  /,
phasorpy/io/_ometiff.py CHANGED
@@ -81,22 +81,21 @@ def phasor_to_ometiff(
81
81
  harmonic : int or sequence of int, optional
82
82
  Harmonics present in the first dimension of `real` and `imag`, if any.
83
83
  Write to image series named 'Phasor harmonic'.
84
- It is only needed if harmonics are not starting at and increasing by
85
- one.
84
+ Only needed if harmonics are not starting at and increasing by one.
86
85
  dims : sequence of str, optional
87
86
  Character codes for `mean` image dimensions.
88
87
  By default, the last dimensions are assumed to be 'TZCYX'.
89
88
  If harmonics are present in `real` and `imag`, an "other" (``Q``)
90
89
  dimension is prepended to axes for those arrays.
91
90
  Refer to the OME-TIFF model for allowed axes and their order.
92
- dtype : dtype-like, optional
91
+ dtype : dtype_like, optional
93
92
  Floating point data type used to store phasor coordinates.
94
93
  The default is ``float32``, which has 6 digits of precision
95
94
  and maximizes compatibility with other software.
96
95
  description : str, optional
97
96
  Plain-text description of dataset. Write as OME dataset description.
98
97
  **kwargs
99
- Additional arguments passed to :py:class:`tifffile.TiffWriter` and
98
+ Optional arguments passed to :py:class:`tifffile.TiffWriter` and
100
99
  :py:meth:`tifffile.TiffWriter.write`.
101
100
  For example, ``compression=None`` writes image data uncompressed.
102
101
 
@@ -136,9 +135,9 @@ def phasor_to_ometiff(
136
135
  if dtype.kind != 'f':
137
136
  raise ValueError(f'{dtype=} not a floating point type')
138
137
 
139
- mean = numpy.asarray(mean, dtype)
140
- real = numpy.asarray(real, dtype)
141
- imag = numpy.asarray(imag, dtype)
138
+ mean = numpy.asarray(mean, dtype=dtype)
139
+ real = numpy.asarray(real, dtype=dtype)
140
+ imag = numpy.asarray(imag, dtype=dtype)
142
141
  datasize = mean.nbytes + real.nbytes + imag.nbytes
143
142
 
144
143
  if real.shape != imag.shape:
@@ -167,7 +166,9 @@ def phasor_to_ometiff(
167
166
  raise ValueError('invalid harmonic')
168
167
 
169
168
  if frequency is not None:
170
- frequency_array = numpy.atleast_2d(frequency).astype(numpy.float64)
169
+ frequency_array = numpy.array(
170
+ frequency, dtype=numpy.float64, ndmin=2, copy=None
171
+ )
171
172
  if frequency_array.size > 1:
172
173
  raise ValueError('frequency must be scalar')
173
174
 
@@ -227,7 +228,7 @@ def phasor_to_ometiff(
227
228
 
228
229
  if harmonic is not None:
229
230
  tif.write(
230
- numpy.atleast_2d(harmonic).astype(numpy.uint32),
231
+ numpy.array(harmonic, dtype=numpy.uint32, ndmin=2),
231
232
  metadata={'Name': 'Phasor harmonic'},
232
233
  )
233
234
 
phasorpy/io/_other.py CHANGED
@@ -8,11 +8,13 @@ __all__ = [
8
8
  'phasor_from_ifli',
9
9
  'signal_from_imspector_tiff',
10
10
  'signal_from_lsm',
11
+ 'signal_from_pqbin',
11
12
  'signal_from_ptu',
12
13
  'signal_from_sdt',
13
14
  ]
14
15
 
15
16
  import os
17
+ import struct
16
18
  from typing import TYPE_CHECKING
17
19
  from xml.etree import ElementTree
18
20
 
@@ -48,7 +50,7 @@ def signal_from_sdt(
48
50
  filename : str or Path
49
51
  Name of Becker & Hickl SDT file to read.
50
52
  index : int, optional, default: 0
51
- Index of dataset to read in case the file contains multiple datasets.
53
+ Index of dataset to read if the file contains multiple datasets.
52
54
 
53
55
  Returns
54
56
  -------
@@ -63,7 +65,7 @@ def signal_from_sdt(
63
65
  Raises
64
66
  ------
65
67
  ValueError
66
- File is not a SDT file containing TCSPC histogram.
68
+ File is not an SDT file containing TCSPC histogram.
67
69
 
68
70
  Notes
69
71
  -----
@@ -127,6 +129,7 @@ def signal_from_ptu(
127
129
  channel: int | None = 0,
128
130
  dtime: int | None = 0,
129
131
  keepdims: bool = False,
132
+ **kwargs: Any,
130
133
  ) -> DataArray:
131
134
  """Return TCSPC histogram and metadata from PicoQuant PTU T3 mode file.
132
135
 
@@ -141,7 +144,7 @@ def signal_from_ptu(
141
144
  Indices for all dimensions of image mode files:
142
145
 
143
146
  - ``None``: return all items along axis (default).
144
- - ``Ellipsis``: return all items along multiple axes.
147
+ - ``Ellipsis`` (``...``): return all items along multiple axes.
145
148
  - ``int``: return single item along axis.
146
149
  - ``slice``: return chunk of axis.
147
150
  ``slice.step`` is a binning factor.
@@ -149,7 +152,7 @@ def signal_from_ptu(
149
152
 
150
153
  trimdims : str, optional, default: 'TCH'
151
154
  Axes to trim.
152
- dtype : dtype-like, optional, default: uint16
155
+ dtype : dtype_like, optional, default: uint16
153
156
  Unsigned integer type of TCSPC histogram.
154
157
  Increase the bit depth to avoid overflows when integrating.
155
158
  frame : int, optional
@@ -168,6 +171,9 @@ def signal_from_ptu(
168
171
  Overrides `selection` for axis ``H``.
169
172
  keepdims : bool, optional, default: False
170
173
  If true, return reduced axes as size-one dimensions.
174
+ **kwargs
175
+ Optional arguments passed to :py:meth:`PtuFile.decode_image`
176
+ or :py:meth:`PtuFile.decode_histogram`.
171
177
 
172
178
  Returns
173
179
  -------
@@ -214,6 +220,8 @@ def signal_from_ptu(
214
220
  import ptufile
215
221
  from xarray import DataArray
216
222
 
223
+ kwargs.pop('records', None)
224
+
217
225
  with ptufile.PtuFile(filename, trimdims=trimdims) as ptu:
218
226
  if not ptu.is_t3:
219
227
  raise ValueError(f'{ptu.filename!r} is not a T3 mode PTU file')
@@ -226,6 +234,7 @@ def signal_from_ptu(
226
234
  dtime=dtime,
227
235
  keepdims=keepdims,
228
236
  asxarray=True,
237
+ **kwargs,
229
238
  )
230
239
  assert isinstance(data, DataArray)
231
240
  elif ptu.measurement_submode == 1:
@@ -233,7 +242,7 @@ def signal_from_ptu(
233
242
  if dtime == -1:
234
243
  raise ValueError(f'{dtime=} not supported for point mode')
235
244
  data = ptu.decode_histogram(
236
- dtype=dtype, dtime=dtime, asxarray=True
245
+ dtype=dtype, dtime=dtime, asxarray=True, **kwargs
237
246
  )
238
247
  assert isinstance(data, DataArray)
239
248
  if channel is not None:
@@ -263,7 +272,7 @@ def signal_from_lsm(
263
272
  ) -> DataArray:
264
273
  """Return hyperspectral image and metadata from Zeiss LSM file.
265
274
 
266
- LSM files contain multi-dimensional images and metadata from laser
275
+ Zeiss LSM files contain multi-dimensional images and metadata from laser
267
276
  scanning microscopy measurements. The file format is based on TIFF.
268
277
 
269
278
  Parameters
@@ -542,7 +551,7 @@ def phasor_from_ifli(
542
551
  Index of channel to return.
543
552
  By default, return the first channel.
544
553
  If None, return all channels.
545
- harmonic : int, sequence of int, or 'all', optional
554
+ harmonic : int, sequence of int, 'any', or 'all', optional
546
555
  Harmonic(s) to return from file.
547
556
  If None (default), return the first harmonic stored in file.
548
557
  If `'all'`, return all harmonics of first frequency stored in file.
@@ -553,7 +562,7 @@ def phasor_from_ifli(
553
562
  If an integer, the returned `real` and `imag` arrays are single
554
563
  harmonic and have the same shape as `mean`.
555
564
  **kwargs
556
- Additional arguments passed to :py:meth:`lfdfiles.VistaIfli.asarray`,
565
+ Optional arguments passed to :py:meth:`lfdfiles.VistaIfli.asarray`,
557
566
  for example ``memmap=True``.
558
567
 
559
568
  Returns
@@ -780,3 +789,102 @@ def signal_from_flif(
780
789
  from xarray import DataArray
781
790
 
782
791
  return DataArray(data, **metadata)
792
+
793
+
794
+ def signal_from_pqbin(
795
+ filename: str | PathLike[Any],
796
+ /,
797
+ ) -> DataArray:
798
+ """Return TCSPC histogram and metadata from PicoQuant BIN file.
799
+
800
+ PicoQuant BIN files contain TCSPC histograms with limited metadata.
801
+
802
+ Parameters
803
+ ----------
804
+ filename : str or Path
805
+ Name of PicoQuant BIN file to read.
806
+
807
+ Returns
808
+ -------
809
+ xarray.DataArray
810
+ TCSPC histogram with :ref:`axes codes <axes>` ``'YXH'``,
811
+ and type ``uint32``.
812
+
813
+ - ``coords['H']``: delay-times of histogram bins in ns.
814
+ - ``attrs['frequency']``: repetition frequency in MHz.
815
+ This assumes that the histogram contains exactly one period.
816
+
817
+ Raises
818
+ ------
819
+ ValueError
820
+ File is not a PicoQuant BIN file.
821
+
822
+ Examples
823
+ --------
824
+ >>> signal = signal_from_pqbin('picoquant.bin') # doctest: +SKIP
825
+ >>> signal.values # doctest: +SKIP
826
+ array(...)
827
+ >>> signal.dtype # doctest: +SKIP
828
+ dtype('uint32')
829
+ >>> signal.shape # doctest: +SKIP
830
+ (256, 256, 2000)
831
+ >>> signal.dims # doctest: +SKIP
832
+ ('Y', 'X', 'H')
833
+ >>> signal.coords['H'].data # doctest: +SKIP
834
+ array([0, ..., 49.975])
835
+ >>> signal.attrs['frequency'] # doctest: +SKIP
836
+ 19.99
837
+
838
+ """
839
+ with open(filename, 'rb') as fh:
840
+ header = fh.read(20)
841
+ if len(header) != 20:
842
+ raise ValueError(
843
+ f'invalid PicoQuant BIN header length {len(header)} != 20'
844
+ )
845
+ (size_x, size_y, pixel_resolution, size_h, tcspc_resolution) = (
846
+ struct.unpack('<IIfIf', header)
847
+ )
848
+ size = size_y * size_x * size_h * 4
849
+
850
+ # check the header values against arbitrary but reasonable limits
851
+ # to detect invalid files and prevent memory errors
852
+ if (
853
+ size <= 0
854
+ or size > 2**35 - 1 # 32 GiB
855
+ or size_x > 16384
856
+ or size_y > 16384
857
+ or size_h > 16384
858
+ or pixel_resolution < 0.0
859
+ or pixel_resolution > 1.0
860
+ or tcspc_resolution <= 0.0
861
+ or tcspc_resolution > 1.0
862
+ ):
863
+ raise ValueError('invalid PicoQuant BIN file header')
864
+
865
+ shape = size_y, size_x, size_h
866
+ data = numpy.empty(shape, dtype='<u4')
867
+ if fh.readinto(data) != size:
868
+ raise ValueError('invalid PicoQuant BIN data size')
869
+
870
+ metadata = xarray_metadata(
871
+ ('Y', 'X', 'H'),
872
+ shape,
873
+ filename,
874
+ attrs={
875
+ 'frequency': 1000.0 / (size_h * tcspc_resolution), # MHz
876
+ 'pixel_resolution': pixel_resolution, # um
877
+ 'tcspc_resolution': tcspc_resolution, # ns
878
+ },
879
+ Y=numpy.linspace(
880
+ 0, size_y * pixel_resolution * 1e-6, size_y, endpoint=False
881
+ ),
882
+ X=numpy.linspace(
883
+ 0, size_x * pixel_resolution * 1e-6, size_x, endpoint=False
884
+ ),
885
+ H=numpy.linspace(0, size_h * tcspc_resolution, size_h, endpoint=False),
886
+ )
887
+
888
+ from xarray import DataArray
889
+
890
+ return DataArray(data, **metadata)