phasorpy 0.2__cp313-cp313-macosx_11_0_arm64.whl → 0.3__cp313-cp313-macosx_11_0_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.
Binary file
phasorpy/_phasorpy.pyx CHANGED
@@ -68,7 +68,8 @@ def _phasor_from_signal(
68
68
  float_t[:, :, ::1] phasor,
69
69
  const signal_t[:, :, ::1] signal,
70
70
  const double[:, :, ::1] sincos,
71
- const int num_threads
71
+ const bint normalize,
72
+ const int num_threads,
72
73
  ):
73
74
  """Return phasor coordinates from signal along middle axis.
74
75
 
@@ -97,6 +98,8 @@ def _phasor_from_signal(
97
98
  1. number samples
98
99
  2. cos and sin
99
100
 
101
+ normalize : bool
102
+ Normalize phasor coordinates.
100
103
  num_threads : int
101
104
  Number of OpenMP threads to use for parallelization.
102
105
 
@@ -145,14 +148,16 @@ def _phasor_from_signal(
145
148
  dc = dc + sample
146
149
  re = re + sample * sincos[h, k, 0]
147
150
  im = im + sample * sincos[h, k, 1]
148
- if dc != 0.0:
149
- re = re / dc
150
- im = im / dc
151
- dc = dc /samples
152
- else:
153
- dc = 0.0
154
- re = NAN if re == 0.0 else re * INFINITY
155
- im = NAN if im == 0.0 else im * INFINITY
151
+ if normalize:
152
+ if dc != 0.0:
153
+ # includes isnan(dc)
154
+ re = re / dc
155
+ im = im / dc
156
+ dc = dc / samples
157
+ else:
158
+ # dc = 0.0
159
+ re = NAN if re == 0.0 else re * INFINITY
160
+ im = NAN if im == 0.0 else im * INFINITY
156
161
  if h == 0:
157
162
  mean[i, j] = <float_t> dc
158
163
  real[h, i, j] = <float_t> re
@@ -173,14 +178,16 @@ def _phasor_from_signal(
173
178
  dc = dc + sample
174
179
  re = re + sample * sincos[h, k, 0]
175
180
  im = im + sample * sincos[h, k, 1]
176
- if dc != 0.0:
177
- re = re / dc
178
- im = im / dc
179
- dc = dc /samples
180
- else:
181
- dc = 0.0
182
- re = NAN if re == 0.0 else re * INFINITY
183
- im = NAN if im == 0.0 else im * INFINITY
181
+ if normalize:
182
+ if dc != 0.0:
183
+ # includes isnan(dc)
184
+ re = re / dc
185
+ im = im / dc
186
+ dc = dc / samples
187
+ else:
188
+ # dc = 0.0
189
+ re = NAN if re == 0.0 else re * INFINITY
190
+ im = NAN if im == 0.0 else im * INFINITY
184
191
  if h == 0:
185
192
  mean[i, j] = <float_t> dc
186
193
  real[h, i, j] = <float_t> re
@@ -201,14 +208,16 @@ def _phasor_from_signal(
201
208
  dc += sample
202
209
  re += sample * sincos[h, k, 0]
203
210
  im += sample * sincos[h, k, 1]
204
- if dc != 0.0:
205
- re /= dc
206
- im /= dc
207
- dc /= samples
208
- else:
209
- dc = 0.0
210
- re = NAN if re == 0.0 else re * INFINITY
211
- im = NAN if im == 0.0 else im * INFINITY
211
+ if normalize:
212
+ if dc != 0.0:
213
+ # includes isnan(dc)
214
+ re /= dc
215
+ im /= dc
216
+ dc = dc / samples
217
+ else:
218
+ # dc = 0.0
219
+ re = NAN if re == 0.0 else re * INFINITY
220
+ im = NAN if im == 0.0 else im * INFINITY
212
221
  if h == 0:
213
222
  mean[i, j] = <float_t> dc
214
223
  real[h, i, j] = <float_t> re
@@ -924,32 +933,41 @@ cdef (float_t, float_t) _phasor_at_harmonic(
924
933
 
925
934
  @cython.ufunc
926
935
  cdef (float_t, float_t) _phasor_multiply(
927
- float_t real1,
928
- float_t imag1,
936
+ float_t real,
937
+ float_t imag,
929
938
  float_t real2,
930
939
  float_t imag2,
931
940
  ) noexcept nogil:
932
- """Return multiplication of two phasors."""
933
- return real1 * real2 - imag1 * imag2, real1 * imag2 + imag1 * real2
941
+ """Return complex multiplication of two phasors."""
942
+ return (
943
+ real * real2 - imag * imag2,
944
+ real * imag2 + imag * real2
945
+ )
934
946
 
935
947
 
936
948
  @cython.ufunc
937
949
  cdef (float_t, float_t) _phasor_divide(
938
- float_t real1,
939
- float_t imag1,
950
+ float_t real,
951
+ float_t imag,
940
952
  float_t real2,
941
953
  float_t imag2,
942
954
  ) noexcept nogil:
943
- """Return division of two phasors."""
955
+ """Return complex division of two phasors."""
944
956
  cdef:
945
- float_t denom = real2 * real2 + imag2 * imag2
957
+ float_t divisor = real2 * real2 + imag2 * imag2
946
958
 
947
- if isnan(denom) or denom == 0.0:
948
- return <float_t> NAN, <float_t> NAN
959
+ if divisor != 0.0:
960
+ # includes isnan(divisor)
961
+ return (
962
+ (real * real2 + imag * imag2) / divisor,
963
+ (imag * real2 - real * imag2) / divisor
964
+ )
949
965
 
966
+ real = real * real2 + imag * imag2
967
+ imag = imag * real2 - real * imag2
950
968
  return (
951
- (real1 * real2 + imag1 * imag2) / denom,
952
- (imag1 * real2 - real1 * imag2) / denom
969
+ NAN if real == 0.0 else real * INFINITY,
970
+ NAN if imag == 0.0 else imag * INFINITY
953
971
  )
954
972
 
955
973
 
phasorpy/io.py CHANGED
@@ -16,6 +16,7 @@ The ``phasorpy.io`` module provides functions to:
16
16
  - read time-resolved and hyperspectral image data and metadata (as relevant
17
17
  to phasor analysis) from many file formats used in bio-imaging:
18
18
 
19
+ - :py:func:`read_imspector_tiff` - ImSpector FLIM TIFF
19
20
  - :py:func:`read_lsm` - Zeiss LSM
20
21
  - :py:func:`read_ifli` - ISS IFLI
21
22
  - :py:func:`read_sdt` - Becker & Hickl SDT
@@ -123,6 +124,7 @@ __all__ = [
123
124
  'read_fbd',
124
125
  'read_flif',
125
126
  'read_ifli',
127
+ 'read_imspector_tiff',
126
128
  # 'read_lif',
127
129
  'read_lsm',
128
130
  # 'read_nd2',
@@ -887,6 +889,146 @@ def read_lsm(
887
889
  return DataArray(data, **metadata)
888
890
 
889
891
 
892
+ def read_imspector_tiff(
893
+ filename: str | PathLike[Any],
894
+ /,
895
+ ) -> DataArray:
896
+ """Return FLIM image stack and metadata from ImSpector TIFF file.
897
+
898
+ Parameters
899
+ ----------
900
+ filename : str or Path
901
+ Name of ImSpector FLIM TIFF file to read.
902
+
903
+ Returns
904
+ -------
905
+ xarray.DataArray
906
+ TCSPC image stack.
907
+ Usually, a 3-to-5-dimensional array of type ``uint16``.
908
+
909
+ - ``coords['H']``: times of histogram bins.
910
+ - ``attrs['frequency']``: repetition frequency in MHz.
911
+
912
+ Raises
913
+ ------
914
+ tifffile.TiffFileError
915
+ File is not a TIFF file.
916
+ ValueError
917
+ File is not an ImSpector FLIM TIFF file.
918
+
919
+ Examples
920
+ --------
921
+ >>> data = read_imspector_tiff(fetch('Embryo.tif'))
922
+ >>> data.values
923
+ array(...)
924
+ >>> data.dtype
925
+ dtype('uint16')
926
+ >>> data.shape
927
+ (56, 512, 512)
928
+ >>> data.dims
929
+ ('H', 'Y', 'X')
930
+ >>> data.coords['H'].data # dtime bins
931
+ array(...)
932
+ >>> data.attrs['frequency'] # doctest: +NUMBER
933
+ 80.109
934
+
935
+ """
936
+ from xml.etree import ElementTree
937
+
938
+ import tifffile
939
+
940
+ with tifffile.TiffFile(filename) as tif:
941
+ tags = tif.pages.first.tags
942
+ omexml = tags.valueof(270, '')
943
+ make = tags.valueof(271, '')
944
+
945
+ if (
946
+ make != 'ImSpector'
947
+ or not omexml.startswith('<?xml version')
948
+ or len(tif.series) != 1
949
+ or not tif.is_ome
950
+ ):
951
+ raise ValueError(f'{tif.filename} is not an ImSpector TIFF file')
952
+
953
+ series = tif.series[0]
954
+ ndim = series.ndim
955
+ axes = series.axes
956
+ shape = series.shape
957
+
958
+ if ndim < 3 or not axes.endswith('YX'):
959
+ raise ValueError(
960
+ f'{tif.filename} is not an ImSpector FLIM TIFF file'
961
+ )
962
+
963
+ data = series.asarray()
964
+
965
+ attrs: dict[str, Any] = {}
966
+ coords = {}
967
+ physical_size = {}
968
+
969
+ root = ElementTree.fromstring(omexml)
970
+ ns = {
971
+ '': 'http://www.openmicroscopy.org/Schemas/OME/2008-02',
972
+ 'ca': 'http://www.openmicroscopy.org/Schemas/CA/2008-02',
973
+ }
974
+
975
+ description = root.find('.//Description', ns)
976
+ if (
977
+ description is not None
978
+ and description.text
979
+ and description.text != 'not_specified'
980
+ ):
981
+ attrs['description'] = description.text
982
+
983
+ pixels = root.find('.//Image/Pixels', ns)
984
+ assert pixels is not None
985
+ for ax in 'TZYX':
986
+ attrib = 'TimeIncrement' if ax == 'T' else f'PhysicalSize{ax}'
987
+ if ax not in axes or attrib not in pixels.attrib:
988
+ continue
989
+ size = float(pixels.attrib[attrib])
990
+ physical_size[ax] = size
991
+ coords[ax] = numpy.linspace(
992
+ 0.0,
993
+ size,
994
+ shape[axes.index(ax)],
995
+ endpoint=False,
996
+ dtype=numpy.float64,
997
+ )
998
+
999
+ axes_labels = root.find('.//ca:CustomAttributes/AxesLabels', ns)
1000
+ if (
1001
+ axes_labels is None
1002
+ or 'X' not in axes_labels.attrib
1003
+ or 'TCSPC' not in axes_labels.attrib['X']
1004
+ or 'FirstAxis' not in axes_labels.attrib
1005
+ or 'SecondAxis' not in axes_labels.attrib
1006
+ ):
1007
+ raise ValueError(f'{tif.filename} is not an ImSpector FLIM TIFF file')
1008
+
1009
+ if axes_labels.attrib['FirstAxis'].endswith('TCSPC T'):
1010
+ ax = axes[-3]
1011
+ assert axes_labels.attrib['FirstAxis-Unit'] == 'ns'
1012
+ elif axes_labels.attrib['SecondAxis'].endswith('TCSPC T') and ndim > 3:
1013
+ ax = axes[-4]
1014
+ assert axes_labels.attrib['SecondAxis-Unit'] == 'ns'
1015
+ else:
1016
+ raise ValueError(f'{tif.filename} is not an ImSpector FLIM TIFF file')
1017
+ axes = axes.replace(ax, 'H')
1018
+ coords['H'] = coords[ax]
1019
+ del coords[ax]
1020
+
1021
+ attrs['frequency'] = float(
1022
+ 1000.0 / (shape[axes.index('H')] * physical_size[ax])
1023
+ )
1024
+
1025
+ metadata = _metadata(axes, shape, filename, attrs=attrs, **coords)
1026
+
1027
+ from xarray import DataArray
1028
+
1029
+ return DataArray(data, **metadata)
1030
+
1031
+
890
1032
  def read_ifli(
891
1033
  filename: str | PathLike[Any],
892
1034
  /,
@@ -942,8 +1084,8 @@ def read_ifli(
942
1084
  (256, 256, 4, 3)
943
1085
  >>> data.dims
944
1086
  ('Y', 'X', 'F', 'S')
945
- >>> data.coords['F'].data
946
- array([8.033...])
1087
+ >>> data.coords['F'].data # doctest: +NUMBER
1088
+ array([8.033e+07, 1.607e+08, 2.41e+08, 4.017e+08])
947
1089
  >>> data.coords['S'].data
948
1090
  array(['mean', 'real', 'imag'], dtype='<U4')
949
1091
  >>> data.attrs
@@ -1035,8 +1177,8 @@ def read_sdt(
1035
1177
  ('Y', 'X', 'H')
1036
1178
  >>> data.coords['H'].data
1037
1179
  array(...)
1038
- >>> data.attrs['frequency']
1039
- 79...
1180
+ >>> data.attrs['frequency'] # doctest: +NUMBER
1181
+ 79.99
1040
1182
 
1041
1183
  """
1042
1184
  import sdtfile
@@ -1148,7 +1290,7 @@ def read_ptu(
1148
1290
  ('T', 'Y', 'X', 'C', 'H')
1149
1291
  >>> data.coords['H'].data
1150
1292
  array(...)
1151
- >>> data.attrs['frequency']
1293
+ >>> data.attrs['frequency'] # doctest: +NUMBER
1152
1294
  78.02
1153
1295
 
1154
1296
  """
@@ -1221,8 +1363,8 @@ def read_flif(
1221
1363
  ('H', 'Y', 'X')
1222
1364
  >>> data.coords['H'].data
1223
1365
  array(...)
1224
- >>> data.attrs['frequency']
1225
- 80.65...
1366
+ >>> data.attrs['frequency'] # doctest: +NUMBER
1367
+ 80.65
1226
1368
 
1227
1369
  """
1228
1370
  import lfdfiles