phasorpy 0.6__cp312-cp312-win_arm64.whl → 0.8__cp312-cp312-win_arm64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- phasorpy/__init__.py +1 -1
- phasorpy/_phasorpy.cp312-win_arm64.pyd +0 -0
- phasorpy/_phasorpy.pyx +320 -10
- phasorpy/_utils.py +114 -33
- phasorpy/cli.py +19 -1
- phasorpy/cluster.py +12 -18
- phasorpy/color.py +11 -7
- phasorpy/{components.py → component.py} +263 -36
- phasorpy/{cursors.py → cursor.py} +31 -33
- phasorpy/datasets.py +118 -8
- phasorpy/experimental.py +4 -168
- phasorpy/filter.py +966 -0
- phasorpy/io/__init__.py +3 -1
- phasorpy/io/_flimlabs.py +26 -16
- phasorpy/io/_leica.py +38 -34
- phasorpy/io/_ometiff.py +10 -9
- phasorpy/io/_other.py +116 -8
- phasorpy/io/_simfcs.py +52 -24
- phasorpy/lifetime.py +2058 -0
- phasorpy/phasor.py +106 -2502
- phasorpy/plot/_functions.py +13 -7
- phasorpy/plot/_lifetime_plots.py +34 -24
- phasorpy/plot/_phasorplot.py +561 -176
- phasorpy/plot/_phasorplot_fret.py +12 -10
- phasorpy/utils.py +22 -10
- {phasorpy-0.6.dist-info → phasorpy-0.8.dist-info}/METADATA +8 -7
- phasorpy-0.8.dist-info/RECORD +36 -0
- phasorpy-0.6.dist-info/RECORD +0 -34
- {phasorpy-0.6.dist-info → phasorpy-0.8.dist-info}/WHEEL +0 -0
- {phasorpy-0.6.dist-info → phasorpy-0.8.dist-info}/entry_points.txt +0 -0
- {phasorpy-0.6.dist-info → phasorpy-0.8.dist-info}/licenses/LICENSE.txt +0 -0
- {phasorpy-0.6.dist-info → phasorpy-0.8.dist-info}/top_level.txt +0 -0
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>`_,
|
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
|
-
- ``'
|
71
|
-
|
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
|
-
|
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 :
|
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>`
|
242
|
-
|
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
|
-
|
252
|
-
|
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
|
-
|
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.
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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 :
|
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
|
-
|
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.
|
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.
|
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
|
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
|
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
|
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 :
|
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
|
-
|
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)
|