pax_global_header00006660000000000000000000000064151576136740014531gustar00rootroot0000000000000052 comment=d7beec469f9a16cd89318802e4591e4f04b752b8 cgohlke-ptufile-ab09357/000077500000000000000000000000001515761367400151475ustar00rootroot00000000000000cgohlke-ptufile-ab09357/.github/000077500000000000000000000000001515761367400165075ustar00rootroot00000000000000cgohlke-ptufile-ab09357/.github/workflows/000077500000000000000000000000001515761367400205445ustar00rootroot00000000000000cgohlke-ptufile-ab09357/.github/workflows/wheel.yml000066400000000000000000000031311515761367400223710ustar00rootroot00000000000000name: Build wheels on: workflow_dispatch: jobs: build: name: Build wheels on ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest, windows-latest] python-version: ["3.14"] steps: - uses: actions/checkout@v6 - uses: pypa/cibuildwheel@v3.4.0 env: # MACOSX_DEPLOYMENT_TARGET: "11.0" # CIBW_ENVIRONMENT: "PIP_PRE=1" CIBW_BUILD_VERBOSITY: 2 # build ABI3 wheels with Python 3.12, and free-threaded wheels with Python 3.14t CIBW_BUILD: "cp312-* cp314t-*" CIBW_SKIP: "*musllinux* *i686 *ppc64le *s390x" CIBW_ARCHS_LINUX: auto CIBW_ARCHS_MACOS: x86_64 arm64 CIBW_ARCHS_WINDOWS: AMD64 ARM64 x86 CIBW_TEST_REQUIRES: numpy CIBW_TEST_COMMAND: python -c "import ptufile;from ptufile import _ptufile;print(ptufile.__version__)" - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install packages run: | python -m pip install -U --find-links=wheelhouse ptufile abi3audit - name: Audit ABI3 wheels run: | abi3audit --strict --report wheelhouse/*abi3*.whl - name: Test ABI3 wheels run: | cd .. python -c "import ptufile;from ptufile import _ptufile;print(ptufile.__version__)" - uses: actions/upload-artifact@v7 with: path: ./wheelhouse/*.whl name: wheels-${{ matrix.os }} cgohlke-ptufile-ab09357/.gitignore000066400000000000000000000112201515761367400171330ustar00rootroot00000000000000_*.c *.wpr *.wpu .idea # Byte-compiled / optimized / DLL files __pycache__/ *.py[codz] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST setup.cfg PKG-INFO mypy.ini # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py.cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # UV # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. uv.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control poetry.lock poetry.toml # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. # https://pdm-project.org/en/latest/usage/project/#working-with-version-control pdm.lock .pdm.toml .pdm-python .pdm-build/ # pixi # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. #pixi.lock # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one # in the .venv directory. It is recommended not to include this directory in version control. .pixi # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .envrc .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # PyCharm # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ # Abstra # Abstra is an AI-powered process automation framework. # Ignore directories containing user credentials, local state, and settings. # Learn more at https://abstra.io/docs .abstra/ # Visual Studio Code # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore # and can be added to the global gitignore or merged into this file. However, if you prefer, # you could uncomment the following to ignore the entire vscode folder # .vscode/ # Ruff stuff: .ruff_cache/ # PyPI configuration file .pypirc # Cursor # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data # refer to https://docs.cursor.com/context/ignore-files .cursorignore .cursorindexingignore # Marimo marimo/_static/ marimo/_lsp/ __marimo__/ cgohlke-ptufile-ab09357/CHANGES.rst000066400000000000000000000065641515761367400167640ustar00rootroot00000000000000Revisions ========= 2026.3.21 - Add bounds checking to encode_t3_image function. - Use format-dispatch in hot decode loops to allow compiler inlining. - Build wheels on Windows with LLVM (30% faster decoding than MSVC). - Drop support for Python 3.11. 2026.2.6 - Fix code review issues. 2026.1.14 - Improve code quality. 2025.12.12 - Add PQUNI file type. - Add attrs properties and return with xarray DataSets. - Improve code quality. 2025.11.8 - Fix reading files with negative TTResult_NumberOfRecords. - Remove cache argument from PtuFile.read_records (breaking). - Add cache_records property to PtuFile to control caching behavior. - Derive PqFileError from ValueError. - Factor out BinaryFile base class. - Build ABI3 wheels. 2025.9.9 - Log error when decoding image with invalid line or frame masks. 2025.7.30 - Add option to specify pixel time for decoding images. - Add functions to read and write PicoQuant BIN files. - Drop support for Python 3.10. 2025.5.10 - Mark Cython extension free-threading compatible. - Support Python 3.14. 2025.2.20 - Rename PqFileMagic to PqFileType (breaking). - Rename PqFile.magic to PqFile.type (breaking). - Add PQDAT and SPQR file types. 2025.2.12 - Add options to specify file open modes to PqFile and PtuFile.read_records. - Add convenience properties to PqFile and PtuFile. - Cache records read from file. 2025.1.13 - Fall back to file size if TTResult_NumberOfRecords is zero (#2). 2024.12.28 - Add imwrite function to encode TCSPC image histogram in T3 PTU format. - Add enums for more PTU tag values. - Add PqFile.datetime property. - Read TDateTime tag as datetime instead of struct_time (breaking). - Rename PtuFile.type property to record_type (breaking). - Fix reading PHU missing HistResDscr_HWBaseResolution tag. - Warn if tags are not 8-byte aligned in file. 2024.12.20 - Support bi-directional sinusoidal scanning (WIP). 2024.11.26 - Support bi-directional scanning (FLIMbee scanner). - Drop support for Python 3.9. 2024.10.10 - Also trim leading channels without photons (breaking). - Add property to identify channels with photons. 2024.9.14 - Improve typing. 2024.7.13 - Detect point scans in image mode. - Deprecate Python 3.9, support Python 3.13. 2024.5.24 - Fix docstring examples not correctly rendered on GitHub. 2024.4.24 - Build wheels with NumPy 2. 2024.2.20 - Change definition of PtuFile.frequency (breaking). - Add option to specify number of bins returned by decode_histogram. - Add option to return histograms of one period. 2024.2.15 - Add PtuFile.scanner property. - Add numcodecs compatible PTU codec. 2024.2.8 - Support sinusoidal scanning correction. 2024.2.2 - Change positive dtime parameter from index to size (breaking). - Fix segfault with ImgHdr_TimePerPixel = 0. - Rename MultiHarp to Generic conforming with changes in PicoQuant reference. 2023.11.16 - Fix empty line when first record is start marker. 2023.11.13 - Change image histogram dimension order to TYXCH (breaking). - Change frame start to start of first line in frame (breaking). - Improve trimming of incomplete frames (breaking). - Remove trim_dtime option (breaking). - Fix selection handling in PtuFile.decode_image. - Add option to trim T, C, and H axes of image histograms. - Add option to decode histograms to memory-mapped or user-provided arrays. - Add ``__getitem__`` interface to image histogram. 2023.11.1 - Initial alpha release. cgohlke-ptufile-ab09357/LICENSE000066400000000000000000000027711515761367400161630ustar00rootroot00000000000000BSD-3-Clause license Copyright (c) 2023-2026, Christoph Gohlke All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. cgohlke-ptufile-ab09357/MANIFEST.in000066400000000000000000000012471515761367400167110ustar00rootroot00000000000000include LICENSE include README.rst include CHANGES.rst include pyproject.toml include ptufile/py.typed include ptufile/_ptufile.pyx include docs/conf.py include docs/index.rst include docs/_static/custom.css include .github/workflows/wheel.yml exclude .env exclude *.cmd exclude *.yaml exclude mypy.ini exclude ruff.toml exclude *.code-workspace exclude .gitignore recursive-exclude .vscode * recursive-exclude doc * recursive-exclude docs * recursive-exclude test * recursive-exclude tests * recursive-exclude * __pycache__ recursive-exclude * *.py[co] recursive-exclude * *Copy* recursive-include tests readme.txt include tests/conftest.py include tests/test_ptufile.py cgohlke-ptufile-ab09357/README.rst000066400000000000000000000203211515761367400166340ustar00rootroot00000000000000.. This file is generated by setup.py Read and write PicoQuant PTU and related files ============================================== Ptufile is a Python library to 1. read data and metadata from PicoQuant PTU and related files (PHU, PCK, PCO, PFS, PUS, PQRES, PQDAT, PQUNI, SPQR, and BIN), and 2. write TCSPC histograms to T3 image mode PTU files. PTU files contain time correlated single photon counting (TCSPC) measurement data and instrumentation parameters. :Author: `Christoph Gohlke `_ :License: BSD-3-Clause :Version: 2026.3.21 :DOI: `10.5281/zenodo.10120021 `_ Quickstart ---------- Install the ptufile package and all dependencies from the `Python Package Index `_:: python -m pip install -U "ptufile[all]" See `Examples`_ for using the programming interface. Source code and support are available on `GitHub `_. Requirements ------------ This revision was tested with the following requirements and dependencies (other versions may work): - `CPython `_ 3.12.10, 3.13.12, 3.14.3 64-bit - `NumPy `_ 2.4.3 - `Xarray `_ 2026.2.0 (recommended) - `Matplotlib `_ 3.10.8 (optional) - `Tifffile `_ 2026.3.3 (optional) - `Numcodecs `_ 0.16.5 (optional) - `Python-dateutil `_ 2.9.0 (optional) - `Cython `_ 3.2.4 (build) Revisions --------- 2026.3.21 - Add bounds checking to encode_t3_image function. - Use format-dispatch in hot decode loops to allow compiler inlining. - Build wheels on Windows with LLVM (30% faster decoding than MSVC). - Drop support for Python 3.11. 2026.2.6 - Fix code review issues. 2026.1.14 - Improve code quality. 2025.12.12 - Add PQUNI file type. - Add attrs properties and return with xarray DataSets. - Improve code quality. 2025.11.8 - Fix reading files with negative TTResult_NumberOfRecords. - Remove cache argument from PtuFile.read_records (breaking). - Add cache_records property to PtuFile to control caching behavior. - Derive PqFileError from ValueError. - Factor out BinaryFile base class. - Build ABI3 wheels. 2025.9.9 - Log error when decoding image with invalid line or frame masks. 2025.7.30 - Add option to specify pixel time for decoding images. - Add functions to read and write PicoQuant BIN files. - Drop support for Python 3.10. 2025.5.10 - Mark Cython extension free-threading compatible. - Support Python 3.14. 2025.2.20 - … Refer to the CHANGES file for older revisions. Notes ----- `PicoQuant GmbH `_ is a manufacturer of photonic components and instruments. The PicoQuant unified file formats are documented at the `PicoQuant-Time-Tagged-File-Format-Demos `_. The following features are currently not implemented due to the lack of test files or documentation: PT2 and PT3 files, decoding images from T2 and SPQR formats, bidirectional per frame, and deprecated image reconstruction. Compatibility with PTU files written by non-PicoQuant software (for example, Leica LAS X or Abberior Imspector) is limited, as is decoding line, bidirectional, and sinusoidal scanning. Other modules for reading or writing PicoQuant files are `Read_PTU.py `_, `readPTU `_, `readPTU_FLIM `_, `fastFLIM `_, `PyPTU `_, `PTU_Reader `_, `PTU_Writer `_, `FlimReader `_, `tangy `_, `tttrlib `_, `picoquantio `_, `ptuparser `_, `phconvert `_, `trattoria `_ (wrapper of `trattoria-core `_, `tttr-toolbox `_), `PAM `_, `FLOPA `_, and `napari-flim-phasor-plotter `_. Examples -------- Read properties and tags from any type of PicoQuant unified tagged file: .. code-block:: python >>> pq = PqFile('tests/data/Settings.pfs') >>> pq.type >>> pq.guid UUID('86d428e2-cb0b-4964-996c-04456ba6be7b') >>> pq.tags {...'CreatorSW_Name': 'SymPhoTime 64', 'CreatorSW_Version': '2.1'...} >>> pq.close() Read metadata from a PicoQuant PTU FLIM file: .. code-block:: python >>> ptu = PtuFile('tests/data/FLIM.ptu') >>> ptu.type >>> ptu.record_type >>> ptu.measurement_mode >>> ptu.measurement_submode Decode TTTR records from the PTU file to ``numpy.recarray``: .. code-block:: python >>> decoded = ptu.decode_records() >>> decoded.dtype dtype([('time', '>> decoded['time'][(decoded['marker'] & ptu.frame_change_mask) > 0] array([1571185680], dtype=uint64) Decode TTTR records to overall delay-time histograms per channel: .. code-block:: python >>> ptu.decode_histogram(dtype='uint8') array([[ 5, 7, 7, ..., 10, 9, 2]], shape=(2, 3126), dtype=uint8) Get information about the FLIM image histogram in the PTU file: .. code-block:: python >>> ptu.shape (1, 256, 256, 2, 3126) >>> ptu.dims ('T', 'Y', 'X', 'C', 'H') >>> ptu.coords {'T': ..., 'Y': ..., 'X': ..., 'H': ...} >>> ptu.dtype dtype('uint16') >>> ptu.active_channels (0, 1) Decode parts of the image histogram to ``numpy.ndarray`` using slice notation. Slice step sizes define binning, -1 being used to integrate along axis: .. code-block:: python >>> ptu[:, ..., 0, ::-1] array([[[103, ..., 38], ... [ 47, ..., 30]]], shape=(1, 256, 256), dtype=uint16) Alternatively, decode the first channel and integrate all histogram bins into a ``xarray.DataArray``, keeping reduced axes: .. code-block:: python >>> ptu.decode_image(channel=0, dtime=-1, asxarray=True) ... array([[[[[103]], ... [[ 30]]]]], shape=(1, 256, 256, 1, 1), dtype=uint16) Coordinates: * T (T) float64... 0.05625 * Y (Y) float64... -0.0001304 ... 0.0001294 * X (X) float64... -0.0001304 ... 0.0001294 * C (C) uint8... 0 * H (H) float64... 0.0 Attributes... name: FLIM.ptu ... Write the TCSPC histogram and metadata to a PicoHarpT3 image mode PTU file: .. code-block:: python >>> imwrite( ... '_test.ptu', ... ptu[:], ... ptu.global_resolution, ... ptu.tcspc_resolution, ... # optional metadata ... pixel_time=ptu.pixel_time, ... record_type=PtuRecordType.PicoHarpT3, ... comment='Written by ptufile.py', ... tags={'File_RawData_GUID': [ptu.guid]}, ... ) Read back the TCSPC histogram from the file: .. code-block:: python >>> tcspc_histogram = imread('_test.ptu') >>> import numpy >>> numpy.array_equal(tcspc_histogram, ptu[:]) True Close the file handle: .. code-block:: python >>> ptu.close() Preview the image and metadata in a PTU file from the console:: python -m ptufile tests/data/FLIM.ptucgohlke-ptufile-ab09357/ptufile/000077500000000000000000000000001515761367400166175ustar00rootroot00000000000000cgohlke-ptufile-ab09357/ptufile/__init__.py000066400000000000000000000005361515761367400207340ustar00rootroot00000000000000# ptufile/__init__.py from .ptufile import * from .ptufile import __all__, __doc__, __version__ # constants are repeated for documentation __version__ = __version__ """Ptufile version string.""" T2_RECORD_DTYPE = T2_RECORD_DTYPE """Numpy dtype of decoded T2 records.""" T3_RECORD_DTYPE = T3_RECORD_DTYPE """Numpy dtype of decoded T3 records.""" cgohlke-ptufile-ab09357/ptufile/__main__.py000066400000000000000000000001731515761367400207120ustar00rootroot00000000000000# ptufile/__main__.py """ptufile package command line script.""" import sys from .ptufile import main sys.exit(main()) cgohlke-ptufile-ab09357/ptufile/_ptufile.pyx000066400000000000000000001577761515761367400212170ustar00rootroot00000000000000# _ptufile.pyx # distutils: language = c # cython: boundscheck = False # cython: wraparound = False # cython: cdivision = True # cython: nonecheck = False # cython: freethreading_compatible = True # Copyright (c) 2023-2026, Christoph Gohlke # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # 3. Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. """Decode PicoQuant Time-Tagged Time-Resolved (TTTR) records.""" from libc.math cimport round from libc.stdint cimport ( UINT64_MAX, int8_t, int16_t, uint8_t, uint16_t, uint32_t, uint64_t, ) cdef packed struct t2_t: uint64_t time int8_t channel uint8_t marker # uint8_t[6] _align cdef packed struct t3_t: uint64_t time int16_t dtime int8_t channel uint8_t marker # uint8_t[4] _align ctypedef uint32_t (*encode_func_t)( const uint32_t time, const uint32_t dtime, const uint32_t channel, const uint32_t overflow, const uint32_t marker ) noexcept nogil ctypedef void (*decode_func_t)( const uint32_t record, uint32_t* time, uint32_t* dtime, uint32_t* channel, uint64_t* overflow, uint8_t* marker, uint8_t* special ) noexcept nogil ctypedef fused uint_t: uint8_t uint16_t uint32_t uint64_t cdef int init_format( const uint32_t format, decode_func_t* decode, ssize_t* bins, # number_bins_max ssize_t* channels, # number_channels_max ) noexcept nogil: if format == 0x00010303: # PicoHarpT3/PicoHarp300 decode[0] = decode_pt3 bins[0] = 4096 channels[0] = 4 elif format == 0x00010203: # PicoHarpT2/PicoHarp300 decode[0] = decode_pt2 bins[0] = 0 channels[0] = 5 # ? elif format in { 0x01010204, # HydraHarp2T2 0x00010205, # TimeHarp260NT2 0x00010206, # TimeHarp260PT2 0x00010207, # GenericT2 (MultiHarpT2 and Picoharp330T2) }: decode[0] = decode_ht2 bins[0] = 0 channels[0] = 64 elif format == 0x00010204: # HydraHarpT2 decode[0] = decode_ht2v1 bins[0] = 0 channels[0] = 64 elif format in { 0x01010304, # HydraHarp2T3 0x00010305, # TimeHarp260NT3 0x00010306, # TimeHarp260PT3 0x00010307, # GenericT3 (MultiHarpT3 and Picoharp330T3) }: decode[0] = decode_ht3 bins[0] = 32768 channels[0] = 64 elif format == 0x00010304: # HydraHarpT3 decode[0] = decode_ht3v1 bins[0] = 32768 channels[0] = 64 else: return 1 return 0 def decode_info( const uint32_t[::1] records, const uint32_t format, const uint32_t line_start, const uint32_t line_stop, const uint32_t frame_change, const ssize_t lines_in_frame, # may be zero to disable frame trimming ): """Return information about PicoQuant TTTR records.""" cdef: ssize_t nrecords = records.size ssize_t i, maxchannels, maxbins ssize_t channels_active_first, channels_active_last ssize_t y = 0 ssize_t skip_first_frame = 0 ssize_t skip_last_frame = 0 uint64_t nbins = 0 uint64_t nframes = 0 uint64_t nphotons = 0 uint64_t nmarkers = 0 uint64_t nlines = 0 uint64_t overflow = 0 uint64_t time_line_start = UINT64_MAX uint64_t time_in_lines = 0 uint64_t channels_active = 0 uint32_t itime = 0 uint32_t idtime, ichannel uint8_t ispecial, imarker with nogil: # dispatch on format so compiler can inline decoder and optimize loop if format == 0x00010303: # PicoHarpT3/PicoHarp300 maxbins = 4096 maxchannels = 4 for i in range(nrecords): decode_pt3( records[i], &itime, &idtime, &ichannel, &overflow, &imarker, &ispecial, ) decode_info_record( itime, idtime, ichannel, overflow, imarker, ispecial, line_start, line_stop, frame_change, lines_in_frame, maxchannels, maxbins, &nphotons, &nbins, &nmarkers, &nlines, &nframes, &channels_active, &time_in_lines, &time_line_start, &y, &skip_first_frame, &skip_last_frame, ) elif format == 0x00010203: # PicoHarpT2/PicoHarp300 maxbins = 0 maxchannels = 5 # ? for i in range(nrecords): decode_pt2( records[i], &itime, &idtime, &ichannel, &overflow, &imarker, &ispecial, ) decode_info_record( itime, idtime, ichannel, overflow, imarker, ispecial, line_start, line_stop, frame_change, lines_in_frame, maxchannels, maxbins, &nphotons, &nbins, &nmarkers, &nlines, &nframes, &channels_active, &time_in_lines, &time_line_start, &y, &skip_first_frame, &skip_last_frame, ) elif format in { 0x01010204, # HydraHarp2T2 0x00010205, # TimeHarp260NT2 0x00010206, # TimeHarp260PT2 0x00010207, # GenericT2 (MultiHarpT2 and Picoharp330T2) }: maxbins = 0 maxchannels = 64 for i in range(nrecords): decode_ht2( records[i], &itime, &idtime, &ichannel, &overflow, &imarker, &ispecial ) decode_info_record( itime, idtime, ichannel, overflow, imarker, ispecial, line_start, line_stop, frame_change, lines_in_frame, maxchannels, maxbins, &nphotons, &nbins, &nmarkers, &nlines, &nframes, &channels_active, &time_in_lines, &time_line_start, &y, &skip_first_frame, &skip_last_frame, ) elif format == 0x00010204: # HydraHarpT2 maxbins = 0 maxchannels = 64 for i in range(nrecords): decode_ht2v1( records[i], &itime, &idtime, &ichannel, &overflow, &imarker, &ispecial, ) decode_info_record( itime, idtime, ichannel, overflow, imarker, ispecial, line_start, line_stop, frame_change, lines_in_frame, maxchannels, maxbins, &nphotons, &nbins, &nmarkers, &nlines, &nframes, &channels_active, &time_in_lines, &time_line_start, &y, &skip_first_frame, &skip_last_frame, ) elif format in { 0x01010304, # HydraHarp2T3 0x00010305, # TimeHarp260NT3 0x00010306, # TimeHarp260PT3 0x00010307, # GenericT3 (MultiHarpT3 and Picoharp330T3) }: maxbins = 32768 maxchannels = 64 for i in range(nrecords): decode_ht3( records[i], &itime, &idtime, &ichannel, &overflow, &imarker, &ispecial, ) decode_info_record( itime, idtime, ichannel, overflow, imarker, ispecial, line_start, line_stop, frame_change, lines_in_frame, maxchannels, maxbins, &nphotons, &nbins, &nmarkers, &nlines, &nframes, &channels_active, &time_in_lines, &time_line_start, &y, &skip_first_frame, &skip_last_frame, ) elif format == 0x00010304: # HydraHarpT3 maxbins = 32768 maxchannels = 64 for i in range(nrecords): decode_ht3v1( records[i], &itime, &idtime, &ichannel, &overflow, &imarker, &ispecial, ) decode_info_record( itime, idtime, ichannel, overflow, imarker, ispecial, line_start, line_stop, frame_change, lines_in_frame, maxchannels, maxbins, &nphotons, &nbins, &nmarkers, &nlines, &nframes, &channels_active, &time_in_lines, &time_line_start, &y, &skip_first_frame, &skip_last_frame, ) else: with gil: msg = f'no decoder available for {format=:02x}' raise ValueError(msg) channels_active_first = 64 channels_active_last = 0 for i in range(64): if channels_active & (( 1) << i): if i < channels_active_first: channels_active_first = i channels_active_last = i if channels_active_first == 64: channels_active_first = 0 channels_active_last = 0 nbins = 0 if maxbins == 0 else nbins + 1 if nframes > 1 and y > 0 and lines_in_frame > y + 1: skip_last_frame = 1 if nframes == 0: skip_first_frame = 0 skip_last_frame = 0 # elif nframes == 1: # # leave incomplete single frame # skip_first_frame = 0 # skip_last_frame = 0 # elif nframes == 2: # if skip_first_frame and skip_last_frame: # # leave both incomplete frames # skip_first_frame = 0 # skip_last_frame = 0 # elif skip_first_frame or skip_last_frame: # # remove incomplete first xor last frame # nframes -= 1 else: # remove incomplete first and/or last frames if skip_first_frame: nframes -= 1 if skip_last_frame and nframes > 0: nframes -= 1 if nlines > 0: time_in_lines = ( round( time_in_lines / nlines) ) else: time_in_lines = 0 return ( format, nrecords, nphotons, nmarkers, nframes, nlines, maxchannels, channels_active, channels_active_first, channels_active_last, maxbins, nbins, skip_first_frame, skip_last_frame, time_in_lines, overflow + itime ) cdef inline void decode_info_record( const uint32_t itime, const uint32_t idtime, const uint32_t ichannel, const uint64_t overflow, const uint8_t imarker, const uint8_t ispecial, const uint32_t line_start, const uint32_t line_stop, const uint32_t frame_change, const ssize_t lines_in_frame, const ssize_t maxchannels, const ssize_t maxbins, uint64_t* nphotons, uint64_t* nbins, uint64_t* nmarkers, uint64_t* nlines, uint64_t* nframes, uint64_t* channels_active, uint64_t* time_in_lines, uint64_t* time_line_start, ssize_t* y, ssize_t* skip_first_frame, ssize_t* skip_last_frame, ) noexcept nogil: """Accumulate one decoded TTTR record into decode_info statistics.""" if ispecial == 0: # photon record nphotons[0] += 1 if ichannel < maxchannels: channels_active[0] |= ( 1) << ichannel if idtime > nbins[0] and idtime < maxbins: nbins[0] = idtime elif ispecial == 2: # marker record nmarkers[0] += 1 if imarker & frame_change: # frame marker if lines_in_frame > y[0] + 1: # not enough lines in frame if nframes[0] == 1: skip_first_frame[0] = 1 else: skip_last_frame[0] = 1 else: skip_last_frame[0] = 0 if not imarker & line_stop: time_line_start[0] = UINT64_MAX y[0] = 0 if imarker & line_stop: # line stop marker if ( time_line_start[0] != UINT64_MAX and (lines_in_frame == 0 or lines_in_frame >= y[0]) ): time_in_lines[0] += (overflow + itime) - time_line_start[0] time_line_start[0] = UINT64_MAX if imarker & line_start: # line start marker # TODO: add to time_in_lines if previous line did not stop? time_line_start[0] = overflow + itime if y[0] == 0: # new frame starts at first line # after start or frame change marker nframes[0] += 1 y[0] += 1 if lines_in_frame == 0 or lines_in_frame >= y[0]: nlines[0] += 1 def decode_t3_point( uint_t[:, :, ::1] histogram, uint64_t[::1] times, const uint32_t[::1] records, const uint32_t format, const uint64_t pixel_time, const ssize_t startt = 0, const ssize_t startc = 0, const ssize_t starth = 0, const ssize_t bint = 1, const ssize_t binc = 1, const ssize_t binh = 1, ): """Return TCSPC histogram from TTTR T3 records of point measurement.""" cdef: ssize_t sizec, sizet, sizeh, stopc, stopt, stoph ssize_t nrecords = records.size uint64_t overflow, time_global uint32_t itime, idtime, ichannel uint8_t ispecial, imarker ssize_t i, iframe, iframe_binned, maxbins_ decode_func_t decode_func if pixel_time == 0: raise ValueError(f'invalid {pixel_time=}') if init_format(format, &decode_func, &maxbins_, &i) != 0: raise ValueError(f'no decoder available for {format=:02x}') if maxbins_ == 0: raise NotImplementedError(f'not a T3 {format=:02x}') if startc < 0 or startt < 0 or starth < 0: raise ValueError(f'invalid {startt=}, {startc=}, or {starth=}') if binc < 1 or bint < 1 or binh < 1: raise ValueError(f'invalid {bint=}, {binc=}, or {binh=}') if times.size != histogram.shape[0]: raise ValueError(f'{times.size=} does not match {histogram.shape=}') sizet, sizec, sizeh = histogram.shape[:3] with nogil: stopt = startt + sizet * bint stopc = startc + sizec * binc stoph = starth + sizeh * binh overflow = 0 iframe = 0 iframe_binned = -1 for i in range(nrecords): decode_func( records[i], &itime, &idtime, &ichannel, &overflow, &imarker, &ispecial ) if ispecial == 0: # regular record if ( ichannel < startc or ichannel >= stopc or idtime < starth or idtime >= stoph ): continue time_global = overflow + itime iframe = time_global // pixel_time if iframe < startt: continue if iframe >= stopt: break if iframe_binned != (iframe - startt) // bint: iframe_binned = (iframe - startt) // bint times[iframe_binned] = time_global histogram[ iframe_binned, (ichannel - startc) // binc, (idtime - starth) // binh, ] += 1 # elif ispecial == 1: # # overflow # elif ispecial == 2: # # no markers def decode_t3_line( uint_t[:, :, :, ::1] histogram, uint64_t[::1] times, const uint32_t[::1] records, const uint32_t format, const uint64_t pixel_time, const uint32_t line_start, const uint32_t line_stop, const ssize_t startt = 0, const ssize_t startx = 0, const ssize_t startc = 0, const ssize_t starth = 0, const ssize_t bint = 1, const ssize_t binx = 1, const ssize_t binc = 1, const ssize_t binh = 1, ): """Return TCSPC histogram from TTTR T3 records of line scan measurement.""" cdef: ssize_t sizec, sizet, sizex, sizeh ssize_t stopc, stopt, stopx, stoph ssize_t nrecords = records.size uint64_t overflow, time_global, time_line_start uint32_t itime, idtime, ichannel uint8_t ispecial, imarker ssize_t i, ix, iframe, iframe_binned, maxbins_ decode_func_t decode_func if pixel_time == 0: raise ValueError(f'invalid {pixel_time=}') if init_format(format, &decode_func, &maxbins_, &i) != 0: raise ValueError(f'no decoder available for {format=:02x}') if maxbins_ == 0: raise NotImplementedError(f'not a T3 {format=:02x}') if startc < 0 or startt < 0 or startx < 0 or starth < 0: raise ValueError( f'invalid {startt=}, {startx=}, {startc=}, or {starth=}' ) if binc < 1 or bint < 1 or binx < 1 or binh < 1: raise ValueError(f'invalid {bint=}, {binx=}, {binc=}, or {binh=}') if times.size != histogram.shape[0]: raise ValueError(f'{times.size=} does not match {histogram.shape=}') sizet, sizex, sizec, sizeh = histogram.shape[:4] with nogil: stopt = startt + sizet * bint stopx = startx + sizex * binx stopc = startc + sizec * binc stoph = starth + sizeh * binh time_line_start = UINT64_MAX overflow = 0 iframe = 0 iframe_binned = -1 ix = 0 for i in range(nrecords): decode_func( records[i], &itime, &idtime, &ichannel, &overflow, &imarker, &ispecial ) time_global = overflow + itime if ispecial == 0: # regular record if ( time_line_start == UINT64_MAX # no line start marker yet or ichannel < startc or ichannel >= stopc or idtime < starth or idtime >= stoph ): continue ix = (time_global - time_line_start) // pixel_time if ix < startx or ix >= stopx: continue histogram[ iframe_binned, (ix - startx) // binx, (ichannel - startc) // binc, (idtime - starth) // binh, ] += 1 # elif ispecial == 1: # # overflow elif ispecial == 2: # marker if imarker & line_stop: time_line_start = UINT64_MAX iframe += 1 if iframe == stopt: break if imarker & line_start and iframe >= startt: time_line_start = time_global if iframe_binned != (iframe - startt) // bint: iframe_binned = (iframe - startt) // bint times[iframe_binned] = time_line_start def decode_t3_image( uint_t[:, :, :, :, ::1] histogram, uint64_t[::1] times, const uint32_t[::1] records, const uint32_t format, const ssize_t pixels_in_line, ssize_t pixel_time, # average global time spent in pixel ssize_t line_time, # global time spent in one line const uint16_t[::1] pixel_at_time, # global time in line to pixel index const uint32_t line_start, # mask const uint32_t line_stop, # mask const uint32_t frame_change, # mask const ssize_t startt = 0, const ssize_t starty = 0, const ssize_t startx = 0, const ssize_t startc = 0, const ssize_t starth = 0, const ssize_t bint = 1, const ssize_t biny = 1, const ssize_t binx = 1, const ssize_t binc = 1, const ssize_t binh = 1, const ssize_t bishift = 0, const bint bidirectional = False, const bint sinusoidal = False, const bint skip_first_frame = 0, ): """Return TCSPC histogram from TTTR T3 records of image measurement.""" cdef: ssize_t sizec, sizet, sizey, sizex, sizeh ssize_t stopc, stopt, stopy, stopx, stoph ssize_t nrecords = records.size ssize_t i, j, ix, iy, iy_binned, iframe, iframe_binned, maxbins_, bidiv uint64_t overflow, overflowj, time_global, time_line_start uint32_t itime, idtime, ichannel uint8_t ispecial, imarker decode_func_t decode_func bint scanline = pixel_time <= 0 or line_time <= 0 if scanline and pixels_in_line <= 0: raise ValueError(f'invalid {pixels_in_line=}') if ( sinusoidal and (pixel_at_time is None or pixel_at_time.size != line_time) ): sizey = 0 if pixel_at_time is None else pixel_at_time.size raise ValueError( f'invalid pixel_at_time.size={sizey} != {line_time=}' ) if init_format(format, &decode_func, &maxbins_, &i) != 0: raise ValueError(f'no decoder available for {format=:02x}') if maxbins_ == 0: raise NotImplementedError(f'not a T3 {format=:02x}') if startc < 0 or startt < 0 or starty < 0 or startx < 0 or starth < 0: raise ValueError( f'invalid {startt=}, {starty=}, {startx=}, {startc=}, or {starth=}' ) if binc < 1 or bint < 1 or biny < 1 or binx < 1 or binh < 1: raise ValueError( f'invalid {bint=}, {biny=}, {binx=}, {binc=}, or {binh=}' ) if times.size != histogram.shape[0]: raise ValueError(f'{times.size=} does not match {histogram.shape=}') # if wraparound and starth > 0: # raise ValueError(f'can not wrap dtime with {starth=}') sizet, sizey, sizex, sizec, sizeh = histogram.shape[:5] with nogil: stopt = startt + sizet * bint stopy = starty + sizey * biny stopx = startx + sizex * binx stopc = startc + sizec * binc stoph = starth + sizeh * binh bidiv = starty % 2 time_line_start = UINT64_MAX overflow = 0 iframe = -2 if skip_first_frame else -1 iframe_binned = -1 iy_binned = -1 iy = -1 ix = 0 # TODO: process channels/frames in parallel? for i in range(nrecords): ispecial = 3 # invalid, should always be overridden decode_func( records[i], &itime, &idtime, &ichannel, &overflow, &imarker, &ispecial ) time_global = overflow + itime if ispecial == 0: # regular record if ( time_line_start == UINT64_MAX # no line start marker yet or ichannel < startc or ichannel >= stopc or idtime < starth or idtime >= stoph # or (not wraparound and idtime >= stoph) ): continue ix = (time_global - time_line_start) if bidirectional and iy_binned % 2 != bidiv: # line backward scan # TODO: support bidirectional per frame # TODO: is bishift correct for sinusoidal scanning? ix = line_time - 1 - ix + bishift if ix < 0 or ix >= line_time: ix = stopx elif sinusoidal: ix = pixel_at_time[ix] else: ix = ix // pixel_time if ix >= startx and ix < stopx: # idtime_binned = (idtime - starth) // binh # if wraparound and idtime_binned >= sizeh: # idtime_binned %= sizeh histogram[ iframe_binned, iy_binned, (ix - startx) // binx, (ichannel - startc) // binc, (idtime - starth) // binh, ] += 1 # elif ispecial == 1: # # overflow elif ispecial == 2: # marker if imarker & frame_change: time_line_start = UINT64_MAX iy = -1 if imarker & line_stop: time_line_start = UINT64_MAX if imarker & line_start: iy += 1 if iy == 0: # new frame starts at first line # after start or frame change marker iframe += 1 if iframe == stopt: break if iframe >= startt and iy >= starty and iy < stopy: iy_binned = (iy - starty) // biny time_line_start = time_global if iframe_binned != (iframe - startt) // bint: iframe_binned = (iframe - startt) // bint times[iframe_binned] = time_line_start if scanline: # scan line to next marker to determine # line_time and pixel_time overflowj = overflow j = i + 1 while True: if j >= nrecords: break decode_func( records[j], &itime, &idtime, &ichannel, &overflowj, &imarker, &ispecial ) time_global = overflowj + itime if ispecial == 2: # any marker break j += 1 line_time = ( time_global - time_line_start ) # pixel_time = round( # line_time / pixels_in_line # ) pixel_time = line_time // pixels_in_line pixel_time = pixel_time if pixel_time > 0 else 1 else: # line is not part of selection time_line_start = UINT64_MAX elif ispecial != 1: with gil: raise ValueError(f'invalid records[{i}]={records[i]}') def decode_t3_histogram( uint_t[:, ::1] histogram, const uint32_t[::1] records, const uint32_t format, const ssize_t startc, ): """Decode PicoQuant T3 TTTR records to histogram per channel.""" cdef: ssize_t nrecords = records.size ssize_t i, nbins, nchannels uint64_t overflow uint32_t itime, idtime, ichannel uint8_t ispecial, imarker if startc < 0: raise ValueError(f'{startc=} < 0') nchannels, nbins = histogram.shape[:2] with nogil: overflow = 0 # dispatch on format so compiler can inline decoder and optimize loop if format == 0x00010303: # PicoHarpT3 for i in range(nrecords): decode_pt3( records[i], &itime, &idtime, &ichannel, &overflow, &imarker, &ispecial ) ichannel -= startc # may underflow if ispecial == 0 and ichannel < nchannels and idtime < nbins: histogram[ichannel, idtime] += 1 elif format in { 0x01010304, # HydraHarp2T3 0x00010305, # TimeHarp260NT3 0x00010306, # TimeHarp260PT3 0x00010307, # GenericT3 (MultiHarpT3 and Picoharp330T3) }: for i in range(nrecords): decode_ht3( records[i], &itime, &idtime, &ichannel, &overflow, &imarker, &ispecial ) ichannel -= startc # may underflow if ispecial == 0 and ichannel < nchannels and idtime < nbins: histogram[ichannel, idtime] += 1 elif format == 0x00010304: # HydraHarpT3 for i in range(nrecords): decode_ht3v1( records[i], &itime, &idtime, &ichannel, &overflow, &imarker, &ispecial ) ichannel -= startc # may underflow if ispecial == 0 and ichannel < nchannels and idtime < nbins: histogram[ichannel, idtime] += 1 # if wraparound: # histogram[ichannel, idtime % nbins] += 1 else: with gil: raise ValueError(f'no decoder available for {format=:02x}') def decode_t2_histogram( uint_t[:, ::1] histogram, const uint32_t[::1] records, const uint32_t format, const uint64_t bin_time, const ssize_t startc, ): """Decode PicoQuant T2 TTTR records to histogram per channel.""" cdef: ssize_t nrecords = records.size ssize_t i, ibin, nbins, nchannels uint64_t overflow uint32_t itime, idtime, ichannel uint8_t ispecial, imarker if bin_time == 0: raise ValueError(f'invalid {bin_time=}') if startc < 0: raise ValueError(f'{startc=} < 0') nchannels, nbins = histogram.shape[:2] with nogil: overflow = 0 # dispatch on format so compiler can inline decoder and optimize loop if format == 0x00010203: # PicoHarpT2 for i in range(nrecords): decode_pt2( records[i], &itime, &idtime, &ichannel, &overflow, &imarker, &ispecial ) ibin = ((overflow + itime) // bin_time) if ibin >= nbins: break ichannel -= startc # may underflow if ispecial == 0 and ichannel < nchannels: histogram[ichannel, ibin] += 1 elif format in { 0x01010204, # HydraHarp2T2 0x00010205, # TimeHarp260NT2 0x00010206, # TimeHarp260PT2 0x00010207, # GenericT2 (MultiHarpT2 and Picoharp330T2) }: for i in range(nrecords): decode_ht2( records[i], &itime, &idtime, &ichannel, &overflow, &imarker, &ispecial ) ibin = ((overflow + itime) // bin_time) if ibin >= nbins: break ichannel -= startc # may underflow if ispecial == 0 and ichannel < nchannels: histogram[ichannel, ibin] += 1 elif format == 0x00010204: # HydraHarpT2 for i in range(nrecords): decode_ht2v1( records[i], &itime, &idtime, &ichannel, &overflow, &imarker, &ispecial ) ibin = ((overflow + itime) // bin_time) if ibin >= nbins: break ichannel -= startc # may underflow if ispecial == 0 and ichannel < nchannels: histogram[ichannel, ibin] += 1 else: with gil: raise ValueError(f'no decoder available for {format=:02x}') def decode_t3_records( t3_t[::1] decoded, const uint32_t[::1] records, const uint32_t format ): """Decode PicoQuant T3 TTTR records.""" cdef: ssize_t nrecords = min(records.size, decoded.size) ssize_t i uint64_t overflow uint32_t itime, idtime, ichannel uint8_t ispecial, imarker with nogil: overflow = 0 # dispatch on format so compiler can inline decoder and optimize loop if format == 0x00010303: # PicoHarpT3 for i in range(nrecords): decode_pt3( records[i], &itime, &idtime, &ichannel, &overflow, &imarker, &ispecial ) if ispecial == 0: decoded[i].time = overflow + itime decoded[i].dtime = idtime decoded[i].channel = ichannel decoded[i].marker = 0 elif ispecial == 1: decoded[i].time = overflow + itime decoded[i].dtime = -1 decoded[i].channel = -1 decoded[i].marker = 0 elif ispecial == 2: decoded[i].time = overflow + itime decoded[i].dtime = -1 decoded[i].channel = -1 decoded[i].marker = imarker elif format in { 0x01010304, # HydraHarp2T3 0x00010305, # TimeHarp260NT3 0x00010306, # TimeHarp260PT3 0x00010307, # GenericT3 (MultiHarpT3 and Picoharp330T3) }: for i in range(nrecords): decode_ht3( records[i], &itime, &idtime, &ichannel, &overflow, &imarker, &ispecial ) if ispecial == 0: decoded[i].time = overflow + itime decoded[i].dtime = idtime decoded[i].channel = ichannel decoded[i].marker = 0 elif ispecial == 1: decoded[i].time = overflow + itime decoded[i].dtime = -1 decoded[i].channel = -1 decoded[i].marker = 0 elif ispecial == 2: decoded[i].time = overflow + itime decoded[i].dtime = -1 decoded[i].channel = -1 decoded[i].marker = imarker elif format == 0x00010304: # HydraHarpT3 for i in range(nrecords): decode_ht3v1( records[i], &itime, &idtime, &ichannel, &overflow, &imarker, &ispecial ) if ispecial == 0: decoded[i].time = overflow + itime decoded[i].dtime = idtime decoded[i].channel = ichannel decoded[i].marker = 0 elif ispecial == 1: decoded[i].time = overflow + itime decoded[i].dtime = -1 decoded[i].channel = -1 decoded[i].marker = 0 elif ispecial == 2: decoded[i].time = overflow + itime decoded[i].dtime = -1 decoded[i].channel = -1 decoded[i].marker = imarker else: with gil: raise ValueError(f'no decoder available for {format=:02x}') def decode_t2_records( t2_t[::1] decoded, const uint32_t[::1] records, const uint32_t format ): """Decode PicoQuant T2 TTTR records.""" cdef: ssize_t nrecords = min(records.size, decoded.size) ssize_t i uint64_t overflow uint32_t itime, idtime, ichannel uint8_t ispecial, imarker with nogil: overflow = 0 # dispatch on format so compiler can inline decoder and optimize loop if format == 0x00010203: # PicoHarpT2 for i in range(nrecords): decode_pt2( records[i], &itime, &idtime, &ichannel, &overflow, &imarker, &ispecial ) if ispecial == 0: decoded[i].time = overflow + itime decoded[i].channel = ichannel decoded[i].marker = 0 elif ispecial == 1: decoded[i].time = overflow + itime decoded[i].channel = -1 decoded[i].marker = 0 elif ispecial == 2: decoded[i].time = overflow + itime decoded[i].channel = -1 decoded[i].marker = imarker elif format in { 0x01010204, # HydraHarp2T2 0x00010205, # TimeHarp260NT2 0x00010206, # TimeHarp260PT2 0x00010207, # GenericT2 (MultiHarpT2 and Picoharp330T2) }: for i in range(nrecords): decode_ht2( records[i], &itime, &idtime, &ichannel, &overflow, &imarker, &ispecial ) if ispecial == 0: decoded[i].time = overflow + itime decoded[i].channel = ichannel decoded[i].marker = 0 elif ispecial == 1: decoded[i].time = overflow + itime decoded[i].channel = -1 decoded[i].marker = 0 elif ispecial == 2: decoded[i].time = overflow + itime decoded[i].channel = -1 decoded[i].marker = imarker elif format == 0x00010204: # HydraHarpT2 for i in range(nrecords): decode_ht2v1( records[i], &itime, &idtime, &ichannel, &overflow, &imarker, &ispecial ) if ispecial == 0: decoded[i].time = overflow + itime decoded[i].channel = ichannel decoded[i].marker = 0 elif ispecial == 1: decoded[i].time = overflow + itime decoded[i].channel = -1 decoded[i].marker = 0 elif ispecial == 2: decoded[i].time = overflow + itime decoded[i].channel = -1 decoded[i].marker = imarker else: with gil: raise ValueError(f'no decoder available for {format=:02x}') # Decode functions only set outputs relevant to each record type. # Callers must check special before using channel, dtime, or marker, # which may be stale from the previous call. cdef inline void decode_pt3( const uint32_t record, uint32_t* time, # nsync uint32_t* dtime, uint32_t* channel, uint64_t* overflow, uint8_t* marker, uint8_t* special ) noexcept nogil: cdef uint32_t tmp time[0] = record & 0xffff # 16 bit nsync dtime[0] = (record >> 16) & 0xfff # 12 bit dtime tmp = (record >> 28) & 0xf # 4 bit channel if tmp != 0xf: # regular record # TODO: understand this undocumented channel decoding if tmp > 0 and tmp < 5: channel[0] = tmp - 1 # one to zero-based else: # should not happen channel[0] = 4 special[0] = 0 elif dtime[0] == 0: # overflow special[0] = 1 overflow[0] += 65536 else: # marker special[0] = 2 marker[0] = dtime[0] dtime[0] = 0 cdef inline void decode_pt2( const uint32_t record, uint32_t* time, # timetag uint32_t* dtime, # not used uint32_t* channel, uint64_t* overflow, uint8_t* marker, uint8_t* special ) noexcept nogil: cdef uint32_t tmp dtime[0] = 0 # not present in T2 record time[0] = record & 0xfffffff # 28 bit timetag tmp = ((record >> 28) & 0xf) # 4 bit channel if tmp != 0xf: # regular record if tmp < 5: channel[0] = tmp else: # should not happen channel[0] = 5 special[0] = 0 else: tmp = record & 0xf # lower 4 bits if tmp == 0: # overflow special[0] = 1 overflow[0] += 210698240 else: # marker special[0] = 2 marker[0] = tmp cdef inline void decode_ht3( const uint32_t record, uint32_t* time, # nsync uint32_t* dtime, uint32_t* channel, uint64_t* overflow, uint8_t* marker, uint8_t* special ) noexcept nogil: cdef uint32_t tmp time[0] = record & 0x3ff # 10 bit nsync dtime[0] = (record >> 10) & 0x7fff # 15 bit dtime tmp = (record >> 31) & 0x1 # 1 bit special if tmp == 0: # regular record special[0] = 0 channel[0] = (record >> 25) & 0x3f # 6 bit channel else: tmp = (record >> 25) & 0x3f # 6 bit if tmp == 0x3f: # overflow special[0] = 1 if time[0] <= 1: overflow[0] += 1024 else: overflow[0] += time[0] * 1024 elif 0 < tmp < 16: # marker special[0] = 2 marker[0] = tmp else: # reserved; treat as overflow/skip special[0] = 1 cdef inline void decode_ht3v1( const uint32_t record, uint32_t* time, # nsync uint32_t* dtime, uint32_t* channel, uint64_t* overflow, uint8_t* marker, uint8_t* special ) noexcept nogil: cdef uint32_t tmp time[0] = record & 0x3ff # 10 bit nsync dtime[0] = (record >> 10) & 0x7fff # 15 bit dtime tmp = (record >> 31) & 0x1 # 1 bit special if tmp == 0: # regular record special[0] = 0 channel[0] = (record >> 25) & 0x3f # 6 bit channel else: tmp = (record >> 25) & 0x3f # 6 bit if tmp == 0x3f: # overflow special[0] = 1 overflow[0] += 1024 elif 0 < tmp < 16: # marker special[0] = 2 marker[0] = tmp else: # reserved; treat as overflow/skip special[0] = 1 cdef inline void decode_ht2( const uint32_t record, uint32_t* time, # timetag uint32_t* dtime, # not used uint32_t* channel, uint64_t* overflow, uint8_t* marker, uint8_t* special ) noexcept nogil: cdef uint32_t tmp dtime[0] = 0 # not present in T2 record time[0] = record & 0x1ffffff # 25 bit timetag tmp = (record >> 31) & 0x1 # 1 bit special if tmp == 0: # regular record special[0] = 0 channel[0] = (record >> 25) & 0x3f # 6 bit channel else: tmp = (record >> 25) & 0x3f # 6 bit if tmp == 0x3f: # overflow special[0] = 1 if time[0] <= 1: overflow[0] += 33554432 else: overflow[0] += time[0] * 33554432 elif tmp == 0: # regular record special[0] = 0 channel[0] = 0 elif tmp < 16: # marker special[0] = 2 marker[0] = tmp else: # reserved; treat as overflow/skip special[0] = 1 cdef inline void decode_ht2v1( const uint32_t record, uint32_t* time, # timetag uint32_t* dtime, # not used uint32_t* channel, uint64_t* overflow, uint8_t* marker, uint8_t* special ) noexcept nogil: cdef uint32_t tmp dtime[0] = 0 # not present in T2 record time[0] = record & 0x1ffffff # 25 bit timetag tmp = (record >> 31) & 0x1 # 1 bit special if tmp == 0: # regular record special[0] = 0 channel[0] = (record >> 25) & 0x3f # 6 bit channel else: tmp = (record >> 25) & 0x3f # 6 bit if tmp == 0x3f: # overflow special[0] = 1 overflow[0] += 33552000 elif tmp == 0: # regular record special[0] = 0 channel[0] = 0 elif tmp < 16: # marker special[0] = 2 marker[0] = tmp else: # reserved; treat as overflow/skip special[0] = 1 def encode_t3_image( uint32_t[::1] records, const uint_t[:, :, :, :, ::1] histogram, const uint32_t format, const uint32_t pixel_time, # average global time spent in pixel const uint32_t line_start, # mask const uint32_t line_stop, # mask const uint32_t frame_change, # mask ): """Return GenericT3 records from TCSPC image histogram.""" # TODO: frame and line markers may be combined in one record # TODO: record photons across channels at same time (slower) # TODO: randomize photon arrival times at fixed count rate cdef: ssize_t sizet, sizey, sizex, sizec, sizeh ssize_t t, y, x, c, h, nrecords, maxrecords uint_t count uint32_t time, time_in_pixel, overflow, maxtime, maxoverflow, i encode_func_t encode sizet, sizey, sizex, sizec, sizeh = histogram.shape[:5] maxrecords = records.size if format == 0x00010303: # PicoHarpT3/PicoHarp300 encode = encode_pt3 maxtime = 65536 maxoverflow = 1 elif format == 0x00010307: # GenericT3 (MultiHarpT3 and Picoharp330T3) encode = encode_ht3 maxtime = 1024 maxoverflow = 1023 else: raise ValueError(f'{format=} not supported') with nogil: time = 0 nrecords = 0 for t in range(sizet): # frame for y in range(sizey): # line if nrecords >= maxrecords: nrecords = -1 break # line start marker records[nrecords] = encode(time, 0, 0, 0, line_start) nrecords += 1 for x in range(sizex): if nrecords < 0: break # pixel time_in_pixel = 0 # record all photons at different time for c in range(sizec): # channel for h in range(sizeh): # bin for count in range(histogram[t, y, x, c, h]): # photon if nrecords >= maxrecords: nrecords = -1 break records[nrecords] = encode( time, h, c, 0, 0 ) nrecords += 1 # increment time time += 1 if time == maxtime: # overflow time = 0 if nrecords >= maxrecords: nrecords = -1 break records[nrecords] = encode(0, 0, 0, 1, 0) nrecords += 1 time_in_pixel += 1 if time_in_pixel == pixel_time: break if nrecords < 0: break if time_in_pixel == pixel_time: break if nrecords < 0: break if time_in_pixel == pixel_time: break if nrecords < 0: break # move to next pixel # TODO: calculate overflows overflow = 0 for i in range(pixel_time - time_in_pixel): time += 1 if time == maxtime: # overflow time = 0 overflow += 1 if overflow == maxoverflow: if nrecords >= maxrecords: nrecords = -1 break records[nrecords] = encode( 0, 0, 0, overflow, 0 ) nrecords += 1 overflow = 0 if nrecords >= 0 and overflow > 0: if nrecords >= maxrecords: nrecords = -1 else: records[nrecords] = encode(0, 0, 0, overflow, 0) nrecords += 1 if nrecords < 0: break if nrecords >= maxrecords: nrecords = -1 break # line end marker records[nrecords] = encode(time, 0, 0, 0, line_stop) nrecords += 1 if nrecords < 0: break if nrecords >= maxrecords: nrecords = -1 break # frame change marker records[nrecords] = encode(time, 0, 0, 0, frame_change) nrecords += 1 if nrecords < 0: pass # error: records array too small return nrecords cdef inline uint32_t encode_pt3( const uint32_t time, const uint32_t dtime, const uint32_t channel, const uint32_t overflow, const uint32_t marker, ) noexcept nogil: cdef: uint32_t record = time # & 0xffff # 16 bit nsync if marker != 0: # dtime is marker record |= marker << 16 # record |= (marker & 0xfff) << 16 # all channel bits set record |= 0xf0000000 elif overflow != 0: # dtime is 0, all channel bits set record |= 0xf0000000 else: # 12 bit dtime record |= dtime << 16 # record |= (dtime & 0xfff) << 16 # 4 bit channel, one-based record |= (channel + 1) << 28 # record |= ((channel + 1) & 0xf) << 28 return record cdef inline uint32_t encode_ht3( const uint32_t time, const uint32_t dtime, const uint32_t channel, const uint32_t overflow, const uint32_t marker, ) noexcept nogil: cdef: uint32_t record = time # & 0x3ff # 10 bit nsync record |= dtime << 10 # 15 bit dtime # record |= (dtime & 0x7fff) << 10 # 15 bit dtime if marker != 0: # 1 bit special record |= 0x80000000 # 4 bit marker instead of channel record |= marker << 25 # record |= (marker & 0xf) << 25 elif overflow != 0: # 1 bit special and all bits set instead of channel record |= 0xfe000000 # 10 bit multiple of 1024 increment of nsync instead of nsync record |= overflow # record |= overflow & 0x3ff else: # 6 bit channel record |= channel << 25 # record |= (channel & 0x3f) << 25 return record cgohlke-ptufile-ab09357/ptufile/numcodecs.py000066400000000000000000000072601515761367400211560ustar00rootroot00000000000000# ptufile/numcodecs.py # Copyright (c) 2024-2026, Christoph Gohlke # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # 3. Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. """PTU codec for the Numcodecs package.""" from __future__ import annotations __all__ = ['Ptu', 'register_codec'] from io import BytesIO from typing import TYPE_CHECKING from numcodecs import registry from numcodecs.abc import Codec from .ptufile import PtuFile if TYPE_CHECKING: from collections.abc import Sequence from types import EllipsisType from typing import Any from numpy.typing import ArrayLike, DTypeLike, NDArray class Ptu(Codec): # type: ignore[misc] """Ptu codec for Numcodecs.""" codec_id = 'ptufile' def __init__( self, *, selection: Sequence[int | slice | EllipsisType | None] | None = None, dtype: DTypeLike | None = None, channel: int | None = None, frame: int | None = None, dtime: int | None = None, pixel_time: float | None = None, trimdims: str | None = None, keepdims: bool = True, ) -> None: if selection is not None: # TODO: serialize slices, EllipsisType msg = f'{selection=}' raise NotImplementedError(msg) self.selection = selection self.dtype = dtype self.channel = channel self.frame = frame self.dtime = dtime self.pixel_time = pixel_time self.trimdims = trimdims self.keepdims = bool(keepdims) def encode(self, buf: ArrayLike) -> None: """Return Ptu file as bytes.""" raise NotImplementedError def decode(self, buf: bytes, out: Any | None = None) -> NDArray[Any]: """Return decoded image as NumPy array.""" with BytesIO(buf) as fh, PtuFile(fh, trimdims=self.trimdims) as ptu: return ptu.decode_image( self.selection, dtype=self.dtype, channel=self.channel, frame=self.frame, dtime=self.dtime, pixel_time=self.pixel_time, keepdims=self.keepdims, ) def register_codec(cls: Codec = Ptu, codec_id: str | None = None) -> None: """Register :py:class:`Ptu` codec with Numcodecs.""" registry.register_codec(cls, codec_id=codec_id) cgohlke-ptufile-ab09357/ptufile/ptufile.py000066400000000000000000003602501515761367400206470ustar00rootroot00000000000000# ptufile.py # Copyright (c) 2023-2026, Christoph Gohlke # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # 3. Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. """Read and write PicoQuant PTU and related files. Ptufile is a Python library to 1. read data and metadata from PicoQuant PTU and related files (PHU, PCK, PCO, PFS, PUS, PQRES, PQDAT, PQUNI, SPQR, and BIN), and 2. write TCSPC histograms to T3 image mode PTU files. PTU files contain time correlated single photon counting (TCSPC) measurement data and instrumentation parameters. :Author: `Christoph Gohlke `_ :License: BSD-3-Clause :Version: 2026.3.21 :DOI: `10.5281/zenodo.10120021 `_ Quickstart ---------- Install the ptufile package and all dependencies from the `Python Package Index `_:: python -m pip install -U "ptufile[all]" See `Examples`_ for using the programming interface. Source code and support are available on `GitHub `_. Requirements ------------ This revision was tested with the following requirements and dependencies (other versions may work): - `CPython `_ 3.12.10, 3.13.12, 3.14.3 64-bit - `NumPy `_ 2.4.3 - `Xarray `_ 2026.2.0 (recommended) - `Matplotlib `_ 3.10.8 (optional) - `Tifffile `_ 2026.3.3 (optional) - `Numcodecs `_ 0.16.5 (optional) - `Python-dateutil `_ 2.9.0 (optional) - `Cython `_ 3.2.4 (build) Revisions --------- 2026.3.21 - Add bounds checking to encode_t3_image function. - Use format-dispatch in hot decode loops to allow compiler inlining. - Build wheels on Windows with LLVM (30% faster decoding than MSVC). - Drop support for Python 3.11. 2026.2.6 - Fix code review issues. 2026.1.14 - Improve code quality. 2025.12.12 - Add PQUNI file type. - Add attrs properties and return with xarray DataSets. - Improve code quality. 2025.11.8 - Fix reading files with negative TTResult_NumberOfRecords. - Remove cache argument from PtuFile.read_records (breaking). - Add cache_records property to PtuFile to control caching behavior. - Derive PqFileError from ValueError. - Factor out BinaryFile base class. - Build ABI3 wheels. 2025.9.9 - Log error when decoding image with invalid line or frame masks. 2025.7.30 - Add option to specify pixel time for decoding images. - Add functions to read and write PicoQuant BIN files. - Drop support for Python 3.10. 2025.5.10 - Mark Cython extension free-threading compatible. - Support Python 3.14. 2025.2.20 - … Refer to the CHANGES file for older revisions. Notes ----- `PicoQuant GmbH `_ is a manufacturer of photonic components and instruments. The PicoQuant unified file formats are documented at the `PicoQuant-Time-Tagged-File-Format-Demos `_. The following features are currently not implemented due to the lack of test files or documentation: PT2 and PT3 files, decoding images from T2 and SPQR formats, bidirectional per frame, and deprecated image reconstruction. Compatibility with PTU files written by non-PicoQuant software (for example, Leica LAS X or Abberior Imspector) is limited, as is decoding line, bidirectional, and sinusoidal scanning. Other modules for reading or writing PicoQuant files are `Read_PTU.py `_, `readPTU `_, `readPTU_FLIM `_, `fastFLIM `_, `PyPTU `_, `PTU_Reader `_, `PTU_Writer `_, `FlimReader `_, `tangy `_, `tttrlib `_, `picoquantio `_, `ptuparser `_, `phconvert `_, `trattoria `_ (wrapper of `trattoria-core `_, `tttr-toolbox `_), `PAM `_, `FLOPA `_, and `napari-flim-phasor-plotter `_. Examples -------- Read properties and tags from any type of PicoQuant unified tagged file: >>> pq = PqFile('tests/data/Settings.pfs') >>> pq.type >>> pq.guid UUID('86d428e2-cb0b-4964-996c-04456ba6be7b') >>> pq.tags {...'CreatorSW_Name': 'SymPhoTime 64', 'CreatorSW_Version': '2.1'...} >>> pq.close() Read metadata from a PicoQuant PTU FLIM file: >>> ptu = PtuFile('tests/data/FLIM.ptu') >>> ptu.type >>> ptu.record_type >>> ptu.measurement_mode >>> ptu.measurement_submode Decode TTTR records from the PTU file to ``numpy.recarray``: >>> decoded = ptu.decode_records() >>> decoded.dtype dtype([('time', '>> decoded['time'][(decoded['marker'] & ptu.frame_change_mask) > 0] array([1571185680], dtype=uint64) Decode TTTR records to overall delay-time histograms per channel: >>> ptu.decode_histogram(dtype='uint8') array([[ 5, 7, 7, ..., 10, 9, 2]], shape=(2, 3126), dtype=uint8) Get information about the FLIM image histogram in the PTU file: >>> ptu.shape (1, 256, 256, 2, 3126) >>> ptu.dims ('T', 'Y', 'X', 'C', 'H') >>> ptu.coords {'T': ..., 'Y': ..., 'X': ..., 'H': ...} >>> ptu.dtype dtype('uint16') >>> ptu.active_channels (0, 1) Decode parts of the image histogram to ``numpy.ndarray`` using slice notation. Slice step sizes define binning, -1 being used to integrate along axis: >>> ptu[:, ..., 0, ::-1] array([[[103, ..., 38], ... [ 47, ..., 30]]], shape=(1, 256, 256), dtype=uint16) Alternatively, decode the first channel and integrate all histogram bins into a ``xarray.DataArray``, keeping reduced axes: >>> ptu.decode_image(channel=0, dtime=-1, asxarray=True) ... array([[[[[103]], ... [[ 30]]]]], shape=(1, 256, 256, 1, 1), dtype=uint16) Coordinates: * T (T) float64... 0.05625 * Y (Y) float64... -0.0001304 ... 0.0001294 * X (X) float64... -0.0001304 ... 0.0001294 * C (C) uint8... 0 * H (H) float64... 0.0 Attributes... name: FLIM.ptu ... Write the TCSPC histogram and metadata to a PicoHarpT3 image mode PTU file: >>> imwrite( ... '_test.ptu', ... ptu[:], ... ptu.global_resolution, ... ptu.tcspc_resolution, ... # optional metadata ... pixel_time=ptu.pixel_time, ... record_type=PtuRecordType.PicoHarpT3, ... comment='Written by ptufile.py', ... tags={'File_RawData_GUID': [ptu.guid]}, ... ) Read back the TCSPC histogram from the file: >>> tcspc_histogram = imread('_test.ptu') >>> import numpy >>> numpy.array_equal(tcspc_histogram, ptu[:]) True Close the file handle: >>> ptu.close() Preview the image and metadata in a PTU file from the console:: python -m ptufile tests/data/FLIM.ptu """ from __future__ import annotations __version__ = '2026.3.21' __all__ = [ 'FILE_EXTENSIONS', 'T2_RECORD_DTYPE', 'T3_RECORD_DTYPE', 'PhuFile', 'PhuMeasurementMode', 'PhuMeasurementSubMode', 'PqFile', 'PqFileError', 'PqFileType', 'PtuFile', 'PtuHwFeatures', 'PtuMeasurementMode', 'PtuMeasurementSubMode', 'PtuMeasurementWarnings', 'PtuRecordType', 'PtuScanDirection', 'PtuScannerType', 'PtuStopReason', 'PtuWriter', '__version__', 'binread', 'binwrite', 'imread', 'imwrite', ] import contextlib import dataclasses import enum import io import logging import math import os import struct import sys import uuid from datetime import datetime, timedelta from functools import cached_property from typing import TYPE_CHECKING, final, overload, override if TYPE_CHECKING: from collections.abc import Iterable, Sequence from types import EllipsisType, TracebackType from typing import IO, Any, ClassVar, Literal, Self from numpy.typing import ArrayLike, DTypeLike, NDArray from xarray import DataArray import numpy type Dimension = Literal['T', 'C', 'H'] type OutputType = str | IO[bytes] | NDArray[Any] | None @overload def imread( file: str | os.PathLike[str] | IO[bytes], /, selection: Sequence[int | slice | EllipsisType | None] | None = None, *, dtype: DTypeLike | None = None, channel: int | None = None, frame: int | None = None, dtime: int | None = None, pixel_time: float | None = None, bishift: int | None = None, trimdims: Sequence[Dimension] | str | None = None, keepdims: bool = True, asxarray: Literal[False] = ..., out: OutputType = None, ) -> NDArray[Any]: ... @overload def imread( file: str | os.PathLike[str] | IO[bytes], /, selection: Sequence[int | slice | EllipsisType | None] | None = None, *, dtype: DTypeLike | None = None, channel: int | None = None, frame: int | None = None, dtime: int | None = None, pixel_time: float | None = None, bishift: int | None = None, trimdims: Sequence[Dimension] | str | None = None, keepdims: bool = True, asxarray: Literal[True] = ..., out: OutputType = None, ) -> DataArray: ... @overload def imread( file: str | os.PathLike[str] | IO[bytes], /, selection: Sequence[int | slice | EllipsisType | None] | None = None, *, dtype: DTypeLike | None = None, channel: int | None = None, frame: int | None = None, dtime: int | None = None, pixel_time: float | None = None, bishift: int | None = None, trimdims: Sequence[Dimension] | str | None = None, keepdims: bool = True, asxarray: bool = False, out: OutputType = None, ) -> NDArray[Any] | DataArray: ... def imread( file: str | os.PathLike[str] | IO[bytes], /, selection: Sequence[int | slice | EllipsisType | None] | None = None, *, dtype: DTypeLike | None = None, channel: int | None = None, frame: int | None = None, dtime: int | None = None, pixel_time: float | None = None, bishift: int | None = None, trimdims: Sequence[Dimension] | str | None = None, keepdims: bool = True, asxarray: bool = False, out: OutputType = None, ) -> NDArray[Any] | DataArray: """Return decoded image histogram from T3 mode PTU file. Parameters: file: File name or seekable binary stream. selection, dtype, channel, frame, dtime, pixel_time, bishift,\ keepdims, asxarray, out: Passed to :py:meth:`PtuFile.decode_image`. trimdims: Passed to :py:class:`PtuFile`. Returns: image: Decoded TTTR T3 records as up to 5-dimensional image array. """ with PtuFile(file, trimdims=trimdims) as ptu: return ptu.decode_image( selection, dtype=dtype, channel=channel, frame=frame, dtime=dtime, bishift=bishift, pixel_time=pixel_time, keepdims=keepdims, asxarray=asxarray, out=out, ) def imwrite( file: str | os.PathLike[str] | IO[bytes], data: ArrayLike, /, global_resolution: float, tcspc_resolution: float, pixel_time: float | None = None, *, has_frames: bool | None = None, record_type: PtuRecordType | None = None, pixel_resolution: float | None = None, guid: str | uuid.UUID | None = None, comment: str | None = None, datetime: datetime | None = None, tags: dict[str, Any] | None = None, mode: Literal['w', 'wb', 'x', 'xb'] | None = None, ) -> None: """Write TCSPC histogram to T3 image mode PTU file. Parameters: file: File name or writable binary stream. data: TCSPC histogram image stack. The order of dimensions must be 'TYXCH', 'YXH', 'YXCH', or 'TYXH' (with `has_frames=True`). The dtype must be unsigned integer. global_resolution: Resolution of time tags in s, typically in ns range. The inverse of the synctime or laser frequency. One photon is encoded per time tag. tcspc_resolution: Resolution of TCSPC in s, typically in ps range. The width of a histogram bin. pixel_time: Time per pixel in s, typically in μs range. Photons that cannot be encoded within pixel_time are omitted. By default, pixel_time is set just large enough to encode all photons. has_frames: 4-dimensional data have frames in first axis ('TYXH'), no channels. By default, true if data contains metadata specifying the first dimension is 'T', else false. record_type, pixel_resolution, guid, comment, datetime, tags, mode: Optional parameters passed to :py:class:`PtuWriter`. """ if hasattr(data, 'dims'): has_frames = 'T' in data.dims and data.dims[0] == 'T' data = numpy.asarray(data) if pixel_time is None: data = data.reshape( PtuWriter.normalize_shape(data.shape, has_frames=has_frames) ) pixel_time = global_resolution * max( 1, float(numpy.max(data.sum(axis=(3, 4), dtype=numpy.uint64))) ) with PtuWriter( file, data.shape, global_resolution, tcspc_resolution, pixel_time, record_type=record_type, pixel_resolution=pixel_resolution, has_frames=has_frames, guid=guid, comment=comment, datetime=datetime, tags=tags, mode=mode, ) as ptu: ptu.write(data) @final class PtuWriter: """Write TCSPC histogram to T3 image mode PTU file. T3 TTTR records allow for a maximum of 63 channels and 32768 bins. The TTTR records written can only be used to reconstruct the encoded TCSPC histogram image stack, not for higher-than-pixel-time-resolution intensity time-trace or correlation analysis. Parameters: file: File name or writable binary stream. File names typically end in '.PTU'. shape: Shape of TCSPC histogram image stack to write. The order of dimensions must be 'TYXCH', 'YXH', 'YXCH', or 'TYXH' (with `has_frames=True`). global_resolution: Resolution of time tags in s, typically in ns range. The inverse of the synctime or laser frequency. One photon is encoded per time tag. tcspc_resolution: Resolution of TCSPC in s, typically in ps range. The width of a histogram bin. pixel_time: Time per pixel in s, typically in μs range. Photons that cannot be encoded within pixel_time are omitted. record_type: Type of TTTR T3 records to write. By default, write ``PicoHarpT3`` records for up to two channels and 4096 bins, else ``GenericT3``. pixel_resolution: Resolution of single pixel in μm. The default is 1 μm. has_frames: 4-dimensional shape has frames in first axis ('TYXH'), no channels. guid: Windows formatted GUID used as global file identifier. By default, a random GUID. Write to File_GUID tag. comment: File comment. Write to File_Comment tag. datetime: File creation date and time. The default is time at function call. Write to File_CreatingTime tag. tags: Additional tag Id and values to write. Critical tags are automatically set and cannot be modified. No validation is performed. Refer to the "PicoQuant Unified Tag Dictionary" for valid Id and values. mode: Binary file open mode if `file` is file name. The default is 'w', which opens files for writing, truncating existing files. 'x' opens files for exclusive creation, failing on existing files. Raises: ValueError Not ``0 < tcspc_resolution <= global_resolution <= pixel_time``. """ _fh: IO[bytes] | None _shape: tuple[int, int, int, int, int] _record_type: PtuRecordType _number_records: int _number_records_offset: int _number_frames: int _number_frames_offset: int _global_resolution: float _tcspc_resolution: float _pixel_time: int _line_start = 1 _line_stop = 2 _frame_change = 3 def __init__( self, file: str | os.PathLike[str] | IO[bytes], /, shape: tuple[int, ...], global_resolution: float, tcspc_resolution: float, pixel_time: float, *, record_type: PtuRecordType | None = None, pixel_resolution: float | None = None, has_frames: bool | None = None, guid: str | uuid.UUID | None = None, comment: str | None = None, datetime: datetime | None = None, tags: dict[str, Any] | None = None, mode: Literal['w', 'wb', 'x', 'xb'] | None = None, ) -> None: """Write PTU header to file.""" # 0 < tcspc_resolution <= global_resolution <= pixel_time if tcspc_resolution <= 0.0: msg = f'{tcspc_resolution=} <= 0.0' raise ValueError(msg) if tcspc_resolution > global_resolution: msg = f'{tcspc_resolution=} > {global_resolution=}' raise ValueError(msg) if pixel_time < global_resolution: msg = f'{pixel_time=} < {global_resolution=}' raise ValueError(msg) self._fh = None self._number_records = 0 self._number_records_offset = 0 self._number_frames = 0 self._number_frames_offset = 0 self._global_resolution = global_resolution self._tcspc_resolution = tcspc_resolution self._pixel_time = round(pixel_time / global_resolution) self._shape = shape = PtuWriter.normalize_shape( shape, has_frames=has_frames ) if record_type is None: if shape[3] <= 2 and shape[4] <= 4096: record_type = PtuRecordType.PicoHarpT3 else: record_type = PtuRecordType.GenericT3 if record_type == PtuRecordType.PicoHarpT3: if shape[3] > 4 or shape[4] > 4096: msg = ( f'{record_type=} does not support ' f'{shape[3]} channels and {shape[4]} bins' ) raise ValueError(msg) self._record_type = PtuRecordType.PicoHarpT3 elif record_type in { PtuRecordType.GenericT3, PtuRecordType.HydraHarp2T3, PtuRecordType.TimeHarp260NT3, PtuRecordType.TimeHarp260PT3, }: if shape[3] > 63 or shape[4] > 32768: msg = ( f'{record_type=} does not support ' f'{shape[3]} channels and {shape[4]} bins' ) raise ValueError(msg) self._record_type = PtuRecordType.GenericT3 else: msg = f'{record_type=} not supported' raise ValueError(msg) if comment is None: comment = '' if guid is None: guid = f'{{{uuid.uuid4()}}}' elif isinstance(guid, uuid.UUID): guid = f'{{{guid}}}' elif len(guid) != 38 or guid[9] != '-': msg = 'invalid GUID' raise ValueError(msg) if pixel_resolution is None: pixel_resolution = 1.0 elif pixel_resolution <= 0.0: msg = f'{pixel_resolution=} <= 0.0' raise ValueError(msg) if datetime is None: datetime = now() critical_tags = { # tags not to be overwritten by user 'Measurement_Mode': PtuMeasurementMode.T3, 'Measurement_SubMode': PtuMeasurementSubMode.IMAGE, 'MeasDesc_GlobalResolution': float(self._global_resolution), 'MeasDesc_Resolution': float(self._tcspc_resolution), 'MeasDesc_BinningFactor': 1, 'TTResult_NumberOfRecords': self._number_records, 'TTResult_SyncRate': round(1.0 / self._global_resolution), 'TTResultFormat_TTTRRecType': self._record_type, 'TTResultFormat_BitsPerRecord': 32, 'ImgHdr_Dimensions': 3, 'ImgHdr_Ident': PtuScannerType.LSM, 'ImgHdr_LineStart': self._line_start, 'ImgHdr_LineStop': self._line_stop, 'ImgHdr_Frame': self._frame_change, 'ImgHdr_MaxFrames': 0, 'ImgHdr_TimePerPixel': pixel_time * 1e3, # ms 'ImgHdr_PixX': int(self._shape[2]), 'ImgHdr_PixY': int(self._shape[1]), 'ImgHdr_BiDirect': False, 'ImgHdr_SinCorrection': 0, } pqtags = { # required tags written first, allowed to be overwritten by user 'File_GUID': guid, 'File_Comment': comment, 'File_CreatingTime': datetime, 'CreatorSW_Name': 'ptufile.py', 'CreatorSW_Version': __version__, } pqtags.update(critical_tags) pqtags.update( # other tags allowed to be overwritten by user { 'ImgHdr_PixResol': float(pixel_resolution), 'HW_InpChannels': self._shape[3] + 1, # used by FlimReader # 'TTResult_StopReason': PtuStopReason(0) } ) if tags is not None: # add user tags but do not overwrite critical tags pqtags.update( {k: v for k, v in tags.items() if k not in critical_tags} ) header_list = [b'PQTTTR\x00\x001.0.00\x00\x00'] # magic and version for tagid, value in pqtags.items(): if isinstance(value, (list, tuple)): for index, item in enumerate(value): header_list.append(encode_tag(tagid, item, index)) else: header_list.append(encode_tag(tagid, value)) header_list.append(encode_tag('Header_End', None)) header = b''.join(header_list) offset = header.find(b'TTResult_NumberOfRecords') if offset < 0: msg = 'TTResult_NumberOfRecords tag not found in header' raise RuntimeError(msg) self._number_records_offset = offset + 40 offset = header.find(b'ImgHdr_MaxFrames') if offset < 0: msg = 'ImgHdr_MaxFrames tag not found in header' raise RuntimeError(msg) self._number_frames_offset = offset + 40 if isinstance(file, (str, os.PathLike)): if mode is None: mode = 'wb' elif mode[-1] != 'b': mode += 'b' # type: ignore[assignment] self._fh = open(file, mode) # noqa: SIM115 self._close = True elif hasattr(file, 'write') and hasattr(file, 'seek'): self._fh = file self._close = False else: msg = f'cannot write to {type(file)=}' raise ValueError(msg) self._fh.write(header) def write(self, data: ArrayLike, /) -> None: """Append T3 encoded TCSPC histogram to file. Parameters: data: TCSPC histogram image stack. The shape must be compatible with the shape passed to PtuWriter(). The dtype must be unsigned integer. """ from ._ptufile import encode_t3_image data = numpy.asarray(data) if data.dtype.kind != 'u': msg = f'{data.dtype=} is not an unsigned integer' raise ValueError(msg) data = data.reshape(self._shape) number_photons = int(data.sum(dtype=numpy.uint64)) if self._record_type == PtuRecordType.PicoHarpT3: maxtime = 65536 else: maxtime = 1024 shape = data.shape number_records = ( number_photons # overflows assuming all empty pixels + (shape[0] * shape[1] * shape[2] * self._pixel_time) // maxtime # line markers + shape[0] * shape[1] * 2 # frame markers + shape[0] ) records = numpy.zeros(number_records, dtype=numpy.uint32) number_records = encode_t3_image( records, data, self._record_type, self._pixel_time, int(2 ** (self._line_start - 1)), int(2 ** (self._line_stop - 1)), int(2 ** (self._frame_change - 1)), ) if number_records < 0: msg = f'{number_records=} < 0' raise ValueError(msg) if self._fh is None: msg = 'file handle is closed' raise RuntimeError(msg) self._fh.write(records[:number_records].tobytes()) self._number_records += number_records self._number_frames += shape[0] def close(self) -> None: """Close file handle after writing final tag values.""" if self._fh is None: return if self._number_records_offset > 0: self._fh.seek(self._number_records_offset) self._fh.write(struct.pack(' Self: return self def __exit__( self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, ) -> None: self.close() @staticmethod def normalize_shape( shape: tuple[int, ...], /, *, has_frames: bool | None = None, ) -> tuple[int, int, int, int, int]: """Return TCSPC histogram shape normalized to 5D 'TYXCH'.""" ndim = len(shape) if ndim == 5: 'TYXCH' return shape # type: ignore[return-value] if ndim == 3: # 'YXH' return (1, shape[0], shape[1], 1, shape[2]) if ndim == 4: if has_frames: # 'TYXH' return (shape[0], shape[1], shape[2], 1, shape[3]) # 'YXCH' return (1, shape[0], shape[1], shape[2], shape[3]) msg = f'invalid number of dimensions {len(shape)=}' raise ValueError(msg) class PqFileType(enum.Enum): """PicoQuant file type identifiers.""" PTU = b'PQTTTR\0\0' """TTTR file, PTU, contains raw data in unified TTTR-format.""" PHU = b'PQHISTO\0' """Histogram file, PHU, contains TCSPC histograms.""" PCK = b'PQCHECK\0' """Internal file, PCK, contains post-acquisition analysis results.""" PCO = b'PQCOMNT\0' """Comment file, PCO, contains manually entered text.""" PFS = b'PQDEFLT\0' """Settings file, PFS or PUS, contains factory or user setting defaults.""" PQRES = b'PQRESLT\0' """Result file, PQRES, contains analysis generated during measurement.""" PQDAT = b'PQDATA\0\0' """Data file, PQDAT, contains undocumented data.""" PQUNI = b'PQUNI\0\0\0' """UniHarp file, PQUNI, contains memory and measured data.""" SPQR = b'PQSPQR\0\0' """Unknown file, SPQR, contains undocumented data.""" class PqFileError(ValueError): """Exception to indicate invalid PicoQuant tagged file structure.""" class BinaryFile: """Binary file. Parameters: file: File name or seekable binary stream. mode: File open mode if `file` is a file name. If not specified, defaults to 'r'. Files are always opened in binary mode. Raises: TypeError: File is a text stream, or an unsupported type. ValueError: Invalid file name, extension, or stream. File stream is not seekable. """ _fh: IO[bytes] _path: str # absolute path of file _name: str # name of file or handle _close: bool # file needs to be closed _closed: bool # file is closed _ext: ClassVar[set[str]] = set() # valid extensions, empty for any def __init__( self, file: str | os.PathLike[str] | IO[bytes], /, *, mode: Literal['r', 'r+'] | None = None, ) -> None: self._path = '' self._name = 'Unnamed' self._close = False self._closed = False if isinstance(file, (str, os.PathLike)): ext = os.path.splitext(file)[-1].lower() if self._ext and ext not in self._ext: msg = f'invalid file extension: {ext!r} not in {self._ext!r}' raise ValueError(msg) if mode is None: mode = 'r' else: if mode[-1:] == 'b': # accept 'rb'/'r+b' mode = mode[:-1] # type: ignore[assignment] if mode not in ('r', 'r+'): msg = f'invalid {mode=!r}' raise ValueError(msg) self._path = os.path.abspath(file) self._close = True self._fh = open(self._path, mode + 'b') # noqa: SIM115 elif hasattr(file, 'seek'): # binary stream: open file, BytesIO, fsspec LocalFileOpener if isinstance(file, io.TextIOBase): # type: ignore[unreachable] msg = ( # type: ignore[unreachable] f'{file=!r} is not open in binary mode' ) raise TypeError(msg) self._fh = file try: self._fh.tell() except Exception as exc: msg = f'{file=!r} is not seekable' raise ValueError(msg) from exc if hasattr(file, 'path'): self._path = os.path.abspath(file.path) elif hasattr(file, 'name'): self._path = os.path.abspath(file.name) elif hasattr(file, 'open'): # fsspec OpenFile self._fh = file.open() self._close = True try: self._fh.tell() except Exception as exc: with contextlib.suppress(Exception): self._fh.close() msg = f'{file=!r} is not seekable' raise ValueError(msg) from exc if hasattr(file, 'path'): self._path = os.path.abspath(file.path) else: msg = f'cannot handle {type(file)=}' raise TypeError(msg) if hasattr(file, 'name') and file.name: self._name = os.path.basename(file.name) elif self._path: self._name = os.path.basename(self._path) else: self._name = type(file).__name__ @property def filehandle(self) -> IO[bytes]: """File handle.""" return self._fh @property def filepath(self) -> str: """Absolute path to file, or empty string if unavailable.""" return self._path @property def filename(self) -> str: """Name of file, or empty if no path is available.""" return os.path.basename(self._path) @property def dirname(self) -> str: """Directory containing file, or empty if no path is available.""" return os.path.dirname(self._path) @property def name(self) -> str: """Display name of file.""" return self._name @name.setter def name(self, value: str) -> None: self._name = value @property def attrs(self) -> dict[str, Any]: """Selected metadata as dict.""" return {'name': self.name, 'filepath': self.filepath} @property def closed(self) -> bool: """File is closed.""" return self._closed def close(self) -> None: """Close file.""" self._closed = True # always report file as closed if self._close: with contextlib.suppress(Exception): self._fh.close() def __enter__(self) -> Self: return self def __exit__( self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, ) -> None: self.close() def __repr__(self) -> str: return f'<{self.__class__.__name__} {self._name!r}>' class PqFile(BinaryFile): """PicoQuant unified tagged file. PTU, PHU, PCK, PCO, PFS, PUS, PQRES, PQDAT, PQUNI, and SPQR files contain measurement metadata and settings encoded as unified tags. ``PqFile`` and subclass instances are not thread safe. All attributes are read-only. ``PqFile`` and subclass instances must be closed with :py:meth:`PqFile.close`, which is automatically called when using the 'with' context manager. Parameters: file: File name or seekable binary stream. mode: File open mode if `file` is file name. The default is 'r'. Files are always opened in binary mode. fastload: If true, only read tags marked for fast loading, else read all tags. Raises: PqFileError: File is not a PicoQuant tagged file or is corrupted. """ type: PqFileType """PicoQuant file type identifier.""" version: str """File version.""" tags: dict[str, Any] """PicoQuant unified tags.""" _data_offset: int # position of raw data in file _number_records_offset: int # position of TTResult_NumberOfRecords value _TYPE: ClassVar[set[PqFileType]] = set(PqFileType) _STR_: tuple[str, ...] = ('type', 'version') # attributes listed first def __init__( self, file: str | os.PathLike[str] | IO[bytes], /, *, mode: Literal['r', 'r+'] | None = None, fastload: bool = False, ) -> None: super().__init__(file, mode=mode) self.version = '' self.tags = {} fh = self._fh magic = fh.read(8) try: self.type = PqFileType(magic) except ValueError as exc: self.close() msg = ( f'{self.filename!r} is not a {self.__class__.__name__} ' f'{magic=!r}' ) raise PqFileError(msg) from exc if self.type not in self._TYPE: self.close() msg = ( f'{self.filename!r} type={self.type} is not in {self._TYPE!r}' ) raise PqFileError(msg) self.version = fh.read(8).strip(b'\0').decode() tags = self.tags def errmsg( msg: str, tagid: str, index: int, typecode: int, value: Any ) -> str: return ( f'{msg} @ {self.name!r} ' f'{tagid=}, {index=}, {typecode=}, {value=!r}' )[:160] tagid: str index: int typecode: int value: Any unpack = struct.unpack try: while True: offset = fh.tell() tagid_, index, typecode, value = unpack( '<32siI8s', fh.read(48) ) # print(tagid.strip(b'\0'), index, typecode, value) tagid = tagid_.rstrip(b'\0').decode('ascii', errors='ignore') # tags must start on positions divisible by 8 # disabled for PQRES and PQDAT if offset % 8 and self.type not in { PqFileType.PQRES, PqFileType.PQDAT, }: logger().error( errmsg( f'tag {offset=} not divisible by 8', tagid, index, typecode, value, ) ) if tagid == 'Header_End': break if tagid == 'Fast_Load_End': if fastload: break continue match typecode: # frequent typecodes case PqTagType.Int8: value = unpack(' uuid.UUID: """Global identifier of file.""" return uuid.UUID(self.tags['File_GUID']) @property def comment(self) -> str | None: """File comment, if any.""" return self.tags.get('File_Comment') @property def datetime(self) -> datetime | None: """File creation date, if any.""" if 'File_CreatingTime' not in self.tags: return None value = self.tags['File_CreatingTime'] if isinstance(value, datetime): return value if isinstance(value, str): try: return datetime.fromisoformat(value) except ValueError: try: from dateutil import parser return parser.parse(value) except Exception: return None if not isinstance(value, float): return None try: return datetime(1899, 12, 30) + timedelta(days=value) except Exception: return None @override @property def attrs(self) -> dict[str, Any]: """Selected metadata as dict.""" return { **super().attrs, 'type': self.type.name, 'version': self.version, 'guid': str(self.guid), 'comment': self.comment, 'datetime': ( None if self.datetime is None else self.datetime.isoformat() ), 'tags': self.tags, } @override def __enter__(self) -> Self: return self def __str__(self) -> str: return indent( repr(self), *( f'{name}: {getattr(self, name)!r}'[:160] for name in self._STR_ if getattr(self, name) is not None ), *( f'{name}: {getattr(self, name)!r}'[:160] for name in dir(self) if not ( name in self._STR_ or name in {'attrs', 'dirname', 'tags', 'name', 'filehandle'} or name.startswith('_') or callable(getattr(self, name)) ) ), indent( 'tags:', *( f'{key}: {value!r}'[:160] for key, value in self.tags.items() ), ), ) @final class PhuFile(PqFile): """PicoQuant histogram file. PHU files contain a series of TCSPC histograms in addition to unified tags. ``PhuFile`` instances are derived from :py:class:`PqFile`. Parameters: file: File name or seekable binary stream. Raises: PqFileError: File is not a PicoQuant PHU file or is corrupted. """ _TYPE: ClassVar[set[PqFileType]] = {PqFileType.PHU} _STR_ = ('type', 'version', 'measurement_mode', 'measurement_submode') def __init__( self, file: str | os.PathLike[str] | IO[bytes], /, *, mode: Literal['r', 'r+'] | None = None, ) -> None: super().__init__(file, mode=mode) @override def __enter__(self) -> Self: return self @property def measurement_mode(self) -> PhuMeasurementMode: """Kind of measurement: HISTOGRAM or CONTI.""" return PhuMeasurementMode(self.tags['Measurement_Mode']) @property def measurement_submode(self) -> PhuMeasurementSubMode: """Sub-kind of measurement: OSCILLOSCOPE, INTEGRATING, or TRES.""" return PhuMeasurementSubMode(self.tags['Measurement_SubMode']) @property def tcspc_resolution(self) -> float: """Resolution of TCSPC in s (BaseResolution * iBinningFactor).""" return float(self.tags.get('MeasDesc_Resolution', 0.0)) @property def histogram_resolutions(self) -> tuple[float, ...] | None: """Base resolution for each histogram.""" ncurves = self.number_histograms if 'HistResDscr_HWBaseResolution' in self.tags: return tuple(self.tags['HistResDscr_HWBaseResolution']) if 'HW_BaseResolution' in self.tags: return tuple([float(self.tags['HW_BaseResolution'])] * ncurves) return None @property def number_histograms(self) -> int: """Number of histograms stored in file.""" return int(self.tags['HistoResult_NumberOfCurves']) # @property # def bits_per_bin(self) -> int: # """Number of bits per histogram bin.""" # return int(self.tags.get('HistoResult_BitsPerBin', 32)) @override @property def attrs(self) -> dict[str, Any]: """Selected metadata as dict.""" return { **super().attrs, 'measurement_mode': self.measurement_mode.name, 'measurement_submode': self.measurement_submode.name, 'tcspc_resolution': self.tcspc_resolution, } @overload def histograms( self, index: int | slice | None = None, /, *, asxarray: Literal[False] = ..., ) -> tuple[NDArray[numpy.uint32], ...]: ... @overload def histograms( self, index: int | slice | None = None, /, *, asxarray: Literal[True] = ..., ) -> tuple[DataArray, ...]: ... @overload def histograms( self, index: int | slice | None = None, /, *, asxarray: bool = ..., ) -> tuple[NDArray[numpy.uint32] | DataArray, ...]: ... def histograms( self, index: int | slice | None = None, /, *, asxarray: bool = False ) -> tuple[NDArray[numpy.uint32] | DataArray, ...]: """Return sequences of histograms from file. Parameters: index: Index of histogram(s) to return. By default, all histograms are returned. asxarray: If true, return histograms as ``xarray.DataArray``, else ``numpy.ndarray`` (default). """ if index is None: index = slice(None) elif isinstance(index, int): index = slice(index, index + 1) ncurves = self.number_histograms if len(self.tags['HistResDscr_DataOffset']) != ncurves: msg = 'invalid HistResDscr_DataOffset tag' raise ValueError(msg) if len(self.tags['HistResDscr_HistogramBins']) != ncurves: msg = 'invalid HistResDscr_HistogramBins tag' raise ValueError(msg) histograms: list[NDArray[numpy.uint32] | DataArray] = [] for offset, nbins in zip( self.tags['HistResDscr_DataOffset'][index], self.tags['HistResDscr_HistogramBins'][index], strict=True, ): self._fh.seek(offset) histograms.append( numpy.fromfile(self._fh, dtype=' None: """Plot histograms using matplotlib. Parameters: verbose: Print information about histogram arrays. show: If true (default), display all figures. Else, defer to user or environment to display figures. """ from matplotlib import pyplot from tifffile import Timer t = Timer() histograms = self.histograms(asxarray=True) if verbose: t.print('decode histograms') print() for hist in histograms: print(hist) for i, hist in enumerate(histograms): y = numpy.trim_zeros(hist.values, trim='b') x = hist.coords['H'].values[: y.size] pyplot.plot(x, y, label=f'ch {i}') pyplot.title(repr(self)) pyplot.xlabel('delay time [s]') pyplot.ylabel('photon count') pyplot.legend() if show: pyplot.show() @final class PtuFile(PqFile): """PicoQuant time-tagged time-resolved (TTTR) file. PTU files contain TTTR records in addition to unified tags. ``PtuFile`` instances are not thread-safe. All attributes are read-only. ``PtuFile`` is derived from :py:class:`PqFile`. Parameters: file: File name or seekable binary stream. trimdims: Axes to trim. The default is ``'TCH'``: - ``'T'``: remove incomplete first or last frame. - ``'C'``: remove leading and trailing channels without photons. Else use record type's default :py:attr:`number_channels_max`. - ``'H'``: remove trailing delay-time bins without photons. Else use record type's default :py:attr:`number_bins_max`. Raises: PqFileError: File is not a PicoQuant PTU file or is corrupted. """ _trimdims: set[str] _cache: bool # cache records in memory _asxarray: bool # return DataArray from slicing and decode_ functions _dtype: numpy.dtype[Any] _records: NDArray[numpy.uint32] | None # cached records _TYPE: ClassVar[set[PqFileType]] = {PqFileType.PTU} _STR_ = ( 'type', 'version', 'record_type', 'measurement_mode', 'measurement_submode', 'scanner', 'sizes', ) def __init__( self, file: str | os.PathLike[str] | IO[bytes], /, *, mode: Literal['r', 'r+'] | None = None, trimdims: Sequence[Dimension] | str | None = None, ) -> None: self._records = None super().__init__(file, mode=mode) if trimdims is None: self._trimdims = {'T', 'C', 'H'} else: self._trimdims = {ax.upper() for ax in trimdims} self._dtype = numpy.dtype(numpy.uint16) self._asxarray = False self._cache = True @override def __enter__(self) -> Self: return self def __getitem__(self, key: Any, /) -> NDArray[Any] | DataArray: return self.decode_image(key, keepdims=False) @override def close(self) -> None: """Close file handle and free resources.""" del self._records # close numpy.memmap file handle self._records = None super().close() @property def record_offset(self) -> int: """Position of records in file.""" return self._data_offset @property def record_type(self) -> PtuRecordType: """Type of TTTR records. Defines the TCSPC device and type of measurement that produced the records. """ return PtuRecordType(self.tags['TTResultFormat_TTTRRecType']) @property def measurement_mode(self) -> PtuMeasurementMode: """Kind of TCSPC measurement: T2 or T3.""" return PtuMeasurementMode(self.tags['Measurement_Mode']) @property def measurement_submode(self) -> PtuMeasurementSubMode: """Sub-kind of measurement: Point, line, or image scan.""" return PtuMeasurementSubMode(self.tags['Measurement_SubMode']) @property def measurement_ndim(self) -> int: """Dimensionality of measurement.""" # Measurement_SubMode is not always correct submode = self.tags['Measurement_SubMode'] if ( submode == 3 and self.tags.get('ImgHdr_Dimensions', 3) == 3 # optional # and self.tags.get('ImgHdr_PixY', 1) > 1 # may be missing ): return 3 if ( submode == 2 and self.tags.get('ImgHdr_Dimensions', 2) == 2 # optional # and self.tags.get('ImgHdr_PixX', 1) > 1 # may be missing ): # TODO: need linescan test file return 2 return 1 @property def measurement_warnings(self) -> PtuMeasurementWarnings | None: """Warnings during measurement, or None if not specified.""" if 'TTResult_MDescWarningFlags' in self.tags: return PtuMeasurementWarnings( self.tags['TTResult_MDescWarningFlags'] ) return None @property def hardware_features(self) -> PtuHwFeatures | None: """Hardware features, or None if not specified.""" if 'HW_Features' in self.tags: return PtuHwFeatures(self.tags['HW_Features']) return None @property def stop_reason(self) -> PtuStopReason | None: """Reason for measurement end, or None if not specified.""" if 'TTResult_StopReason' in self.tags: return PtuStopReason(self.tags['TTResult_StopReason']) return None @property def scanner(self) -> PtuScannerType | None: """Scanner hardware, or None if not specified.""" if 'ImgHdr_Ident' in self.tags: return PtuScannerType(self.tags['ImgHdr_Ident']) return None @property def global_resolution(self) -> float: """Resolution of time tags in s.""" return float(self.tags['MeasDesc_GlobalResolution']) @property def tcspc_resolution(self) -> float: """Resolution of TCSPC in s (BaseResolution * iBinningFactor).""" return float(self.tags.get('MeasDesc_Resolution', 0.0)) @cached_property def number_records(self) -> int: """Number of TTTR records.""" # NOTE: this does not catch invalid TTResult_NumberOfRecords > 0 count = value = int(self.tags.get('TTResult_NumberOfRecords', 0)) if count <= 0: count = (self._fh.seek(0, os.SEEK_END) - self._data_offset) // 4 if count != 0: logger().warning( f'{self!r} invalid TTResult_NumberOfRecords={value}. ' 'Using remaining file content as records' ) return count @property def number_photons(self) -> int: """Number of photons counted.""" return self._info.photons @property def number_markers(self) -> int: """Number of marker events.""" return self._info.markers @property def number_images(self) -> int: """Number of images separated by frame change markers.""" return self._info.frames @property def number_lines(self) -> int: """Number of line marker pairs.""" return self._info.lines @property def number_channels_max(self) -> int: """Maximum number of channels for record type.""" return self._info.channels @property def number_channels(self) -> int: """Number of channels, without leading and trailing empty channels.""" return ( 1 + self._info.channels_active_last - self._info.channels_active_first ) @property def active_channels(self) -> tuple[int, ...]: """Indices of un-trimmed channels containing photons.""" channels_active = self._info.channels_active return tuple( ch for ch in range(self._info.channels) if channels_active & (1 << ch) ) @property def number_bins_max(self) -> int: """Maximum number of delay-time bins for record type.""" return self._info.bins @property def number_bins(self) -> int: """Number of bins up to the largest occupied delay-time bin. Not available for T2 records. """ return self._info.bins_used @property def number_bins_in_period(self) -> int: """Delay time in one period. Not available for T2 records. Same as ``global_resolution / tcspc_resolution`` """ if self.tcspc_resolution < 1e-14: return 1 nbins = math.floor(self.global_resolution / self.tcspc_resolution) return max(nbins, 1) @property def line_start_mask(self) -> int: """Marker mask defining line start, or 0 if not defined.""" value = self.tags.get('ImgHdr_LineStart', None) return int(2 ** (value - 1) if value is not None else 0) @property def line_stop_mask(self) -> int: """Marker mask defining line end, or 0 if not defined.""" value = self.tags.get('ImgHdr_LineStop', None) return int(2 ** (value - 1) if value is not None else 0) @property def frame_change_mask(self) -> int: """Marker mask defining image frame change, or 0 if not defined.""" value = self.tags.get('ImgHdr_Frame', None) return int(2 ** (value - 1) if value is not None else 0) @property def global_pixel_time(self) -> int: """Global time per pixel. Multiply with global resolution to get time in s. """ if self.tags.get('ImgHdr_TimePerPixel', 0.0) > 0.0: pixeltime = ( float(self.tags['ImgHdr_TimePerPixel']) / float(self.tags['MeasDesc_GlobalResolution']) / 1e3 ) elif self._info.lines > 0: pixeltime = self._info.line_time / self.pixels_in_line else: pixeltime = 1e-3 / float(self.tags['MeasDesc_GlobalResolution']) return max(1, round(pixeltime)) @property def global_line_time(self) -> int: """Global time per line, excluding retrace. Might be approximate. Multiply with global resolution to get time in s. """ if 'ImgHdr_TimePerPixel' in self.tags: linetime = self.pixels_in_line * self.global_pixel_time elif self._info.lines > 0: linetime = self._info.line_time else: # point scan: line of one pixel linetime = 1e-3 / self.tags['MeasDesc_GlobalResolution'] return round(linetime) @property def global_frame_time(self) -> int: """Global time per image, line, or point scan cycle. Multiply with global resolution to get time in s. """ if self.tags['Measurement_SubMode'] == 3: # image, including retrace if self._info.frames == 0: return self._info.acquisition_time return self._info.acquisition_time // self._info.frames if self._info.lines > 0: # line scan return self._info.line_time # point scan return self.pixels_in_frame * self.global_pixel_time @property def global_acquisition_time(self) -> int: """Global time of acquisition.""" # MeasDesc_AcquisitionTime not reliable # aqt = self.tags.get('MeasDesc_AcquisitionTime', 0.0) # if aqt > 0: # return round(1e-3 * aqt / self.global_resolution) return self._info.acquisition_time @property def pixels_in_frame(self) -> int: """Number of pixels in one scan cycle.""" return self.lines_in_frame * self.pixels_in_line @property def pixels_in_line(self) -> int: """Number of pixels in line.""" ndim = self.measurement_ndim if ndim == 3: # image pixels = self.tags['ImgHdr_PixX'] elif ndim == 2: # line scan time_per_pixel = self.tags['ImgHdr_TimePerPixel'] line_frequency = self.tags['ImgHdr_LineFrequency'] if time_per_pixel > 0.0 and line_frequency > 0.0: pixels = round(1e-3 / (time_per_pixel * line_frequency)) else: pixels = 1 else: pixels = 1 return max(1, int(pixels)) @property def lines_in_frame(self) -> int: """Number of lines in frame.""" if self.measurement_ndim == 3: # TODO: Warning from the PicoQuant documentation: # Attention, in some images one will find a different number # of lines than defined by PixY (less or more, even different # in every frame), so do not trust this value. return max(1, int(self.tags['ImgHdr_PixY'])) return 1 @property def pixel_time(self) -> float: """Time per pixel in s.""" pixel_time = float(self.tags.get('ImgHdr_TimePerPixel', 0.0)) if pixel_time > 0.0: return pixel_time * 1e-3 # ms to s return self.global_pixel_time * self.global_resolution @property def line_time(self) -> float: """Average time between line markers or ``pixel_time`` in s.""" return self.global_line_time * self.global_resolution @property def frame_time(self) -> float: """Time per image, line, or point scan cycle in s. Image scan times include retrace. """ return self.global_frame_time * self.global_resolution @property def acquisition_time(self) -> float: """Duration of acquisition in s.""" # MeasDesc_AcquisitionTime not reliable # if 'MeasDesc_AcquisitionTime' in self.tags: # return self.tags['MeasDesc_AcquisitionTime'] * 1e-3 return self._info.acquisition_time * self.global_resolution @property def frequency(self) -> float: """Repetition frequency in Hz. The inverse of :py:attr:`PtuFile.global_resolution`. """ period = float(self.tags.get('MeasDesc_GlobalResolution', 0.0)) return 1.0 / period if period > 1e-14 else 0.0 @property def syncrate(self) -> int: """Sync events per s as recorded at beginning of measurement.""" return int(self.tags['TTResult_SyncRate']) @property def is_image(self) -> bool: """File contains image data.""" return ( self.tags['Measurement_SubMode'] == 3 and self.tags.get('ImgHdr_Dimensions', 1) == 3 and 'ImgHdr_PixX' in self.tags # some Leica PTU are missing this ) @property def is_t3(self) -> bool: """File contains T3 records.""" return bool(self.tags['Measurement_Mode'] == 3) # return self.tags['TTResultFormat_TTTRRecType'] in { # 0x00010303, 0x00010304, 0x01010304, # 0x00010305, 0x00010306, 0x00010307, # } @property def is_bidirectional(self) -> bool: """Bidirectional scan mode.""" return bool(self.tags.get('ImgHdr_BiDirect', 0) > 0) @property def is_sinusoidal(self) -> bool: """Sinusoidal scan mode.""" return bool(self.tags.get('ImgHdr_SinCorrection', 0) != 0) @property def use_xarray(self) -> bool: """Slicing and decode methods return ``xarray.DataArray``.""" return self._asxarray @use_xarray.setter def use_xarray(self, value: bool, /) -> None: self._asxarray = bool(value) @property def cache_records(self) -> bool: """Cache records read from file in memory.""" return self._cache @cache_records.setter def cache_records(self, value: bool, /) -> None: self._cache = bool(value) if not self._cache: del self._records # close numpy.memmap file handle self._records = None @property def dtype(self) -> numpy.dtype[Any]: """Data type of image histogram array.""" return self._dtype @dtype.setter def dtype(self, dtype: DTypeLike | None, /) -> None: dtype = numpy.dtype('uint16' if dtype is None else dtype) if dtype.kind != 'u': msg = f'{dtype=!r} not an unsigned integer' raise ValueError(msg) self._dtype = dtype @cached_property def shape(self) -> tuple[int, ...]: """Shape of image histogram array.""" if not self.is_t3: return () if 'C' in self._trimdims: nchannels = max(self.number_channels, 1) else: nchannels = self.number_channels_max if 'H' in self._trimdims: nbins = self.number_bins else: nbins = self.number_bins_max ndim = self.measurement_ndim if ndim == 3: return ( max(self._info.frames, 1), self.lines_in_frame, self.pixels_in_line, nchannels, nbins, ) if ndim == 2: return ( max(self._info.lines, 1), self.pixels_in_line, nchannels, nbins, ) if ndim in {0, 1}: return ( max(1, self._info.photons // self.global_pixel_time), nchannels, nbins, ) return () @cached_property def dims(self) -> tuple[str, ...]: """Axes labels for each dimension in image histogram array.""" if not self.shape: return () ndim = self.measurement_ndim if ndim == 3: return ('T', 'Y', 'X', 'C', 'H') if ndim == 2: return ('T', 'X', 'C', 'H') return ('T', 'C', 'H') @property def sizes(self) -> dict[str, int]: """Map dimension names to lengths.""" return dict(zip(self.dims, self.shape, strict=True)) @property def ndims(self) -> int: """Number of dimensions in image histogram array.""" return len(self.dims) @property def nbytes(self) -> int: """Number of bytes consumed by image histogram.""" size = 1 for i in self.shape: size *= int(i) return size * self._dtype.itemsize @property def size(self) -> int: """Number of elements in image histogram.""" size = 1 for i in self.shape: size *= int(i) return size @property def itemsize(self) -> int: """Length of one array element in image histogram in bytes.""" return self._dtype.itemsize @property def _coords_c(self) -> NDArray[Any]: """Coordinate array labelling all channels.""" if 'C' in self._trimdims: return numpy.arange( self._info.channels_active_first, self._info.channels_active_last + 1, dtype=numpy.uint8, ) return numpy.arange(self._info.channels, dtype=numpy.uint8) @property def _coords_h(self) -> NDArray[Any]: """Coordinate array labelling all delay-time bins.""" if 'H' in self._trimdims: nbins = self.number_bins else: nbins = self.number_bins_max return numpy.linspace( 0, nbins * self.tags['MeasDesc_Resolution'], nbins, endpoint=False ) @cached_property def coords(self) -> dict[str, NDArray[Any]]: """Coordinate arrays labelling each point in image histogram array. Coordinates for the time axis are approximate. Exact coordinates are returned with :py:meth:`PtuFile.decode_image` as ``xarray.DataArray``. """ if not self.shape: return {} ndim = self.measurement_ndim coords = {} shape = self.shape # exact time coordinates must be decoded from records coords['T'] = numpy.linspace( 0, shape[0] * self.frame_time, shape[0], endpoint=False ) res = self.tags.get('ImgHdr_PixResol', None) if res is not None: res *= 1e-6 # um if ndim > 2: offset = self.tags.get('ImgHdr_Y0', 0.0) * 1e-6 # um coords['Y'] = numpy.linspace( offset, offset + shape[-4] * res, shape[-4], endpoint=False ) if ndim > 1: offset = self.tags.get('ImgHdr_X0', 0.0) * 1e-6 coords['X'] = numpy.linspace( offset, offset + shape[-3] * res, shape[-3], endpoint=False ) coords['C'] = self._coords_c coords['H'] = self._coords_h return coords @override @property def attrs(self) -> dict[str, Any]: """Selected metadata as dict.""" return { **super().attrs, 'acquisition_time': self.acquisition_time, 'active_channels': self.active_channels, 'frame_time': self.frame_time, 'frequency': self.frequency, 'global_acquisition_time': self.global_acquisition_time, 'global_frame_time': self.global_frame_time, 'global_line_time': self.global_line_time, 'global_pixel_time': self.global_pixel_time, 'global_resolution': self.global_resolution, 'line_time': self.line_time, 'max_delaytime': self.number_bins_max, # for PhasorPy 'measurement_mode': self.measurement_mode.name, 'measurement_submode': self.measurement_submode.name, 'number_bins': self.number_bins, 'number_bins_in_period': self.number_bins_in_period, 'number_bins_max': self.number_bins_max, 'pixel_time': self.pixel_time, 'record_type': self.record_type.name, 'scanner': None if self.scanner is None else self.scanner.name, 'syncrate': self.syncrate, 'tcspc_resolution': self.tcspc_resolution, } @cached_property def _info(self) -> PtuInfo: """Information about decoded records.""" from ._ptufile import decode_info lines_in_frame = 0 if ( 'T' in self._trimdims and 'ImgHdr_PixY' in self.tags and self.tags['Measurement_SubMode'] == 3 ): lines_in_frame = max(1, self.tags['ImgHdr_PixY']) return PtuInfo( *decode_info( self.read_records(), self.tags['TTResultFormat_TTTRRecType'], self.line_start_mask, self.line_stop_mask, self.frame_change_mask, lines_in_frame, ) ) def read_records( self, *, memmap: bool | Literal['r', 'r+', 'c'] = False, ) -> NDArray[numpy.uint32]: """Return encoded TTTR records from file. Records are cached depending on the :py:attr:`PtuFile.cache_records` property. Parameters: memmap: Memory-map records in file using specified mode. If false (default), read records from file into main memory. """ if self._cache and self._records is not None: return self._records if self.tags['TTResultFormat_BitsPerRecord'] not in {0, 32}: msg = f"invalid {self.tags['TTResultFormat_BitsPerRecord']=}" raise ValueError(msg) count = self.number_records records: NDArray[numpy.uint32] if memmap: if memmap is True: memmap = 'r' elif memmap not in {'r', 'r+', 'c'}: msg = f'invalid memmap mode={memmap=!r}' raise ValueError(msg) records = numpy.memmap( self._fh, dtype=numpy.uint32, mode=memmap, offset=self._data_offset, shape=(count,), ) else: records = numpy.empty(count, numpy.uint32) self._fh.seek(self._data_offset) n = self._fh.readinto(records) # type: ignore[attr-defined] if n != count * 4: logger().error( f'{self!r} expected {count} records, got {n // 4}' ) records = records[: n // 4] if self._cache: self._records = records return records def decode_records( self, records: NDArray[numpy.uint32] | None = None, /, *, out: OutputType = None, ) -> NDArray[Any]: """Return decoded TTTR records. Parameters: records: Encoded TTTR records. By default, read records from file. out: Array where decoded records are stored. If ``None``, create a new NumPy recarray in main memory. If ``'memmap'``, create a memory-mapped recarray in a temporary file. If a ``numpy.ndarray``, a writable recarray of compatible shape and dtype. If a ``file name`` or ``open file``, create a memory-mapped array in the specified file. Returns: : ``numpy.recarray`` of size :py:attr:`number_records` and dtype :py:attr:`T3_RECORD_DTYPE` or :py:attr:`T2_RECORD_DTYPE`. A channel >= 0 indicates that a record contains a photon. Otherwise, the record contains an overflow event or marker > 0. """ from ._ptufile import decode_t2_records, decode_t3_records if records is None: records = self.read_records() rectype = self.tags['TTResultFormat_TTTRRecType'] if self.is_t3: result = create_output( out, (records.size,), T3_RECORD_DTYPE, fillvalue=0 ) decode_t3_records(result, records, rectype) else: result = create_output( out, (records.size,), T2_RECORD_DTYPE, fillvalue=0 ) decode_t2_records(result, records, rectype) return result @overload def decode_histogram( self, /, *, records: NDArray[numpy.uint32] | None = None, dtype: DTypeLike | None = None, sampling_time: int | None = None, dtime: int | None = None, asxarray: Literal[False] = ..., out: OutputType = None, ) -> NDArray[Any]: ... @overload def decode_histogram( self, /, *, records: NDArray[numpy.uint32] | None = None, dtype: DTypeLike | None = None, sampling_time: int | None = None, dtime: int | None = None, asxarray: Literal[True] = ..., out: OutputType = None, ) -> DataArray: ... @overload def decode_histogram( self, /, *, records: NDArray[numpy.uint32] | None = None, dtype: DTypeLike | None = None, sampling_time: int | None = None, dtime: int | None = None, asxarray: bool = ..., out: OutputType = None, ) -> NDArray[Any] | DataArray: ... def decode_histogram( self, /, *, records: NDArray[numpy.uint32] | None = None, dtype: DTypeLike | None = None, sampling_time: int | None = None, dtime: int | None = None, asxarray: bool = False, out: OutputType = None, ) -> NDArray[Any] | DataArray: """Return histogram of all photons by channel. Parameters: records: Encoded TTTR records. By default, read records from file. dtype: Unsigned integer type of histogram array. The default is ``uint32`` for T3, else ``uint16``. Increase the bit depth to avoid overflows. sampling_time: Global time per sample for T2 mode. The default is :py:meth:`PtuFile.global_pixel_time`. dtime: Number of bins in histogram. If 0, return :py:attr:`number_bins_in_period` bins. If > 0, return up to specified bin. asxarray: If true, return ``xarray.DataArray``, else ``numpy.ndarray`` (default). out: Array where decoded histogram is stored. If ``None``, create a new NumPy array in main memory. If ``'memmap'``, create a memory-mapped array in a temporary file. If a ``numpy.ndarray``, a writable, initialized array of :py:attr:`shape` and unsigned integer dtype. If a ``file name`` or ``open file``, create a memory-mapped array in the specified file. Returns: : Decoded TTTR T3 records as 2-dimensional histogram array: - ``'C'`` channel - ``'H'`` histogram bins """ from ._ptufile import decode_t2_histogram, decode_t3_histogram if dtype is None: dtype = numpy.uint32 if self.is_t3 else numpy.uint16 dtype = numpy.dtype(dtype) if dtype.kind != 'u': msg = f'not an unsigned integer {dtype=!r}' raise ValueError(msg) if records is None: records = self.read_records() rectype = self.tags['TTResultFormat_TTTRRecType'] if 'C' in self._trimdims: first_channel = self._info.channels_active_first else: first_channel = 0 if self.is_t3: if dtime is None: nbins = self.shape[-1] elif dtime == 0: nbins = self.number_bins_in_period elif dtime > 0: nbins = dtime else: msg = f'{dtime=} < 0' raise ValueError(msg) histogram = create_output( out, (self.shape[-2], nbins), dtype, fillvalue=0 ) decode_t3_histogram(histogram, records, rectype, first_channel) coords = numpy.linspace( 0, histogram.shape[-1] * self.tags['MeasDesc_Resolution'], histogram.shape[-1], endpoint=False, ) else: if sampling_time is None or sampling_time <= 0: sampling_time = self.global_pixel_time histogram = create_output( out, ( self.number_channels, max(1, self.global_acquisition_time // sampling_time), ), dtype, fillvalue=0, ) decode_t2_histogram( histogram, records, rectype, sampling_time, first_channel ) coords = numpy.linspace( 0, self.acquisition_time, histogram.shape[1], endpoint=False ) if not self._asxarray and not asxarray: return histogram from xarray import DataArray return DataArray( histogram, dims=('C', 'H'), coords={'C': self._coords_c, 'H': coords}, # name=self.name attrs=self.attrs, ) @overload def decode_image( self, selection: Sequence[int | slice | EllipsisType | None] | None = None, /, *, records: NDArray[numpy.uint32] | None = None, dtype: DTypeLike | None = None, frame: int | None = None, channel: int | None = None, dtime: int | None = None, pixel_time: float | None = None, bishift: int | None = None, keepdims: bool = True, asxarray: Literal[False] = ..., out: OutputType = None, ) -> NDArray[Any]: ... @overload def decode_image( self, selection: Sequence[int | slice | EllipsisType | None] | None = None, /, *, records: NDArray[numpy.uint32] | None = None, dtype: DTypeLike | None = None, frame: int | None = None, channel: int | None = None, dtime: int | None = None, pixel_time: float | None = None, bishift: int | None = None, keepdims: bool = True, asxarray: Literal[True] = ..., out: OutputType = None, ) -> DataArray: ... @overload def decode_image( self, selection: Sequence[int | slice | EllipsisType | None] | None = None, /, *, records: NDArray[numpy.uint32] | None = None, dtype: DTypeLike | None = None, frame: int | None = None, channel: int | None = None, dtime: int | None = None, pixel_time: float | None = None, bishift: int | None = None, keepdims: bool = True, asxarray: bool = ..., out: OutputType = None, ) -> NDArray[Any] | DataArray: ... def decode_image( self, selection: Sequence[int | slice | EllipsisType | None] | None = None, /, *, records: NDArray[numpy.uint32] | None = None, dtype: DTypeLike | None = None, frame: int | None = None, channel: int | None = None, dtime: int | None = None, pixel_time: float | None = None, bishift: int | None = None, keepdims: bool = True, asxarray: bool = False, out: OutputType = None, ) -> NDArray[Any] | DataArray: """Return T3 mode point, line, or image histogram. The histogram may not include photons counted during incomplete frame scans or during line retraces. Parameters: selection: Indices for all dimensions: - ``None``: return all items along axis (default). - ``Ellipsis``: return all items along multiple axes. - ``int``: return single item along axis. - ``slice``: return chunk of axis. ``slice.step`` is binning factor. If ``slice.step=-1``, integrate all items along axis. records: Encoded TTTR records. By default, read records from file. dtype: Unsigned integer type of image histogram array. The default is ``uint16``. Increase the bit depth to avoid overflows when integrating. frame: If < 0, integrate time axis, else return specified frame. Overrides ``selection`` for axis ``T``. channel: If < 0, integrate channel axis, else return specified channel. Overrides ``selection`` for axis ``C``. dtime: Number of bins in image histogram. If 0, return :py:attr:`number_bins_in_period` bins. If < 0, integrate delay time axis. If > 0, return up to specified bin. Overrides ``selection`` for axis ``H``. pixel_time: float, optional Time per pixel in s. If zero, determine pixel times per line from line scan markers (cannot be used with bidirectional or line scans). The default is :py:attr:`PtuFile.pixel_time`. bishift: Global time shift of odd vs. even lines in bidirectional mode. The default is zero. Positive shifts invalidate left odd columns, while negative shifts invalidate right odd columns. keepdims: If true (default), reduced axes are left as size-one dimension. asxarray: If true, return ``xarray.DataArray``, else ``numpy.ndarray`` (default). out: Array where decoded image histogram is stored. If ``None``, create a new NumPy array in main memory. If ``'memmap'``, create a memory-mapped array in a temporary file. If a ``numpy.ndarray``, a writable, initialized array of :py:attr:`shape` and unsigned integer dtype. If a ``file name`` or ``open file``, create a memory-mapped array in the specified file. Returns: image: Decoded TTTR T3 records as up to 5-dimensional image array: - ``'T'`` time/frame - ``'Y'`` slow scan axis for image scans - ``'X'`` fast scan axis for line and image scans - ``'C'`` channel - ``'H'`` histogram bins Raises: NotImplementedError: T2 images, bidirectional sinusoidal scanning, and deprecated image reconstruction are not supported. IndexError: Selection is out of bounds. """ # TODO: support ReqHdr_ScanningPattern = 1, bidirectional per frame if not self.is_t3: # TODO: T2 images msg = 'not a T3 image' raise NotImplementedError(msg) if self.is_bidirectional and not self.is_image: msg = 'bidirectional scanning only supported for images' raise NotImplementedError(msg) if self.is_image and 'ImgHdr_LineStart' not in self.tags: # TODO: deprecated image reconstruction using # ImgHdr_PixResol, ImgHdr_TStartTo, ImgHdr_TStopTo, # ImgHdr_TStartFro, ImgHdr_TStopFro msg = 'old-style image reconstruction' raise NotImplementedError(msg) shape = list(self.shape) ndim = len(shape) keepaxes: list[slice | int] = [slice(None)] * ndim if selection is None: selection = [None] * ndim else: try: len(selection) except TypeError: selection = [selection] # type: ignore[list-item] if len(selection) > ndim: msg = f'too many indices in {selection=}' raise IndexError(msg) if len(selection) == ndim: selection = list(selection).copy() if Ellipsis in selection: selection[selection.index(Ellipsis)] = None # elif len(selection) < ndim: elif Ellipsis in selection: selection = list(selection).copy() i = selection.index(Ellipsis) selection = ( selection[:i] + ([None] * (1 + ndim - len(selection))) + selection[i + 1 :] ) else: selection = list(selection) + [None] * (ndim - len(selection)) if Ellipsis in selection: msg = f'more than one Ellipsis in {selection=}' raise IndexError(msg) if frame is not None: if frame >= shape[0]: msg = f'{frame=} out of range' raise IndexError(msg) selection[0] = frame if frame >= 0 else slice(None, None, -1) if channel is not None: if channel >= shape[-2]: msg = f'{channel=} out of range' raise IndexError(msg) selection[-2] = channel if channel >= 0 else slice(None, None, -1) if dtime is not None: if dtime == 0: dtime = self.number_bins_in_period if dtime > 0: if dtime > self.number_bins_max: msg = f'{dtime=} out of range {self.number_bins_max}' raise IndexError(msg) selection[-1] = slice(0, dtime, 1) shape[-1] = dtime else: selection[-1] = slice(None, None, -1) start = [0] * ndim step = [1] * ndim for i, (index, size) in enumerate( zip(selection, self.shape, strict=True) ): if index is None: pass elif isinstance(index, int): idx = index if idx < 0: idx %= shape[i] if not 0 <= idx < size: msg = f'axis {i} {idx=} out of range [0, {size}]' raise IndexError(msg) start[i] = idx shape[i] = 1 keepaxes[i] = 0 elif isinstance(index, slice): istart = index.start if istart is not None: if istart < 0: istart %= shape[i] if not 0 <= istart < shape[i]: msg = f'axis {i} {index=} start out of range' raise IndexError(msg) start[i] = istart if index.stop is not None: istop = index.stop if istop < 0: istop %= shape[i] if not start[i] < istop <= shape[i]: msg = f'axis {i} {index=} stop out of range' raise IndexError(msg) shape[i] = istop shape[i] -= start[i] if index.step is not None: if index.step == 0: msg = f'axis {i} slice step cannot be zero' raise IndexError(msg) if index.step < 0: # negative step size -> integrate all step[i] = shape[i] keepaxes[i] = 0 else: step[i] = min(index.step, shape[i]) shape[i] = shape[i] // step[i] + min(1, shape[i] % step[i]) else: msg = f'axis {i} index type {type(index)!r} invalid' raise IndexError(msg) if self._info.channels_active_first > 0 and 'C' in self._trimdims: # set channel offset start[-2] += self._info.channels_active_first if pixel_time is None: global_pixel_time = self.global_pixel_time global_line_time = self.global_line_time elif pixel_time <= 0.0: global_pixel_time = 0 global_line_time = 0 if self.is_sinusoidal: msg = f'cannot use sinusoidal correction with {pixel_time=}' raise ValueError(msg) if ndim == 4: msg = f'cannot decode line scan with {pixel_time=}' raise ValueError(msg) else: global_pixel_time = max( 1, round(pixel_time / self.global_resolution) ) global_line_time = global_pixel_time * self.pixels_in_line if self.is_sinusoidal: pixel_at_time = sinusoidal_correction( self.tags['ImgHdr_SinCorrection'], global_line_time, self.pixels_in_line, dtype=numpy.uint16, # should be enough for pixels_in_line ) else: pixel_at_time = None if dtype is None: dtype = self._dtype else: dtype = numpy.dtype(dtype) if dtype.kind != 'u': msg = f'not an unsigned integer {dtype=!r}' raise ValueError(msg) histogram = create_output(out, shape, dtype, fillvalue=0) times = numpy.zeros(shape[0], numpy.uint64) from ._ptufile import decode_t3_image, decode_t3_line, decode_t3_point if records is None: records = self.read_records() if ndim == 5: if ( self.line_start_mask == 0 # noqa: PLR1714 or self.line_stop_mask == 0 or self.frame_change_mask == 0 or self.line_stop_mask == self.line_start_mask or self.frame_change_mask == self.line_start_mask or self.frame_change_mask == self.line_stop_mask ): logger().error( 'invalid line_start, line_stop, or frame_change masks (' f'0b{self.line_start_mask:b}, ' f'0b{self.line_stop_mask:b}, ' f'0b{self.frame_change_mask:b})' ) decode_t3_image( histogram, times, records, self.tags['TTResultFormat_TTTRRecType'], self.pixels_in_line, global_pixel_time, global_line_time, pixel_at_time if self.is_sinusoidal else None, self.line_start_mask, self.line_stop_mask, self.frame_change_mask, *start, *step, 0 if bishift is None else bishift, self.is_bidirectional, self.is_sinusoidal, self._info.skip_first_frame, ) elif ndim == 4: # not tested if ( self.line_start_mask == 0 # noqa: PLR1714 or self.line_stop_mask == 0 or self.line_stop_mask == self.line_start_mask ): logger().error( 'invalid line_start or line_stop masks (' f'0b{self.line_start_mask:b}, 0b{self.line_stop_mask:b})' ) decode_t3_line( histogram, times, records, self.tags['TTResultFormat_TTTRRecType'], global_pixel_time, self.line_start_mask, self.line_stop_mask, *start, *step, ) elif ndim == 3: decode_t3_point( histogram, times, records, self.tags['TTResultFormat_TTTRRecType'], global_pixel_time, *start, *step, ) if not keepdims: histogram = histogram[tuple(keepaxes)] if not self._asxarray and not asxarray: return histogram from xarray import DataArray if self._info.channels_active_first > 0 and 'C' in self._trimdims: # unset channel offset start[-2] -= self._info.channels_active_first dims = [] coords = self.coords.copy() for i, ax in enumerate(self.dims): if keepdims or keepaxes[i] != 0: dims.append(ax) if ax in coords: index = slice( start[i], start[i] + shape[i] * step[i], step[i] ) coords[ax] = coords[ax][index] elif ax in coords: del coords[ax] if 'H' in dims and len(coords['H']) < shape[-1]: coords['H'] = numpy.linspace( 0, shape[-1] * coords['H'][1], shape[-1], endpoint=False, ) if 'T' in dims: coords['T'] = times * self.global_resolution return DataArray( histogram, dims=dims, coords=coords, attrs=self.attrs, # name=self.name, ) def plot( self, *, samples: int | None = None, frame: int | None = None, channel: int | None = None, dtime: int | None = -1, verbose: bool = False, show: bool = True, **kwargs: Any, ) -> None: """Plot histograms using matplotlib. Parameters: samples: Number of bins along measurement for T2 mode. The default is 1000. frame: If < 0, integrate time axis, else show specified frame. By default, all frames are shown. Applies to T3 images. channel: If < 0, integrate channel axis, else show specified channel. By default, all channels are shown. Applies to T3 images. dtime: Number of bins in T3 histograms. If < 0 (default), integrate delay time axis of images. If 0, show :py:attr:`number_bins_in_period` bins. If > 0, show histograms up to specified bin. If None, show all bins. verbose: Print information about histogram arrays. show: If true (default), display all figures. Else, defer to user or environment to display figures. **kwargs: Additional arguments passed to ``tifffile.imshow``. """ from matplotlib import pyplot from tifffile import Timer, imshow t = Timer() if self.is_t3: if self.measurement_ndim > 1: t.start() histogram: Any = self.decode_image( frame=frame, channel=channel, dtime=dtime, asxarray=True ) if verbose: print() t.print('decode_image') print() print(histogram.squeeze()) imshow( numpy.transpose( histogram.values, (0, 3, 4, 1, 2) ).squeeze(), title=repr(self), photometric='minisblack', **kwargs, ) pyplot.figure() t.start() # histogram = histogram.sum(axis=(0, 1, 2), dtype=numpy.uint32) dtime = None if dtime is None or dtime < 0 else dtime histogram = self.decode_histogram(asxarray=True, dtime=dtime) else: if samples is None or samples < 1: samples = 1000 histogram = self.decode_histogram( sampling_time=self.global_acquisition_time // samples, asxarray=True, ) if verbose: print() t.print('decode_histogram') print() print(histogram) channels = histogram.coords['C'].values for i, hist in enumerate(histogram): pyplot.plot( hist.coords['H'], hist.values, label=f'ch {channels[i]}' ) if self.frequency > 0.0: pyplot.axvline(x=1 / self.frequency, color='0.5', ls=':', lw=0.75) pyplot.title(repr(self)) pyplot.xlabel('delay time [s]' if self.is_t3 else 'time [s]') pyplot.ylabel('photon count') pyplot.legend() if show: pyplot.show() class PhuMeasurementMode(enum.IntEnum): """Kind of TCSPC measurement (Measurement_Mode tag).""" UNKNOWN = -1 """Unknown mode.""" HISTOGRAM = 0 """Histogram mode.""" CONTI = 8 """Conti mode.""" @classmethod def _missing_(cls, value: object) -> object: if not isinstance(value, int): return None obj = cls(-1) # Unknown obj._value_ = value return obj class PhuMeasurementSubMode(enum.IntEnum): """Kind of measurement (Measurement_SubMode tag).""" UNKNOWN = -1 """Unknown mode.""" OSCILLOSCOPE = 0 """Oscilloscope mode.""" INTEGRATING = 1 """Integrating mode.""" TRES = 2 """Time-Resolved Emission Spectra mode.""" SEQ = 3 """Sequence mode.""" @classmethod def _missing_(cls, value: object) -> object: if not isinstance(value, int): return None obj = cls(-1) obj._value_ = value return obj class PtuMeasurementMode(enum.IntEnum): """Kind of TCSPC Measurement (Measurement_Mode tag).""" UNKNOWN = -1 """Unknown mode.""" T2 = 2 """T2 mode.""" T3 = 3 """T3 mode.""" @classmethod def _missing_(cls, value: object) -> object: if not isinstance(value, int): return None obj = cls(-1) # Unknown obj._value_ = value return obj class PtuMeasurementSubMode(enum.IntEnum): """Kind of measurement (Measurement_SubMode tag).""" UNKNOWN = -1 """Unknown mode.""" POINT = 1 """Point scan mode.""" LINE = 2 """Line scan mode.""" IMAGE = 3 """Image scan mode.""" @classmethod def _missing_(cls, value: object) -> object: if not isinstance(value, int): return None obj = cls(1 if value == 0 else -1) obj._value_ = value return obj class PtuScannerType(enum.IntEnum): """Scanner hardware (ImgHdr_Ident tag).""" UNKNOWN = -1 """Unknown scanner.""" PI_E710 = 1 """PI E-710 scanner.""" LSM = 3 """PicoQuant LSM scanner.""" PI_LINEWBS = 5 """PI Line WB scanner.""" PI_E725 = 6 """PI E-725 scanner.""" PI_E727 = 7 """PI E-727 scanner.""" MCL = 8 """MCL scanner.""" FLIMBEE = 9 """PicoQuant FLIMBee scanner.""" SCANBOX = 10 """Zeiss ScanBox scanner.""" @classmethod def _missing_(cls, value: object) -> object: if not isinstance(value, int): return None obj = cls(-1) # Unknown obj._value_ = value return obj class PtuScanDirection(enum.IntEnum): """Scan direction (ImgHdr_ScanDirection tag).""" XY = 0 """X-Y scan.""" XZ = 1 """X-Z scan.""" YZ = 2 """Y-Z scan.""" @classmethod def _missing_(cls, value: object) -> object: if not isinstance(value, int) or value != 0: return None obj = cls(0) # XY obj._value_ = value return obj class PtuStopReason(enum.IntEnum): """Reason for measurement end (TTResult_StopReason tag).""" TIME_OVER = 0 MANUAL = 1 OVERFLOW = 2 ERROR = 3 UNKNOWN = -1 FIFO_OVERRUN = -2 LEGACY_ERROR = -3 TCSPC_ERROR = -4 FILE_ERROR = -5 OUT_OF_MEMORY = -6 SUSPENDED = -7 SYS_ERROR = -8 QUEUE_OVERRUN = -9 DATA_XFER_FAIL = -10 DATA_CHECK_FAIL = -11 REF_CLK_LOST = -12 SYNC_LOST = -13 @classmethod def _missing_(cls, value: object) -> object: if not isinstance(value, int): return None obj = cls(-1) # Unknown obj._value_ = value return obj class PtuMeasurementWarnings(enum.IntFlag): """Warnings during measurement (TTResult_MDescWarningFlags tag).""" SYNC_RATE_ZERO = 0x1 SYNC_RATE_TOO_LOW = 0x2 SYNC_RATE_TOO_HIGH = 0x4 INPT_RATE_ZERO = 0x10 INPT_RATE_TOO_HIGH = 0x40 EVENTS_DROPPED = 0x80 INPT_RATE_RATIO = 0x100 DIVIDER_GT_ONE = 0x200 TIME_SPAN_TOO_SMALL = 0x400 OFFSET_UNNECESSARY = 0x800 class PtuHwFeatures(enum.IntFlag): """Hardware features (HW_Features tag).""" DLL = 0x1 TTTR = 0x2 MARKERS = 0x4 LOW_RES = 0x8 TRIG_OUT = 0x10 PROG_DEADTIME = 0x20 EXT_FPGA = 0x40 PROG_HYSTERESES = 0x80 COINCIDENCE_FILTERING = 0x100 INPUT_MODES = 0x200 class PqTagType(enum.IntEnum): """Tag type definition.""" Empty8 = 0xFFFF0008 Bool8 = 0x00000008 Int8 = 0x10000008 BitSet64 = 0x11000008 Color8 = 0x12000008 Float8 = 0x20000008 TDateTime = 0x21000008 Float8Array = 0x2001FFFF AnsiString = 0x4001FFFF WideString = 0x4002FFFF BinaryBlob = 0xFFFFFFFF class PtuRecordType(enum.IntEnum): """TTTR record type.""" PicoHarpT3 = 0x00010303 """PicoHarp 300 T3.""" PicoHarpT2 = 0x00010203 """PicoHarp 300 T2.""" HydraHarpT3 = 0x00010304 """HydraHarp V1.x T3.""" HydraHarpT2 = 0x00010204 """HydraHarp V1.x T2.""" HydraHarp2T3 = 0x01010304 """HydraHarp V2.x T3.""" HydraHarp2T2 = 0x01010204 """HydraHarp V2.x T2.""" TimeHarp260NT3 = 0x00010305 """TimeHarp 260N T3.""" TimeHarp260NT2 = 0x00010205 """TimeHarp 260N T2.""" TimeHarp260PT3 = 0x00010306 """TimeHarp 260P T3.""" TimeHarp260PT2 = 0x00010206 """TimeHarp 260P T2.""" GenericT2 = 0x00010207 """MultiHarp and Picoharp 330 T2.""" GenericT3 = 0x00010307 """MultiHarp and Picoharp 330 T3.""" @dataclasses.dataclass class PtuInfo: """Information about decoded TTTR records. Returned by ``_ptufile.decode_info``. """ format: int """Type of records.""" records: int """Number of records.""" photons: int """Number of photons counted.""" markers: int """Number of marker events.""" frames: int """Number of frames detected. May exclude incomplete frames.""" lines: int """Number of lines between line markers.""" channels: int """Maximum number of channels for record type.""" channels_active: int """Bitfield identifying channels with photons.""" channels_active_first: int """First channel with photons.""" channels_active_last: int """Last channel with photons.""" bins: int """Maximum delay time for record type.""" bins_used: int """Number of bins up to the largest occupied delay-time bin.""" skip_first_frame: bool """First frame of multi-frame image is incomplete.""" skip_last_frame: bool """Last frame of multi-frame image is incomplete.""" line_time: int """Average global time between line markers.""" acquisition_time: int """Global time of last sync event.""" def __str__(self) -> str: return indent( f'{self.__class__.__name__}(', *(f'{key}={value},' for key, value in self.__dict__.items()), end='\n)', ) T2_RECORD_DTYPE = numpy.dtype( [ ('time', numpy.uint64), ('channel', numpy.int8), ('marker', numpy.uint8), ] ) """Numpy dtype of decoded T2 records.""" T3_RECORD_DTYPE = numpy.dtype( [ ('time', numpy.uint64), ('dtime', numpy.int16), ('channel', numpy.int8), ('marker', numpy.uint8), ] ) """Numpy dtype of decoded T3 records.""" FILE_EXTENSIONS = { '.ptu': PqFileType.PTU, '.phu': PqFileType.PHU, '.pck': PqFileType.PCK, '.pco': PqFileType.PCO, '.pfs': PqFileType.PFS, '.pus': PqFileType.PFS, '.pqres': PqFileType.PQRES, '.pqdat': PqFileType.PQDAT, '.pquni': PqFileType.PQUNI, '.spqr': PqFileType.SPQR, } """File extensions of PicoQuant tagged files.""" def binwrite( filename: str | os.PathLike[str], data: ArrayLike, /, tcspc_resolution: float, pixel_resolution: float, ) -> None: """Write TCSPC image histogram and metadata to PicoQuant BIN file. Parameters: filename: Name of PicoQuant BIN file. data: TCSPC image histogram array of shape (length, width, bins). Must be compatible with dtype.uint32. tcspc_resolution: TCSPC resolution in s. pixel_resolution: Pixel resolution in μm. """ data = numpy.asarray(data, ' tuple[NDArray[Any], dict[str, Any]]: """Return TCSPC image histogram and metadata from PicoQuant BIN file. Parameters: filename: Name of PicoQuant BIN file. memmap: If true, return a read-only memory-mapped array. Returns: tuple: - histogram: TCSPC image histogram array of shape (length, width, bins). - metadata: Dictionary with metadata. - 'shape': Shape of histogram array. - 'pixel_resolution': Pixel resolution in μm. - 'tcspc_resolution': TCSPC resolution in s. """ data: NDArray[Any] with open(filename, 'rb') as fh: size_x, size_y, pixel_resolution, size_h, tcspc_resolution = ( struct.unpack(' bytes: """Return encoded PicoQuant tag. Parameters: tagid: Tag identifier string. value: Tag value to encode. index: Array index for tag values, -1 for single values. Returns: Encoded tag as bytes. """ match value: case None: typecode = PqTagType.Empty8 buffer = b'\x00\x00\x00\x00\x00\x00\x00\x00' case bool() as v: # must check bool before int typecode = PqTagType.Bool8 buffer = struct.pack(' NDArray[Any]: """Return pixel indices of global times in line for sinusoidal scanning. Parameters: sincorrect: Amount of sine wave used for measurement. Either percentage of amplitude (PicoQuant) or period (Leica) depending on `is_amplitude`. The value of the `ImgHdr_SinCorrection` tag. global_line_time: Global time per line. pixels_in_line: Number of pixels in line. is_amplitude: Correction value is percentage of amplitude of sine wave. If false, correction value is percentage of period of sine wave. Returns: Array of size `global_line_time`, mapping global time in line to pixel index in line. """ dtype = numpy.dtype(numpy.uint16 if dtype is None else dtype) if sincorrect <= 0.0 or sincorrect > 100.0: msg = f'{sincorrect=} out of range' raise ValueError(msg) if global_line_time < 2: msg = f'{global_line_time=} out of range' raise ValueError(msg) if pixels_in_line < 2 or pixels_in_line >= numpy.iinfo(dtype).max: msg = f'{pixels_in_line=} out of range' raise ValueError(msg) if not is_amplitude: sincorrect = math.sin(sincorrect * math.pi / 200.0) * 100.0 limit = math.asin(-sincorrect / 100.0) a = numpy.linspace(limit, -limit, global_line_time, endpoint=False) a = numpy.sin(a) a *= -0.5 * pixels_in_line / a[0] a -= a[0] return a.astype(dtype) def create_output( out: OutputType, /, shape: Sequence[int], dtype: DTypeLike | None, *, mode: Literal['r+', 'w+', 'r', 'c'] = 'w+', suffix: str | None = None, fillvalue: float | None = None, ) -> NDArray[Any] | numpy.memmap[Any, Any]: """Return NumPy array where data of shape and dtype can be copied. Parameters: out: Specifies kind of array of `shape` and `dtype` to return: `None`: Return new array. `numpy.ndarray`: Return view of existing array. `'memmap'` or `'memmap:tempdir'`: Return memory-map to array stored in temporary binary file. `str` or open file: Return memory-map to array stored in specified binary file. shape: Shape of array to return. dtype: Data type of array to return. If `out` is an existing array, `dtype` must be castable to its data type. mode: File mode to create memory-mapped array. The default is 'w+' to create new, or overwrite existing file for reading and writing. suffix: Suffix of `NamedTemporaryFile` if `out` is `'memmap'`. The default is '.memmap'. fillvalue: Value to initialize output array. By default, return uninitialized array. Returns: NumPy array or memory-mapped array of `shape` and `dtype`. Raises: ValueError: Existing array cannot be reshaped to `shape` or cast to `dtype`. """ shape = tuple(shape) dtype = numpy.dtype(dtype) if out is None: if fillvalue is None: return numpy.empty(shape, dtype) if fillvalue: return numpy.full(shape, fillvalue, dtype) return numpy.zeros(shape, dtype) if isinstance(out, numpy.ndarray): if product(shape) != product(out.shape): msg = f'cannot reshape {shape} to {out.shape}' raise ValueError(msg) if not numpy.can_cast(dtype, out.dtype): msg = f'cannot cast {dtype} to {out.dtype}' raise ValueError(msg) out = out.reshape(shape) if fillvalue is not None: out.fill(fillvalue) return out if isinstance(out, str) and out[:6] == 'memmap': import tempfile tempdir = out[7:] if len(out) > 7 else None if suffix is None: suffix = '.memmap' with tempfile.NamedTemporaryFile(dir=tempdir, suffix=suffix) as fh: out = numpy.memmap(fh, shape=shape, dtype=dtype, mode=mode) if fillvalue is not None: out.fill(fillvalue) return out out = numpy.memmap(out, shape=shape, dtype=dtype, mode=mode) if fillvalue is not None: out.fill(fillvalue) return out def product(iterable: Iterable[int], /) -> int: """Return product of integers. Like math.prod, but does not overflow with numpy arrays. """ prod = 1 for i in iterable: prod *= int(i) return prod def now() -> datetime: """Return current date and time.""" return datetime.now() def align_bytes(size: int, align: int, /) -> bytes: """Return trailing bytes to align bytes of size.""" size %= align return b'' if size == 0 else b'\0' * (align - size) def indent(*args: Any, sep: str = '', end: str = '') -> str: """Return joined string representations of objects with indented lines.""" text = (sep + '\n').join( arg if isinstance(arg, str) else repr(arg) for arg in args ) return ( '\n'.join( (' ' + line if line else line) for line in text.splitlines() if line )[4:] + end ) def logger() -> logging.Logger: """Return logger for ptufile module.""" return logging.getLogger('ptufile') def askopenfilename(**kwargs: Any) -> str: """Return file name(s) from Tkinter's file open dialog.""" from tkinter import Tk, filedialog root = Tk() root.withdraw() root.update() filenames = filedialog.askopenfilename(**kwargs) root.destroy() return filenames def main(argv: list[str] | None = None) -> int: """Command line usage main function. Preview image and metadata in specified files or all files in directory. ``python -m ptufile file_or_directory`` """ from glob import glob from tifffile import Timer if argv is None: argv = sys.argv fltr = False if len(argv) == 1: path = askopenfilename( title='Select a PTU file', filetypes=[ (f'{ext.upper()} files', f'*{ext}') for ext in FILE_EXTENSIONS ] + [('All files', '*')], ) files = [path] if path else [] elif '*' in argv[1]: files = glob(argv[1]) elif os.path.isdir(argv[1]): files = glob(f'{argv[1]}/*.p*') fltr = True else: files = argv[1:] for filename in files: if ( fltr and os.path.splitext(filename)[-1].lower() not in FILE_EXTENSIONS ): continue try: with PqFile(filename) as pq: if pq.type == PqFileType.PTU: t = Timer() with PtuFile(filename) as ptu: t.print(' open file') t.start() ptu.read_records() t.print('read records') t.start() ptu_info = ptu._info t.print('scan records') # t.start() # ptu.decode_records() # t.print('decode records') print() print(ptu) print(ptu_info) try: ptu.plot(verbose=True) except NotImplementedError as exc: print('NotImplementedError:', exc) elif pq.type == PqFileType.PHU: with PhuFile(filename) as phu: print(phu) phu.plot(verbose=True) else: print(pq) print() except Exception: import traceback print('Failed to read', filename) traceback.print_exc() print() continue return 0 if __name__ == '__main__': sys.exit(main()) cgohlke-ptufile-ab09357/ptufile/py.typed000066400000000000000000000000001515761367400203040ustar00rootroot00000000000000cgohlke-ptufile-ab09357/pyproject.toml000066400000000000000000000004521515761367400200640ustar00rootroot00000000000000[build-system] requires = ["setuptools", "numpy>=2", "Cython>=3.2"] build-backend = "setuptools.build_meta" [tool.black] line-length = 79 target-version = ["py312", "py313", "py314"] skip-string-normalization = true [tool.isort] known_first_party = ["ptufile"] profile = "black" line_length = 79 cgohlke-ptufile-ab09357/setup.py000066400000000000000000000147371515761367400166750ustar00rootroot00000000000000# ptufile/setup.py """Ptufile package Setuptools script.""" import os import re import sys import sysconfig import numpy from setuptools import Extension, setup DEBUG = bool(os.environ.get('CG_DEBUG', '')) LIMITED_API = os.environ.get('CG_LIMITED_API', '1').lower() in ('1', 'true') LLVM_PATH = os.environ.get('CG_LLVM_PATH', '') if LLVM_PATH: # Redirect the MSVC compiler class to use clang-cl/lld-link. # setuptools on Windows ignores CC; patching MSVCCompiler.initialize # is the only reliable way to swap the compiler executable. import importlib from collections.abc import Callable from typing import Any clang_cl = os.path.join(LLVM_PATH, 'bin', 'clang-cl.exe') lld_link = os.path.join(LLVM_PATH, 'bin', 'lld-link.exe') for modname in ( 'setuptools._distutils._msvccompiler', 'distutils._msvccompiler', ): try: mod = importlib.import_module(modname) def make_clang_init( orig: Callable[..., None], cc: str, linker: str ) -> Callable[..., None]: """Return patched MSVCCompiler.initialize method using LLVM.""" def clang_init( self: Any, plat_name: str | None = None ) -> None: orig(self, plat_name) self.cc = cc self.linker = linker # remove MSVC flags unsupported by clang-cl for attr in ('compile_options', 'compile_options_debug'): opts = getattr(self, attr, None) if opts is not None: setattr( self, attr, [f for f in opts if f != '/GL'], ) return clang_init mod.MSVCCompiler.initialize = make_clang_init( mod.MSVCCompiler.initialize, clang_cl, lld_link ) except (ImportError, AttributeError): pass if LIMITED_API and not sysconfig.get_config_var('Py_GIL_DISABLED'): py_limited_api = True define_macros = [ ('Py_LIMITED_API', 0x030C0000), ('CYTHON_LIMITED_API', '1'), ] setup_options = {'bdist_wheel': {'py_limited_api': 'cp312'}} else: py_limited_api = False define_macros = [] setup_options = {} def search(pattern: str, string: str, flags: int = 0) -> str: """Return first match of pattern in string.""" match = re.search(pattern, string, flags) if match is None: msg = f'{pattern=!r} not found' raise ValueError(msg) return match.groups()[0] def fix_docstring_examples(docstring: str) -> str: """Return docstring with examples fixed for GitHub.""" start = True indent = False lines = ['..', ' This file is generated by setup.py', ''] for line in docstring.splitlines(): if not line.strip(): start = True indent = False if line.startswith('>>> '): indent = True if start: lines.extend(['.. code-block:: python', '']) start = False lines.append((' ' if indent else '') + line) return '\n'.join(lines) with open('ptufile/ptufile.py', encoding='utf-8') as fh: code = fh.read() version = search(r"__version__ = '(.*?)'", code).replace('.x.x', '.dev0') description = search(r'"""(.*)\.(?:\r\n|\r|\n)', code) readme = search( r'(?:\r\n|\r|\n){2}"""(.*)"""(?:\r\n|\r|\n){2}from __future__', code, re.MULTILINE | re.DOTALL, ) readme = '\n'.join( [description, '=' * len(description), *readme.splitlines()[1:]] ) if 'sdist' in sys.argv: # update README, LICENSE, and CHANGES files with open('README.rst', 'w', encoding='utf-8', newline='\n') as fh: fh.write(fix_docstring_examples(readme)) license = search( r'(# Copyright.*?(?:\r\n|\r|\n))(?:\r\n|\r|\n)+""', code, re.MULTILINE | re.DOTALL, ) license = license.replace('# ', '').replace('#', '') with open('LICENSE', 'w', encoding='utf-8', newline='\n') as fh: fh.write('BSD-3-Clause license\n\n') fh.write(license) revisions = search( r'(?:\r\n|\r|\n){2}(Revisions.*)- …', readme, re.MULTILINE | re.DOTALL, ).strip() with open('CHANGES.rst', encoding='utf-8') as fh: old = fh.read() old = old.split(revisions.splitlines()[-1])[-1] with open('CHANGES.rst', 'w', encoding='utf-8', newline='\n') as fh: fh.write(revisions.replace('---------', '=========').strip()) fh.write(old) ext_modules = [ Extension( 'ptufile._ptufile', ['ptufile/_ptufile.pyx'], define_macros=[ *define_macros, # ('CYTHON_TRACE_NOGIL', '1'), ('NPY_NO_DEPRECATED_API', 'NPY_2_0_API_VERSION'), ], py_limited_api=py_limited_api, extra_compile_args=( ['/Zi', '/Od'] if DEBUG else ['/GS-'] if LLVM_PATH else [] ), extra_link_args=['-debug:full'] if DEBUG else [], include_dirs=[numpy.get_include()], ) ] setup( name='ptufile', version=version, license='BSD-3-Clause', description=description, long_description=readme, long_description_content_type='text/x-rst', author='Christoph Gohlke', author_email='cgohlke@cgohlke.com', url='https://www.cgohlke.com', project_urls={ 'Bug Tracker': 'https://github.com/cgohlke/ptufile/issues', 'Source Code': 'https://github.com/cgohlke/ptufile', # 'Documentation': 'https://', }, packages=['ptufile'], package_data={'ptufile': ['py.typed']}, entry_points={'console_scripts': ['ptufile = ptufile.ptufile:main']}, python_requires='>=3.12', install_requires=['numpy>=2.0'], extras_require={ 'all': [ 'xarray', 'tifffile', 'matplotlib', 'python-dateutil', 'numcodecs', ], }, ext_modules=ext_modules, options=setup_options, platforms=['any'], classifiers=[ 'Development Status :: 4 - Beta', 'Intended Audience :: Science/Research', 'Intended Audience :: Developers', 'Operating System :: OS Independent', 'Programming Language :: Cython', 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13', 'Programming Language :: Python :: 3.14', ], ) cgohlke-ptufile-ab09357/tests/000077500000000000000000000000001515761367400163115ustar00rootroot00000000000000cgohlke-ptufile-ab09357/tests/conftest.py000066400000000000000000000012731515761367400205130ustar00rootroot00000000000000# ptufile/tests/conftest.py """Pytest configuration.""" import os import sys if os.environ.get('VSCODE_CWD'): # work around pytest not using PYTHONPATH in VSCode sys.path.insert( 0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) ) def pytest_report_header(config): """Return pytest report header.""" try: import ptufile return ( f'Python {sys.version.splitlines()[0]}\n' f'packagedir: {ptufile.__path__[0]}\n' f'version: ptufile {ptufile.__version__}' ) except Exception as exc: return f'pytest_report_header failed: {exc!s}' collect_ignore = ['data'] # mypy: ignore-errors cgohlke-ptufile-ab09357/tests/data/000077500000000000000000000000001515761367400172225ustar00rootroot00000000000000cgohlke-ptufile-ab09357/tests/data/ExampleFLIM/000077500000000000000000000000001515761367400212655ustar00rootroot00000000000000cgohlke-ptufile-ab09357/tests/data/ExampleFLIM/readme.txt000066400000000000000000000001671515761367400232670ustar00rootroot00000000000000Obtained from https://zenodo.org/records/10148789 +---ExampleFLIM | Example2_6.ptu | Example_image.sc.ptu cgohlke-ptufile-ab09357/tests/data/FALCON_ptu_examples/000077500000000000000000000000001515761367400227525ustar00rootroot00000000000000cgohlke-ptufile-ab09357/tests/data/FALCON_ptu_examples/readme.txt000066400000000000000000000002311515761367400247440ustar00rootroot00000000000000Obtained from https://github.com/dwaithe/FCS_point_correlator/issues/20 +---FALCON_ptu_examples | 40MHz_example.ptu | 80MHz_cy3_example.ptu cgohlke-ptufile-ab09357/tests/data/Flipper TR time series.sptw/000077500000000000000000000000001515761367400243175ustar00rootroot00000000000000cgohlke-ptufile-ab09357/tests/data/Flipper TR time series.sptw/readme.txt000066400000000000000000000547471515761367400263360ustar00rootroot00000000000000Obtained from Leica Microsystems (Nov 20, 2023) +---Flipper TR time series.sptw | FLIPPER TR baseline H2o glucose GOOD_t1_z1.ptu | FLIPPER TR baseline H2o glucose GOOD_t1_z2.ptu | FLIPPER TR baseline H2o glucose GOOD_t1_z3.ptu | FLIPPER TR baseline H2o glucose GOOD_t1_z4.ptu | FLIPPER TR baseline H2o glucose GOOD_t1_z5.ptu | FLIPPER TR baseline H2o glucose GOOD_t1_z6.ptu | FLIPPER TR baseline H2o glucose GOOD_t1_z7.ptu | FLIPPER TR baseline H2o glucose GOOD_t1_z8.ptu | FLIPPER TR baseline H2o glucose GOOD_t1_z9.ptu | FLIPPER TR baseline H2o glucose GOOD_t1_z10.ptu | FLIPPER TR baseline H2o glucose GOOD_t2_z1.ptu | FLIPPER TR baseline H2o glucose GOOD_t2_z2.ptu | FLIPPER TR baseline H2o glucose GOOD_t2_z3.ptu | FLIPPER TR baseline H2o glucose GOOD_t2_z4.ptu | FLIPPER TR baseline H2o glucose GOOD_t2_z5.ptu | FLIPPER TR baseline H2o glucose GOOD_t2_z6.ptu | FLIPPER TR baseline H2o glucose GOOD_t2_z7.ptu | FLIPPER TR baseline H2o glucose GOOD_t2_z8.ptu | FLIPPER TR baseline H2o glucose GOOD_t2_z9.ptu | FLIPPER TR baseline H2o glucose GOOD_t2_z10.ptu | FLIPPER TR baseline H2o glucose GOOD_t3_z1.ptu | FLIPPER TR baseline H2o glucose GOOD_t3_z2.ptu | FLIPPER TR baseline H2o glucose GOOD_t3_z3.ptu | FLIPPER TR baseline H2o glucose GOOD_t3_z4.ptu | FLIPPER TR baseline H2o glucose GOOD_t3_z5.ptu | FLIPPER TR baseline H2o glucose GOOD_t3_z6.ptu | FLIPPER TR baseline H2o glucose GOOD_t3_z7.ptu | FLIPPER TR baseline H2o glucose GOOD_t3_z8.ptu | FLIPPER TR baseline H2o glucose GOOD_t3_z9.ptu | FLIPPER TR baseline H2o glucose GOOD_t3_z10.ptu | FLIPPER TR baseline H2o glucose GOOD_t4_z1.ptu | FLIPPER TR baseline H2o glucose GOOD_t4_z2.ptu | FLIPPER TR baseline H2o glucose GOOD_t4_z3.ptu | FLIPPER TR baseline H2o glucose GOOD_t4_z4.ptu | FLIPPER TR baseline H2o glucose GOOD_t4_z5.ptu | FLIPPER TR baseline H2o glucose GOOD_t4_z6.ptu | FLIPPER TR baseline H2o glucose GOOD_t4_z7.ptu | FLIPPER TR baseline H2o glucose GOOD_t4_z8.ptu | FLIPPER TR baseline H2o glucose GOOD_t4_z9.ptu | FLIPPER TR baseline H2o glucose GOOD_t4_z10.ptu | FLIPPER TR baseline H2o glucose GOOD_t5_z1.ptu | FLIPPER TR baseline H2o glucose GOOD_t5_z2.ptu | FLIPPER TR baseline H2o glucose GOOD_t5_z3.ptu | FLIPPER TR baseline H2o glucose GOOD_t5_z4.ptu | FLIPPER TR baseline H2o glucose GOOD_t5_z5.ptu | FLIPPER TR baseline H2o glucose GOOD_t5_z6.ptu | FLIPPER TR baseline H2o glucose GOOD_t5_z7.ptu | FLIPPER TR baseline H2o glucose GOOD_t5_z8.ptu | FLIPPER TR baseline H2o glucose GOOD_t5_z9.ptu | FLIPPER TR baseline H2o glucose GOOD_t5_z10.ptu | FLIPPER TR baseline H2o glucose GOOD_t6_z1.ptu | FLIPPER TR baseline H2o glucose GOOD_t6_z2.ptu | FLIPPER TR baseline H2o glucose GOOD_t6_z3.ptu | FLIPPER TR baseline H2o glucose GOOD_t6_z4.ptu | FLIPPER TR baseline H2o glucose GOOD_t6_z5.ptu | FLIPPER TR baseline H2o glucose GOOD_t6_z6.ptu | FLIPPER TR baseline H2o glucose GOOD_t6_z7.ptu | FLIPPER TR baseline H2o glucose GOOD_t6_z8.ptu | FLIPPER TR baseline H2o glucose GOOD_t6_z9.ptu | FLIPPER TR baseline H2o glucose GOOD_t6_z10.ptu | FLIPPER TR baseline H2o glucose GOOD_t7_z1.ptu | FLIPPER TR baseline H2o glucose GOOD_t7_z2.ptu | FLIPPER TR baseline H2o glucose GOOD_t7_z3.ptu | FLIPPER TR baseline H2o glucose GOOD_t7_z4.ptu | FLIPPER TR baseline H2o glucose GOOD_t7_z5.ptu | FLIPPER TR baseline H2o glucose GOOD_t7_z6.ptu | FLIPPER TR baseline H2o glucose GOOD_t7_z7.ptu | FLIPPER TR baseline H2o glucose GOOD_t7_z8.ptu | FLIPPER TR baseline H2o glucose GOOD_t7_z9.ptu | FLIPPER TR baseline H2o glucose GOOD_t7_z10.ptu | FLIPPER TR baseline H2o glucose GOOD_t8_z1.ptu | FLIPPER TR baseline H2o glucose GOOD_t8_z2.ptu | FLIPPER TR baseline H2o glucose GOOD_t8_z3.ptu | FLIPPER TR baseline H2o glucose GOOD_t8_z4.ptu | FLIPPER TR baseline H2o glucose GOOD_t8_z5.ptu | FLIPPER TR baseline H2o glucose GOOD_t8_z6.ptu | FLIPPER TR baseline H2o glucose GOOD_t8_z7.ptu | FLIPPER TR baseline H2o glucose GOOD_t8_z8.ptu | FLIPPER TR baseline H2o glucose GOOD_t8_z9.ptu | FLIPPER TR baseline H2o glucose GOOD_t8_z10.ptu | FLIPPER TR baseline H2o glucose GOOD_t9_z1.ptu | FLIPPER TR baseline H2o glucose GOOD_t9_z2.ptu | FLIPPER TR baseline H2o glucose GOOD_t9_z3.ptu | FLIPPER TR baseline H2o glucose GOOD_t9_z4.ptu | FLIPPER TR baseline H2o glucose GOOD_t9_z5.ptu | FLIPPER TR baseline H2o glucose GOOD_t9_z6.ptu | FLIPPER TR baseline H2o glucose GOOD_t9_z7.ptu | FLIPPER TR baseline H2o glucose GOOD_t9_z8.ptu | FLIPPER TR baseline H2o glucose GOOD_t9_z9.ptu | FLIPPER TR baseline H2o glucose GOOD_t9_z10.ptu | FLIPPER TR baseline H2o glucose GOOD_t10_z1.ptu | FLIPPER TR baseline H2o glucose GOOD_t10_z2.ptu | FLIPPER TR baseline H2o glucose GOOD_t10_z3.ptu | FLIPPER TR baseline H2o glucose GOOD_t10_z4.ptu | FLIPPER TR baseline H2o glucose GOOD_t10_z5.ptu | FLIPPER TR baseline H2o glucose GOOD_t10_z6.ptu | FLIPPER TR baseline H2o glucose GOOD_t10_z7.ptu | FLIPPER TR baseline H2o glucose GOOD_t10_z8.ptu | FLIPPER TR baseline H2o glucose GOOD_t10_z9.ptu | FLIPPER TR baseline H2o glucose GOOD_t10_z10.ptu | FLIPPER TR baseline H2o glucose GOOD_t11_z1.ptu | FLIPPER TR baseline H2o glucose GOOD_t11_z2.ptu | FLIPPER TR baseline H2o glucose GOOD_t11_z3.ptu | FLIPPER TR baseline H2o glucose GOOD_t11_z4.ptu | FLIPPER TR baseline H2o glucose GOOD_t11_z5.ptu | FLIPPER TR baseline H2o glucose GOOD_t11_z6.ptu | FLIPPER TR baseline H2o glucose GOOD_t11_z7.ptu | FLIPPER TR baseline H2o glucose GOOD_t11_z8.ptu | FLIPPER TR baseline H2o glucose GOOD_t11_z9.ptu | FLIPPER TR baseline H2o glucose GOOD_t11_z10.ptu | FLIPPER TR baseline H2o glucose GOOD_t12_z1.ptu | FLIPPER TR baseline H2o glucose GOOD_t12_z2.ptu | FLIPPER TR baseline H2o glucose GOOD_t12_z3.ptu | FLIPPER TR baseline H2o glucose GOOD_t12_z4.ptu | FLIPPER TR baseline H2o glucose GOOD_t12_z5.ptu | FLIPPER TR baseline H2o glucose GOOD_t12_z6.ptu | FLIPPER TR baseline H2o glucose GOOD_t12_z7.ptu | FLIPPER TR baseline H2o glucose GOOD_t12_z8.ptu | FLIPPER TR baseline H2o glucose GOOD_t12_z9.ptu | FLIPPER TR baseline H2o glucose GOOD_t12_z10.ptu | FLIPPER TR baseline H2o glucose GOOD_t13_z1.ptu | FLIPPER TR baseline H2o glucose GOOD_t13_z2.ptu | FLIPPER TR baseline H2o glucose GOOD_t13_z3.ptu | FLIPPER TR baseline H2o glucose GOOD_t13_z4.ptu | FLIPPER TR baseline H2o glucose GOOD_t13_z5.ptu | FLIPPER TR baseline H2o glucose GOOD_t13_z6.ptu | FLIPPER TR baseline H2o glucose GOOD_t13_z7.ptu | FLIPPER TR baseline H2o glucose GOOD_t13_z8.ptu | FLIPPER TR baseline H2o glucose GOOD_t13_z9.ptu | FLIPPER TR baseline H2o glucose GOOD_t13_z10.ptu | FLIPPER TR baseline H2o glucose GOOD_t14_z1.ptu | FLIPPER TR baseline H2o glucose GOOD_t14_z2.ptu | FLIPPER TR baseline H2o glucose GOOD_t14_z3.ptu | FLIPPER TR baseline H2o glucose GOOD_t14_z4.ptu | FLIPPER TR baseline H2o glucose GOOD_t14_z5.ptu | FLIPPER TR baseline H2o glucose GOOD_t14_z6.ptu | FLIPPER TR baseline H2o glucose GOOD_t14_z7.ptu | FLIPPER TR baseline H2o glucose GOOD_t14_z8.ptu | FLIPPER TR baseline H2o glucose GOOD_t14_z9.ptu | FLIPPER TR baseline H2o glucose GOOD_t14_z10.ptu | FLIPPER TR baseline H2o glucose GOOD_t15_z1.ptu | FLIPPER TR baseline H2o glucose GOOD_t15_z2.ptu | FLIPPER TR baseline H2o glucose GOOD_t15_z3.ptu | FLIPPER TR baseline H2o glucose GOOD_t15_z4.ptu | FLIPPER TR baseline H2o glucose GOOD_t15_z5.ptu | FLIPPER TR baseline H2o glucose GOOD_t15_z6.ptu | FLIPPER TR baseline H2o glucose GOOD_t15_z7.ptu | FLIPPER TR baseline H2o glucose GOOD_t15_z8.ptu | FLIPPER TR baseline H2o glucose GOOD_t15_z9.ptu | FLIPPER TR baseline H2o glucose GOOD_t15_z10.ptu | FLIPPER TR baseline H2o glucose GOOD_t16_z1.ptu | FLIPPER TR baseline H2o glucose GOOD_t16_z2.ptu | FLIPPER TR baseline H2o glucose GOOD_t16_z3.ptu | FLIPPER TR baseline H2o glucose GOOD_t16_z4.ptu | FLIPPER TR baseline H2o glucose GOOD_t16_z5.ptu | FLIPPER TR baseline H2o glucose GOOD_t16_z6.ptu | FLIPPER TR baseline H2o glucose GOOD_t16_z7.ptu | FLIPPER TR baseline H2o glucose GOOD_t16_z8.ptu | FLIPPER TR baseline H2o glucose GOOD_t16_z9.ptu | FLIPPER TR baseline H2o glucose GOOD_t16_z10.ptu | FLIPPER TR baseline H2o glucose GOOD_t17_z1.ptu | FLIPPER TR baseline H2o glucose GOOD_t17_z2.ptu | FLIPPER TR baseline H2o glucose GOOD_t17_z3.ptu | FLIPPER TR baseline H2o glucose GOOD_t17_z4.ptu | FLIPPER TR baseline H2o glucose GOOD_t17_z5.ptu | FLIPPER TR baseline H2o glucose GOOD_t17_z6.ptu | FLIPPER TR baseline H2o glucose GOOD_t17_z7.ptu | FLIPPER TR baseline H2o glucose GOOD_t17_z8.ptu | FLIPPER TR baseline H2o glucose GOOD_t17_z9.ptu | FLIPPER TR baseline H2o glucose GOOD_t17_z10.ptu | FLIPPER TR baseline H2o glucose GOOD_t18_z1.ptu | FLIPPER TR baseline H2o glucose GOOD_t18_z2.ptu | FLIPPER TR baseline H2o glucose GOOD_t18_z3.ptu | FLIPPER TR baseline H2o glucose GOOD_t18_z4.ptu | FLIPPER TR baseline H2o glucose GOOD_t18_z5.ptu | FLIPPER TR baseline H2o glucose GOOD_t18_z6.ptu | FLIPPER TR baseline H2o glucose GOOD_t18_z7.ptu | FLIPPER TR baseline H2o glucose GOOD_t18_z8.ptu | FLIPPER TR baseline H2o glucose GOOD_t18_z9.ptu | FLIPPER TR baseline H2o glucose GOOD_t18_z10.ptu | FLIPPER TR baseline H2o glucose GOOD_t19_z1.ptu | FLIPPER TR baseline H2o glucose GOOD_t19_z2.ptu | FLIPPER TR baseline H2o glucose GOOD_t19_z3.ptu | FLIPPER TR baseline H2o glucose GOOD_t19_z4.ptu | FLIPPER TR baseline H2o glucose GOOD_t19_z5.ptu | FLIPPER TR baseline H2o glucose GOOD_t19_z6.ptu | FLIPPER TR baseline H2o glucose GOOD_t19_z7.ptu | FLIPPER TR baseline H2o glucose GOOD_t19_z8.ptu | FLIPPER TR baseline H2o glucose GOOD_t19_z9.ptu | FLIPPER TR baseline H2o glucose GOOD_t19_z10.ptu | FLIPPER TR baseline H2o glucose GOOD_t20_z1.ptu | FLIPPER TR baseline H2o glucose GOOD_t20_z2.ptu | FLIPPER TR baseline H2o glucose GOOD_t20_z3.ptu | FLIPPER TR baseline H2o glucose GOOD_t20_z4.ptu | FLIPPER TR baseline H2o glucose GOOD_t20_z5.ptu | FLIPPER TR baseline H2o glucose GOOD_t20_z6.ptu | FLIPPER TR baseline H2o glucose GOOD_t20_z7.ptu | FLIPPER TR baseline H2o glucose GOOD_t20_z8.ptu | FLIPPER TR baseline H2o glucose GOOD_t20_z9.ptu | FLIPPER TR baseline H2o glucose GOOD_t20_z10.ptu | FLIPPER TR baseline H2o glucose GOOD_t21_z1.ptu | FLIPPER TR baseline H2o glucose GOOD_t21_z2.ptu | FLIPPER TR baseline H2o glucose GOOD_t21_z3.ptu | FLIPPER TR baseline H2o glucose GOOD_t21_z4.ptu | FLIPPER TR baseline H2o glucose GOOD_t21_z5.ptu | FLIPPER TR baseline H2o glucose GOOD_t21_z6.ptu | FLIPPER TR baseline H2o glucose GOOD_t21_z7.ptu | FLIPPER TR baseline H2o glucose GOOD_t21_z8.ptu | FLIPPER TR baseline H2o glucose GOOD_t21_z9.ptu | FLIPPER TR baseline H2o glucose GOOD_t21_z10.ptu | FLIPPER TR baseline H2o glucose GOOD_t22_z1.ptu | FLIPPER TR baseline H2o glucose GOOD_t22_z2.ptu | FLIPPER TR baseline H2o glucose GOOD_t22_z3.ptu | FLIPPER TR baseline H2o glucose GOOD_t22_z4.ptu | FLIPPER TR baseline H2o glucose GOOD_t22_z5.ptu | FLIPPER TR baseline H2o glucose GOOD_t22_z6.ptu | FLIPPER TR baseline H2o glucose GOOD_t22_z7.ptu | FLIPPER TR baseline H2o glucose GOOD_t22_z8.ptu | FLIPPER TR baseline H2o glucose GOOD_t22_z9.ptu | FLIPPER TR baseline H2o glucose GOOD_t22_z10.ptu | FLIPPER TR baseline H2o glucose GOOD_t23_z1.ptu | FLIPPER TR baseline H2o glucose GOOD_t23_z2.ptu | FLIPPER TR baseline H2o glucose GOOD_t23_z3.ptu | FLIPPER TR baseline H2o glucose GOOD_t23_z4.ptu | FLIPPER TR baseline H2o glucose GOOD_t23_z5.ptu | FLIPPER TR baseline H2o glucose GOOD_t23_z6.ptu | FLIPPER TR baseline H2o glucose GOOD_t23_z7.ptu | FLIPPER TR baseline H2o glucose GOOD_t23_z8.ptu | FLIPPER TR baseline H2o glucose GOOD_t23_z9.ptu | FLIPPER TR baseline H2o glucose GOOD_t23_z10.ptu | FLIPPER TR baseline H2o glucose GOOD_t24_z1.ptu | FLIPPER TR baseline H2o glucose GOOD_t24_z2.ptu | FLIPPER TR baseline H2o glucose GOOD_t24_z3.ptu | FLIPPER TR baseline H2o glucose GOOD_t24_z4.ptu | FLIPPER TR baseline H2o glucose GOOD_t24_z5.ptu | FLIPPER TR baseline H2o glucose GOOD_t24_z6.ptu | FLIPPER TR baseline H2o glucose GOOD_t24_z7.ptu | FLIPPER TR baseline H2o glucose GOOD_t24_z8.ptu | FLIPPER TR baseline H2o glucose GOOD_t24_z9.ptu | FLIPPER TR baseline H2o glucose GOOD_t24_z10.ptu | FLIPPER TR baseline H2o glucose GOOD_t25_z1.ptu | FLIPPER TR baseline H2o glucose GOOD_t25_z2.ptu | FLIPPER TR baseline H2o glucose GOOD_t25_z3.ptu | FLIPPER TR baseline H2o glucose GOOD_t25_z4.ptu | FLIPPER TR baseline H2o glucose GOOD_t25_z5.ptu | FLIPPER TR baseline H2o glucose GOOD_t25_z6.ptu | FLIPPER TR baseline H2o glucose GOOD_t25_z7.ptu | FLIPPER TR baseline H2o glucose GOOD_t25_z8.ptu | FLIPPER TR baseline H2o glucose GOOD_t25_z9.ptu | FLIPPER TR baseline H2o glucose GOOD_t25_z10.ptu | FLIPPER TR baseline H2o glucose GOOD_t26_z1.ptu | FLIPPER TR baseline H2o glucose GOOD_t26_z2.ptu | FLIPPER TR baseline H2o glucose GOOD_t26_z3.ptu | FLIPPER TR baseline H2o glucose GOOD_t26_z4.ptu | FLIPPER TR baseline H2o glucose GOOD_t26_z5.ptu | FLIPPER TR baseline H2o glucose GOOD_t26_z6.ptu | FLIPPER TR baseline H2o glucose GOOD_t26_z7.ptu | FLIPPER TR baseline H2o glucose GOOD_t26_z8.ptu | FLIPPER TR baseline H2o glucose GOOD_t26_z9.ptu | FLIPPER TR baseline H2o glucose GOOD_t26_z10.ptu | FLIPPER TR baseline H2o glucose GOOD_t27_z1.ptu | FLIPPER TR baseline H2o glucose GOOD_t27_z2.ptu | FLIPPER TR baseline H2o glucose GOOD_t27_z3.ptu | FLIPPER TR baseline H2o glucose GOOD_t27_z4.ptu | FLIPPER TR baseline H2o glucose GOOD_t27_z5.ptu | FLIPPER TR baseline H2o glucose GOOD_t27_z6.ptu | FLIPPER TR baseline H2o glucose GOOD_t27_z7.ptu | FLIPPER TR baseline H2o glucose GOOD_t27_z8.ptu | FLIPPER TR baseline H2o glucose GOOD_t27_z9.ptu | FLIPPER TR baseline H2o glucose GOOD_t27_z10.ptu | FLIPPER TR baseline H2o glucose GOOD_t28_z1.ptu | FLIPPER TR baseline H2o glucose GOOD_t28_z2.ptu | FLIPPER TR baseline H2o glucose GOOD_t28_z3.ptu | FLIPPER TR baseline H2o glucose GOOD_t28_z4.ptu | FLIPPER TR baseline H2o glucose GOOD_t28_z5.ptu | FLIPPER TR baseline H2o glucose GOOD_t28_z6.ptu | FLIPPER TR baseline H2o glucose GOOD_t28_z7.ptu | FLIPPER TR baseline H2o glucose GOOD_t28_z8.ptu | FLIPPER TR baseline H2o glucose GOOD_t28_z9.ptu | FLIPPER TR baseline H2o glucose GOOD_t28_z10.ptu | FLIPPER TR baseline H2o glucose GOOD_t29_z1.ptu | FLIPPER TR baseline H2o glucose GOOD_t29_z2.ptu | FLIPPER TR baseline H2o glucose GOOD_t29_z3.ptu | FLIPPER TR baseline H2o glucose GOOD_t29_z4.ptu | FLIPPER TR baseline H2o glucose GOOD_t29_z5.ptu | FLIPPER TR baseline H2o glucose GOOD_t29_z6.ptu | FLIPPER TR baseline H2o glucose GOOD_t29_z7.ptu | FLIPPER TR baseline H2o glucose GOOD_t29_z8.ptu | FLIPPER TR baseline H2o glucose GOOD_t29_z9.ptu | FLIPPER TR baseline H2o glucose GOOD_t29_z10.ptu | FLIPPER TR baseline H2o glucose GOOD_t30_z1.ptu | FLIPPER TR baseline H2o glucose GOOD_t30_z2.ptu | FLIPPER TR baseline H2o glucose GOOD_t30_z3.ptu | FLIPPER TR baseline H2o glucose GOOD_t30_z4.ptu | FLIPPER TR baseline H2o glucose GOOD_t30_z5.ptu | FLIPPER TR baseline H2o glucose GOOD_t30_z6.ptu | FLIPPER TR baseline H2o glucose GOOD_t30_z7.ptu | FLIPPER TR baseline H2o glucose GOOD_t30_z8.ptu | FLIPPER TR baseline H2o glucose GOOD_t30_z9.ptu | FLIPPER TR baseline H2o glucose GOOD_t30_z10.ptu | FLIPPER TR baseline H2o glucose GOOD_t31_z1.ptu | FLIPPER TR baseline H2o glucose GOOD_t31_z2.ptu | FLIPPER TR baseline H2o glucose GOOD_t31_z3.ptu | FLIPPER TR baseline H2o glucose GOOD_t31_z4.ptu | FLIPPER TR baseline H2o glucose GOOD_t31_z5.ptu | FLIPPER TR baseline H2o glucose GOOD_t31_z6.ptu | FLIPPER TR baseline H2o glucose GOOD_t31_z7.ptu | FLIPPER TR baseline H2o glucose GOOD_t31_z8.ptu | FLIPPER TR baseline H2o glucose GOOD_t31_z9.ptu | FLIPPER TR baseline H2o glucose GOOD_t31_z10.ptu | FLIPPER TR baseline H2o glucose GOOD_t32_z1.ptu | FLIPPER TR baseline H2o glucose GOOD_t32_z2.ptu | FLIPPER TR baseline H2o glucose GOOD_t32_z3.ptu | FLIPPER TR baseline H2o glucose GOOD_t32_z4.ptu | FLIPPER TR baseline H2o glucose GOOD_t32_z5.ptu | FLIPPER TR baseline H2o glucose GOOD_t32_z6.ptu | FLIPPER TR baseline H2o glucose GOOD_t32_z7.ptu | FLIPPER TR baseline H2o glucose GOOD_t32_z8.ptu | FLIPPER TR baseline H2o glucose GOOD_t32_z9.ptu | FLIPPER TR baseline H2o glucose GOOD_t32_z10.ptu | FLIPPER TR baseline H2o glucose GOOD_t33_z1.ptu | FLIPPER TR baseline H2o glucose GOOD_t33_z2.ptu | FLIPPER TR baseline H2o glucose GOOD_t33_z3.ptu | FLIPPER TR baseline H2o glucose GOOD_t33_z4.ptu | FLIPPER TR baseline H2o glucose GOOD_t33_z5.ptu | FLIPPER TR baseline H2o glucose GOOD_t33_z6.ptu | FLIPPER TR baseline H2o glucose GOOD_t33_z7.ptu | FLIPPER TR baseline H2o glucose GOOD_t33_z8.ptu | FLIPPER TR baseline H2o glucose GOOD_t33_z9.ptu | FLIPPER TR baseline H2o glucose GOOD_t33_z10.ptu | FLIPPER TR baseline H2o glucose GOOD_t34_z1.ptu | FLIPPER TR baseline H2o glucose GOOD_t34_z2.ptu | FLIPPER TR baseline H2o glucose GOOD_t34_z3.ptu | FLIPPER TR baseline H2o glucose GOOD_t34_z4.ptu | FLIPPER TR baseline H2o glucose GOOD_t34_z5.ptu | FLIPPER TR baseline H2o glucose GOOD_t34_z6.ptu | FLIPPER TR baseline H2o glucose GOOD_t34_z7.ptu | FLIPPER TR baseline H2o glucose GOOD_t34_z8.ptu | FLIPPER TR baseline H2o glucose GOOD_t34_z9.ptu | FLIPPER TR baseline H2o glucose GOOD_t34_z10.ptu | FLIPPER TR baseline H2o glucose GOOD_t35_z1.ptu | FLIPPER TR baseline H2o glucose GOOD_t35_z2.ptu | FLIPPER TR baseline H2o glucose GOOD_t35_z3.ptu | FLIPPER TR baseline H2o glucose GOOD_t35_z4.ptu | FLIPPER TR baseline H2o glucose GOOD_t35_z5.ptu | FLIPPER TR baseline H2o glucose GOOD_t35_z6.ptu | FLIPPER TR baseline H2o glucose GOOD_t35_z7.ptu | FLIPPER TR baseline H2o glucose GOOD_t35_z8.ptu | FLIPPER TR baseline H2o glucose GOOD_t35_z9.ptu | FLIPPER TR baseline H2o glucose GOOD_t35_z10.ptu | FLIPPER TR baseline H2o glucose GOOD_t36_z1.ptu | FLIPPER TR baseline H2o glucose GOOD_t36_z2.ptu | FLIPPER TR baseline H2o glucose GOOD_t36_z3.ptu | FLIPPER TR baseline H2o glucose GOOD_t36_z4.ptu | FLIPPER TR baseline H2o glucose GOOD_t36_z5.ptu | FLIPPER TR baseline H2o glucose GOOD_t36_z6.ptu | FLIPPER TR baseline H2o glucose GOOD_t36_z7.ptu | FLIPPER TR baseline H2o glucose GOOD_t36_z8.ptu | FLIPPER TR baseline H2o glucose GOOD_t36_z9.ptu | FLIPPER TR baseline H2o glucose GOOD_t36_z10.ptu | FLIPPER TR baseline H2o glucose GOOD_t37_z1.ptu | FLIPPER TR baseline H2o glucose GOOD_t37_z2.ptu | FLIPPER TR baseline H2o glucose GOOD_t37_z3.ptu | FLIPPER TR baseline H2o glucose GOOD_t37_z4.ptu | FLIPPER TR baseline H2o glucose GOOD_t37_z5.ptu | FLIPPER TR baseline H2o glucose GOOD_t37_z6.ptu | FLIPPER TR baseline H2o glucose GOOD_t37_z7.ptu | FLIPPER TR baseline H2o glucose GOOD_t37_z8.ptu | FLIPPER TR baseline H2o glucose GOOD_t37_z9.ptu | FLIPPER TR baseline H2o glucose GOOD_t37_z10.ptu | FLIPPER TR baseline H2o glucose GOOD_t38_z1.ptu | FLIPPER TR baseline H2o glucose GOOD_t38_z2.ptu | FLIPPER TR baseline H2o glucose GOOD_t38_z3.ptu | FLIPPER TR baseline H2o glucose GOOD_t38_z4.ptu | FLIPPER TR baseline H2o glucose GOOD_t38_z5.ptu | FLIPPER TR baseline H2o glucose GOOD_t38_z6.ptu | FLIPPER TR baseline H2o glucose GOOD_t38_z7.ptu | FLIPPER TR baseline H2o glucose GOOD_t38_z8.ptu | FLIPPER TR baseline H2o glucose GOOD_t38_z9.ptu | FLIPPER TR baseline H2o glucose GOOD_t38_z10.ptu | FLIPPER TR baseline H2o glucose GOOD_t39_z1.ptu | FLIPPER TR baseline H2o glucose GOOD_t39_z2.ptu | FLIPPER TR baseline H2o glucose GOOD_t39_z3.ptu | FLIPPER TR baseline H2o glucose GOOD_t39_z4.ptu | FLIPPER TR baseline H2o glucose GOOD_t39_z5.ptu | FLIPPER TR baseline H2o glucose GOOD_t39_z6.ptu | FLIPPER TR baseline H2o glucose GOOD_t39_z7.ptu | FLIPPER TR baseline H2o glucose GOOD_t39_z8.ptu | FLIPPER TR baseline H2o glucose GOOD_t39_z9.ptu | FLIPPER TR baseline H2o glucose GOOD_t39_z10.ptu | FLIPPER TR baseline H2o glucose GOOD_t40_z1.ptu | FLIPPER TR baseline H2o glucose GOOD_t40_z2.ptu | FLIPPER TR baseline H2o glucose GOOD_t40_z3.ptu | FLIPPER TR baseline H2o glucose GOOD_t40_z4.ptu | FLIPPER TR baseline H2o glucose GOOD_t40_z5.ptu | FLIPPER TR baseline H2o glucose GOOD_t40_z6.ptu | FLIPPER TR baseline H2o glucose GOOD_t40_z7.ptu | FLIPPER TR baseline H2o glucose GOOD_t40_z8.ptu | FLIPPER TR baseline H2o glucose GOOD_t40_z9.ptu | FLIPPER TR baseline H2o glucose GOOD_t40_z10.ptu | FLIPPER TR baseline H2o glucose GOOD_t41_z1.ptu | FLIPPER TR baseline H2o glucose GOOD_t41_z2.ptu | FLIPPER TR baseline H2o glucose GOOD_t41_z3.ptu | FLIPPER TR baseline H2o glucose GOOD_t41_z4.ptu | FLIPPER TR baseline H2o glucose GOOD_t41_z5.ptu | FLIPPER TR baseline H2o glucose GOOD_t41_z6.ptu | FLIPPER TR baseline H2o glucose GOOD_t41_z7.ptu | FLIPPER TR baseline H2o glucose GOOD_t41_z8.ptu | FLIPPER TR baseline H2o glucose GOOD_t41_z9.ptu | FLIPPER TR baseline H2o glucose GOOD_t41_z10.ptu | WSLogfile.sptl cgohlke-ptufile-ab09357/tests/data/FluoPlot/000077500000000000000000000000001515761367400207665ustar00rootroot00000000000000cgohlke-ptufile-ab09357/tests/data/FluoPlot/readme.txt000066400000000000000000000003521515761367400227640ustar00rootroot00000000000000Obtained from PicoQuant FluoPlot installer +---FluoPlot | C6_Rubrene_TRES.hhd | Correction_Device1.scd | Naphtal_BuOH_TRES.phd | Naphtal_BuOH_TRES.phu | Sensitivity_Device1.scd | TRES_spectrum.thd cgohlke-ptufile-ab09357/tests/data/Fretica/000077500000000000000000000000001515761367400205775ustar00rootroot00000000000000cgohlke-ptufile-ab09357/tests/data/Fretica/readme.txt000066400000000000000000000000631515761367400225740ustar00rootroot00000000000000Obtained from https://schuler.bioc.uzh.ch/programs/cgohlke-ptufile-ab09357/tests/data/HHU/000077500000000000000000000000001515761367400176465ustar00rootroot00000000000000cgohlke-ptufile-ab09357/tests/data/HHU/readme.txt000066400000000000000000000001221515761367400216370ustar00rootroot00000000000000Obtained by email on Aug 27, 2025 +---HHU | PQSpcm_2021-12-13_17-53-45.ptu cgohlke-ptufile-ab09357/tests/data/Luminosa/000077500000000000000000000000001515761367400210115ustar00rootroot00000000000000cgohlke-ptufile-ab09357/tests/data/Luminosa/readme.txt000066400000000000000000000004701515761367400230100ustar00rootroot00000000000000Obtained from PicoQuant on Jan 31, 2025 +---Luminosa | | GattaQUant_Cells_FLIM.ptu | | | +---dsDNA Acceptor only A655 , 134 uW , small volume | | RawData.ptu | | | +---FRET_20230606-185222 | | RawData.ptu | | | \---mixture_dsDNA A655_Cy5_20221116-155706 | RawData.ptu cgohlke-ptufile-ab09357/tests/data/QuCoa/000077500000000000000000000000001515761367400202325ustar00rootroot00000000000000cgohlke-ptufile-ab09357/tests/data/QuCoa/readme.txt000066400000000000000000000003641515761367400222330ustar00rootroot00000000000000Obtained from https://www.picoquant.com/dl_software/QuCoa/QuCoa_v1_4_0_5818.zip +---QuCoa | Antibunching_1.pqres | AtB_Fitting_1.pqres | AtB_Fitting_2.pqres | CW_Shelved.ptu | Pulsed.ptu | Pulsed_OAtB.pqres cgohlke-ptufile-ab09357/tests/data/Samples.sptw/000077500000000000000000000000001515761367400216225ustar00rootroot00000000000000cgohlke-ptufile-ab09357/tests/data/Samples.sptw/readme.txt000066400000000000000000000073141515761367400236250ustar00rootroot00000000000000Obtained from https://www.picoquant.com/dl_software/SPT64/Demo_workspace_SPT64.zip +---Samples.sptw | AnisotropyImage.pqres | Atto488_485nm_pulsed.pco | Atto488_485nm_pulsed.ptu | Atto488_diff_cw_total_correlation.pco | Atto488_diff_cw_total_correlation.ptu | ATTO488_FCS.pqres | ATTO488_FCS_Calibration.pqres | ATTO488_FCS_Fitting.pqres | Atto655+Cy5_diff_FCS+FLCS.pco | Atto655+Cy5_diff_FCS+FLCS.ptu | Atto655_diff_2FFCS.pco | Atto655_diff_2FFCS.ptu | Atto655_diff_FLCS-pattern.pco | Atto655_diff_FLCS-pattern.ptu | Atto655_focus1_horz_exc.pqres | Atto655_focus1_X_focus2.pqres | Atto655_focus2_perp_exc.pqres | Atto655_immo_On-Off-Analysis.pco | Atto655_immo_On-Off-Analysis.ptu | Atto655_TCSPC_Fitting.pqres | Blinking_FCS.pqres | Blinking_LifetimeTrace.pqres | Blinking_TimeTrace.pqres | CENP-FLIM.pqres | CENP-labelled_cells_for_FRET.pco | CENP-labelled_cells_for_FRET.ptu | CENP-labelled_cells_for_FRET_IRF_Det1.pco | CENP-labelled_cells_for_FRET_IRF_Det1.ptu | CENP-labelled_cells_for_FRET_IRF_Det2.pco | CENP-labelled_cells_for_FRET_IRF_Det2.ptu | CENP_FRET_Image.pqres | CENP_LT-FRET_2.pqres | Classical_FCS.pqres | CW_Antibunching.pqres | Cy3+Cy5_diff_PIE-FRET.pco | Cy3+Cy5_diff_PIE-FRET.ptu | Cy3+Cy5_FRET_TimeTrace.pqres | Cy3+Cy5_PIE_FRET_TimeTrace.pqres | Cy5_diff_IRF+FLCS-pattern.pco | Cy5_diff_IRF+FLCS-pattern.ptu | Cy5_immo_FLIM+Pol-Imaging.pco | Cy5_immo_FLIM+Pol-Imaging.ptu | Cy5_immo_Lifetime_Trace.pco | Cy5_immo_Lifetime_Trace.ptu | cy5_immo_TimeTrace.pqres | DaisyPollen_cells_FLIM.pco | DaisyPollen_cells_FLIM.ptu | Fast_FLIM.pqres | FLCS_Atto655_ONLY.pqres | FLCS_Cy5_ONLY.pqres | FLCS_Cy5_ONLY_1.pqres | FLIM non-FRET sample.pqres | FLIM_1_expon.pqres | FLIM_2.pqres | FLIM_3_expon.pqres | FLIM_dual_expon.pqres | FLIM_FRET_GFP_and_mRFP.pqres | FocalWidthEstimation.pqres | FRET_GFP and mRFP.pco | FRET_GFP_and_mRFP.ptu | FRET_Image.pqres | GFP_RFP_cells_FLIM-FRET.pco | GFP_RFP_cells_FLIM-FRET.ptu | Grouped_TCSPC_Fitting.pqres | GUVs.pco | GUVs.ptu | GUVs_MFLIM.pqres | IBA488+547_crosslinked.pco | IBA488+547_crosslinked.ptu | IBA488+547_FCCS_Grouped_1.pqres | IBA488+IBA547_unlinked_FCCS_grouped.pqres | IBA488+IBA547_unlinked_mix.pco | IBA488+IBA547_unlinked_mix.ptu | Left_Focus.pqres | LT-FRET.pqres | LT-FRET_1.pqres | LT-FRET_Binding.pqres | MFLIM.pqres | non-FRET sample.pco | non-FRET sample.ptu | NV-Center_for_Antibunching_1.pco | NV-Center_for_Antibunching_1.ptu | NV-Center_for_Antibunching_2.pco | NV-Center_for_Antibunching_2.ptu | NV-Center_for_Antibunching_several.pco | NV-Center_for_Antibunching_several.ptu | NV_Antibunching_1.pqres | NV_Antibunching_2.pqres | NV_Antibunching_several.pqres | OnOffHistogram.pqres | PatternMatching_non-FRET pattern from additional sample.pqres | PatternMatching_Patterns from DDM.pqres | PatternMatching_Patterns from Image.pqres | PatternMatching_with_Background.pqres | Pat_FRET_GFP and mRFP.pqres | Pat_FRET_GFP_and_mRFP.pqres | Pat_non-FRET sample.pqres | Right_Focus.pqres | TCSPC_Decay.pqres | TotalCorrelation.pqres | TS-Bead_immo_xy-scan_Dual Focus.pco | TS-Bead_immo_xy-scan_Dual Focus.ptu | TS-Bead_immo_xz-scan.pco | TS-Bead_immo_xz-scan.ptu | WSLogfile.sptl cgohlke-ptufile-ab09357/tests/data/TU_Dortmund/000077500000000000000000000000001515761367400214265ustar00rootroot00000000000000cgohlke-ptufile-ab09357/tests/data/TU_Dortmund/readme.txt000066400000000000000000000002101515761367400234150ustar00rootroot00000000000000Obtained by email on 2025.5.21 +---TU_Dortmund | 13_7_5.ptu | daisy1.ptu | fluorescein_ref_4ns.ptu | Pollen.ptucgohlke-ptufile-ab09357/tests/data/TimeHarp/000077500000000000000000000000001515761367400207335ustar00rootroot00000000000000cgohlke-ptufile-ab09357/tests/data/TimeHarp/readme.txt000066400000000000000000000002261515761367400227310ustar00rootroot00000000000000Obtained from https://www.tcspc.com/doku.php/howto:data_file_import, https://www.picoquant.com/dl_software/TimeHarp260/TimeHarp260_SW_and_DLL_V3_2.zipcgohlke-ptufile-ab09357/tests/data/Tutorials.sptw/000077500000000000000000000000001515761367400222045ustar00rootroot00000000000000cgohlke-ptufile-ab09357/tests/data/Tutorials.sptw/readme.txt000066400000000000000000000006151515761367400242040ustar00rootroot00000000000000Obtained from https://figshare.com/s/4957fcfa684daef86c23 +---Tutorials.sptw | Hyperosmotic_Shock_MDCK_Cell.pco | Hyperosmotic_Shock_MDCK_Cells.ptu | IRF_Fluorescein.pck | IRF_Fluorescein.pco | IRF_Fluorescein.ptu | Kidney _Cell_FLIM.pco | Kidney _Cell_FLIM.ptu | MicroBeads.pck | MicroBeads.pco | MicroBeads.ptu | WSLogfile.sptl cgohlke-ptufile-ab09357/tests/data/fastFLIM/000077500000000000000000000000001515761367400206275ustar00rootroot00000000000000cgohlke-ptufile-ab09357/tests/data/fastFLIM/readme.txt000066400000000000000000000002021515761367400226170ustar00rootroot00000000000000Obtained from Robert Molenaar on Nov 26, 2024 +---fastFLIM | | A2_Shep2_26.ptu | | B2_Shep2_2.ptu | | B2_Shep2_3.ptu cgohlke-ptufile-ab09357/tests/data/flimview/000077500000000000000000000000001515761367400210445ustar00rootroot00000000000000cgohlke-ptufile-ab09357/tests/data/flimview/readme.txt000066400000000000000000000001421515761367400230370ustar00rootroot00000000000000Obtained from https://github.com/Biophotonics-COMI/flimview +---flimview | macrophages.ptu cgohlke-ptufile-ab09357/tests/data/i3S/000077500000000000000000000000001515761367400176605ustar00rootroot00000000000000cgohlke-ptufile-ab09357/tests/data/i3S/readme.txt000066400000000000000000000001631515761367400216560ustar00rootroot00000000000000Obtained by email on Oct 27, 2025 +---i3S | AlessandroSlide_10x_488nm.ptu | Coumarin6_10x_488nm.ptu cgohlke-ptufile-ab09357/tests/data/image_from_ptu/000077500000000000000000000000001515761367400222175ustar00rootroot00000000000000cgohlke-ptufile-ab09357/tests/data/image_from_ptu/readme.txt000066400000000000000000000001441515761367400242140ustar00rootroot00000000000000Obtained from https://github.com/PicoQuant/image_from_ptu +---image_from_ptu | RawImage.ptu cgohlke-ptufile-ab09357/tests/data/napari_flim_phasor_plotter/000077500000000000000000000000001515761367400246305ustar00rootroot00000000000000cgohlke-ptufile-ab09357/tests/data/napari_flim_phasor_plotter/readme.txt000066400000000000000000000005501515761367400266260ustar00rootroot00000000000000Obtained from https://github.com/zoccoler/napari-flim-phasor-plotter/tree/main/src/napari_flim_phasor_plotter/data +---napari_flim_phasor_plotter | hazelnut_FLIM_single_image.ptu | hazelnut_FLIM_z_stack.zip | lifetime_cat.tif | lifetime_cat_labels.tif | lifetime_cat_metadata.yml | seminal_receptacle_FLIM_single_image.sdt cgohlke-ptufile-ab09357/tests/data/nc.picoquant.com/000077500000000000000000000000001515761367400224015ustar00rootroot00000000000000cgohlke-ptufile-ab09357/tests/data/nc.picoquant.com/readme.txt000066400000000000000000000002301515761367400243720ustar00rootroot00000000000000Obtained from PicoQuant GmbH +---nc.picoquant.com | DaisyPollen1.ptu | GattaQuant_Cells.ptu | HYPO 1ml of water in 500 medium 2.ptu cgohlke-ptufile-ab09357/tests/data/phconvert/000077500000000000000000000000001515761367400212325ustar00rootroot00000000000000cgohlke-ptufile-ab09357/tests/data/phconvert/readme.txt000066400000000000000000000003251515761367400232300ustar00rootroot00000000000000Obtained from https://github.com/Photon-HDF5/phconvert/issues/12 +---phconvert | 161128_DM1_50pM_pH71.ptu | 161128_DM1_50pM_pH73.ptu | 161128_DM1_50pM_pH74.ptu | Cy3+Cy5_diff_PIE-FRET.ptu cgohlke-ptufile-ab09357/tests/data/picoquant-sample-data/000077500000000000000000000000001515761367400234135ustar00rootroot00000000000000cgohlke-ptufile-ab09357/tests/data/picoquant-sample-data/readme.txt000066400000000000000000000011451515761367400254120ustar00rootroot00000000000000Obtained from https://github.com/tsbischof/picoquant-sample-data +---picoquant-sample-data | | LICENSE | | | +---hydraharp | | v10.hhd | | v10.ht2 | | v10.ht3 | | v10_t2.ptu | | v10_t3.ptu | | v20.hhd | | v20.ht2 | | v20.ht3 | | v20_t2.ptu | | v20_t3.ptu | | | +---picoharp | | v20.phd | | v20.pt2 | | v20.pt3 | | v30.cor | | v30_t2.ptu | | | \---timeharp | v20.thd | v30.t3r | v30.thd | v50.t3r | v50.thd | v60.thd cgohlke-ptufile-ab09357/tests/data/ptuparser/000077500000000000000000000000001515761367400212475ustar00rootroot00000000000000cgohlke-ptufile-ab09357/tests/data/ptuparser/readme.txt000066400000000000000000000001311515761367400232400ustar00rootroot00000000000000Obtained from https://pypi.org/project/ptuparser/ +---ptuparser | default_007.ptu cgohlke-ptufile-ab09357/tests/data/readPTU_FLIM/000077500000000000000000000000001515761367400213355ustar00rootroot00000000000000cgohlke-ptufile-ab09357/tests/data/readPTU_FLIM/readme.txt000066400000000000000000000003251515761367400233330ustar00rootroot00000000000000Obtained from https://github.com/SumeetRohilla/readPTU_FLIM, https://drive.google.com/file/d/1XtGL2yh_hJhaXIJhEDD5BpHNQXYQZX_p/view?usp=sharing +---readPTU_FLIM | Test_FLIM_image_daisyPollen_PicoHarp_2.ptu cgohlke-ptufile-ab09357/tests/data/tttr-data/000077500000000000000000000000001515761367400211265ustar00rootroot00000000000000cgohlke-ptufile-ab09357/tests/data/tttr-data/readme.txt000066400000000000000000000101021515761367400231160ustar00rootroot00000000000000Obtained from https://gitlab.peulen.xyz/skf/tttr-data/ +---tttr-data | | README.md | | readme.txt | | | +---imaging | | | | | +---leica | | | +---sp5 | | | | | convolaria_1.ptu | | | | | LSM_1.ptu | | | | | | | | | +---20190918_Khm_Cam-eGFP.sptw | | | | | FLIM.pqres | | | | | FLIM_1.pqres | | | | | FLIM_2.pqres | | | | | FLIM_3.pqres | | | | | LSM_1.ptu | | | | | LSM_1_OFLIM.pqres | | | | | LSM_2.ptu | | | | | LSM_2_OFLIM.pqres | | | | | LSM_3.ptu | | | | | LSM_3_OFLIM.pqres | | | | | WSLogfile.sptl | | | | | | | | | \---test FLIM.sptw | | | | | convolaria_1.ptu | | | | | convolaria_1_OFLIM.pqres | | | | | FLIM.pqres | | | | | WSLogfile.sptl | | | | | | | | | \---Headers | | | | convolaria_1_Header.txt | | | | | | | +---sp8 | | | | | IRF488_20MHz_25_1.ptu | | | | | | | | | +---d0 | | | | | G-28_S1_1_1.ptu | | | | | | | | | \---da | | | | Capture.PNG | | | | G-28_C-28_S1_6_1.ptu | | | | | | | \---sp8_2 | | | 11-2_z9.ptu | | | AF555-WGA-1.ptu | | | Capture.PNG | | | | | +---pq | | | +---ht3 | | | | 58 MEF ko mGBP7 + GFP-mGBP7 + mCherry-mGBP6 + IFNg_green.ht3 | | | | crn_clv_img.ht3 | | | | crn_clv_mirror.ht3 | | | | INFO.txt | | | | mGBP_DA.ht3 | | | | mGBP_IRF.ht3 | | | | pq_ht3_clsm.ht3 | | | | | | | +---Microtime200_HH400 | | | | beads.ptu | | | | INFO.txt | | | | | | | \---Microtime200_TH260 | | | beads.ptu | | | INFO.txt | | | | | \---zeiss | | \---lsm980_pq | | \---Training_2021-03-04.sptw | | | Alexa488_485_40MHz_MBmax.pck | | | Alexa488_485_40MHz_MBmax.pco | | | Alexa488_485_40MHz_MBmax.ptu | | | ATTO488_485_40MHz_MBfit.pck | | | ATTO488_485_40MHz_MBfit.ptu | | | FCS_Calibration.pqres | | | FCS_Grouped.pqres | | | LSM_1.pck | | | LSM_1.ptu | | | LSM_1_OFCS.pqres | | | LSM_1_OFCS_1.pqres | | | LSM_1_OFCS_2.pqres | | | LSM_1_OTCSPC.pqres | | | LSM_1_OTCSPC_1.pqres | | | LSM_1_OTCSPC_2.pqres | | | WSLogfile.sptl | | | | | +---Cell_GFP | | | AnisotropyImage.pqres | | | Cell1_T_0_P_0_Idx_4.pck | | | Cell1_T_0_P_0_Idx_4.ptu | | | Cell1_T_0_P_0_Idx_4_OFLIM.pqres | | | Cell1_T_0_P_0_Idx_4_OFLIM_1.pqres | | | Cell1_T_0_P_0_Idx_4_OTCSPC.pqres | | | FLIM.pqres | | | | | \---Cell_GFP-TAMRA | | AnisotropyImage.pqres | | AnisotropyImage_1.pqres | | AnisotropyImage_2.pqres | | Cell1_T_0_P_0_Idx_2.pck | | Cell1_T_0_P_0_Idx_2.ptu | | Cell1_T_0_P_0_Idx_2_OFLIM.pqres | | Cell1_T_0_P_0_Idx_2_OFLIM_1.pqres | | Cell1_T_0_P_0_Idx_2_OTCSPC.pqres | | FLIM.pqres | | FLIM_1.pqres | | LT-FRET.pqres | | | \---pq | +---ht3 | | pq_ht3v1.0_hh_t3.ht3 | | pq_ht3_sf-compression.ht3 | | | \---ptu | pq_ptu_hh_t2.ptu | pq_ptu_hh_t2_test2.ptu | pq_ptu_hh_t3.ptu | pq_ptu_hh_t3_cw_5GB.ptu cgohlke-ptufile-ab09357/tests/data/tttrlib/000077500000000000000000000000001515761367400207065ustar00rootroot00000000000000cgohlke-ptufile-ab09357/tests/data/tttrlib/readme.txt000066400000000000000000000002051515761367400227010ustar00rootroot00000000000000Obtained from https://github.com/Fluorescence-Tools/tttrlib/issues/24#issuecomment-864879721 +---tttrlib | 5kDa_1st_1_1_1.ptu cgohlke-ptufile-ab09357/tests/test_ptufile.py000066400000000000000000002625011515761367400214000ustar00rootroot00000000000000# test_ptufile.py # Copyright (c) 2023-2026, Christoph Gohlke # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # 3. Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. """Unittests for the ptufile package. :Version: 2026.3.21 """ import datetime import glob import io import itertools import logging import os import pathlib import sys import sysconfig import tempfile import uuid import numpy import pytest from numpy.testing import assert_almost_equal, assert_array_equal import ptufile import ptufile.numcodecs from ptufile import ( FILE_EXTENSIONS, T2_RECORD_DTYPE, T3_RECORD_DTYPE, PhuFile, PhuMeasurementMode, PhuMeasurementSubMode, PqFile, PqFileError, PqFileType, PtuFile, PtuMeasurementMode, PtuMeasurementSubMode, PtuMeasurementWarnings, PtuRecordType, PtuScannerType, PtuWriter, __version__, binread, binwrite, imread, imwrite, ) from ptufile.ptufile import BinaryFile try: import fsspec except ImportError: fsspec = None # type: ignore[assignment] try: import xarray except ImportError: xarray = None # type: ignore[assignment] try: from matplotlib import pyplot except ImportError: pyplot = None # type: ignore[assignment] RNG = numpy.random.default_rng(42) DATA = pathlib.Path(os.path.dirname(__file__)) / 'data' FILES = [ # PicoHarpT3 'napari_flim_phasor_plotter/hazelnut_FLIM_single_image.ptu', # PicoHarpT2 'Samples.sptw/Atto488_diff_cw_total_correlation.ptu', # HydraHarpT3 'picoquant-sample-data/hydraharp/v10_t3.ptu', # HydraHarpT2 'Samples.sptw/NV-Center_for_Antibunching_several.ptu', # HydraHarp2T3 'tttr-data/imaging/pq/Microtime200_HH400/beads.ptu', # HydraHarp2T2 'tttr-data/pq/ptu/pq_ptu_hh_t2_test2.ptu', # TODO: TimeHarp260NT3 # TimeHarp260NT2 'ptuparser/default_007.ptu', # TimeHarp260PT3 'tttr-data/imaging/pq/Microtime200_TH260/beads.ptu', # TODO: TimeHarp260PT2 # TODO: GenericT2/MultiHarpT2 or Picoharp330T2 # GenericT3/MultiHarpT3 'Tutorials.sptw/Hyperosmotic_Shock_MDCK_Cells.ptu', ] @pytest.mark.skipif(__doc__ is None, reason='__doc__ is None') def test_version(): """Assert ptufile versions match docstrings.""" ver = ':Version: ' + __version__ assert ver in __doc__ assert ver in ptufile.__doc__ def test_import_xarray(): """Assert xarray is installed.""" assert xarray is not None def test_import_matplotlib(): """Assert matplotlib is installed.""" assert pyplot is not None class TestBinaryFile: """Test BinaryFile with different file-like inputs.""" def setup_method(self): self.filename = os.path.normpath(DATA / 'binary.bin') if not os.path.exists(self.filename): pytest.skip(f'{self.filename!r} not found') def validate( self, fh: BinaryFile, filepath: str | None = None, filename: str | None = None, dirname: str | None = None, name: str | None = None, *, closed: bool = True, ) -> None: """Assert BinaryFile attributes.""" if filepath is None: filepath = self.filename if filename is None: filename = os.path.basename(self.filename) if dirname is None: dirname = os.path.dirname(self.filename) if name is None: name = fh.filename attrs = fh.attrs assert attrs['name'] == name assert attrs['filepath'] == filepath assert fh.filepath == filepath assert fh.filename == filename assert fh.dirname == dirname assert fh.name == name assert fh.closed is False assert len(fh.filehandle.read()) == 256 fh.filehandle.seek(10) assert fh.filehandle.tell() == 10 assert fh.filehandle.read(1) == b'\n' fh.close() # underlying filehandle may still be be open if # BinaryFile was given an open filehandle assert fh._fh.closed is closed # BinaryFile always reports itself as closed after close() is called assert fh.closed def test_str(self): """Test BinaryFile with str path.""" file = self.filename with BinaryFile(file) as fh: self.validate(fh, closed=True) def test_pathlib(self): """Test BinaryFile with pathlib.Path.""" file = pathlib.Path(self.filename) with BinaryFile(file) as fh: self.validate(fh, closed=True) def test_open_file(self): """Test BinaryFile with open binary file.""" with open(self.filename, 'rb') as fh, BinaryFile(fh) as bf: self.validate(bf, closed=False) def test_bytesio(self): """Test BinaryFile with BytesIO.""" with open(self.filename, 'rb') as fh: file = io.BytesIO(fh.read()) with BinaryFile(file) as fh: self.validate( fh, filepath='', filename='', dirname='', name='BytesIO', closed=False, ) @pytest.mark.skipif(fsspec is None, reason='fsspec not installed') def test_fsspec_openfile(self): """Test BinaryFile with fsspec OpenFile.""" file = fsspec.open(self.filename) with BinaryFile(file) as fh: self.validate(fh, closed=True) @pytest.mark.skipif(fsspec is None, reason='fsspec not installed') def test_fsspec_localfileopener(self): """Test BinaryFile with fsspec LocalFileOpener.""" with fsspec.open(self.filename) as file, BinaryFile(file) as fh: self.validate(fh, closed=False) def test_text_file_fails(self): """Test BinaryFile with open text file fails.""" with open(self.filename) as fh: # noqa: SIM117 with pytest.raises(TypeError): BinaryFile(fh) def test_file_extension_fails(self): """Test BinaryFile with wrong file extension fails.""" ext = BinaryFile._ext BinaryFile._ext = {'.lif'} try: with pytest.raises(ValueError): BinaryFile(self.filename) finally: BinaryFile._ext = ext def test_file_not_seekable(self): """Test BinaryFile with non-seekable file fails.""" class File: # mock file object without tell methods def seek(self): pass with pytest.raises(ValueError): BinaryFile(File) def test_openfile_not_seekable(self): """Test BinaryFile with non-seekable file fails.""" class File: # mock fsspec OpenFile without seek/tell methods @staticmethod def open(*args, **kwargs): del args, kwargs return File() with pytest.raises(ValueError): BinaryFile(File) def test_invalid_object(self): """Test BinaryFile with invalid file object fails.""" class File: # mock non-file object pass with pytest.raises(TypeError): BinaryFile(File) def test_invalid_mode(self): """Test BinaryFile with invalid mode fails.""" with pytest.raises(ValueError): BinaryFile(self.filename, mode='ab') @pytest.mark.parametrize('memmap', [False, True]) def test_binread(memmap): """Test read and write PicoQuant BIN file.""" filename = DATA / 'UNC/805.bin' data, attrs = binread(filename, memmap=memmap) assert attrs['shape'] == (256, 256, 2000) assert attrs['pixel_resolution'] == 0.078125 assert attrs['tcspc_resolution'] == 2.5000000372529032e-11 assert data.shape == (256, 256, 2000) assert data.dtype == numpy.uint32 assert numpy.sum(data) == 43071870 with tempfile.NamedTemporaryFile(suffix='.bin', delete=False) as tmp: fnout = tmp.name try: binwrite( fnout, data, attrs['tcspc_resolution'], pixel_resolution=attrs['pixel_resolution'], ) del data with open(filename, 'rb') as fh: data1 = fh.read() with open(fnout, 'rb') as fh: data2 = fh.read() assert data1 == data2 finally: if os.path.exists(fnout): os.remove(fnout) def test_non_pqfile(): """Test read non-PicoQuant file fails.""" filename = DATA / 'FRET_GFP and mRFP.pt3' with pytest.raises(PqFileError): # noqa: SIM117 with PqFile(filename): pass def test_non_ptu(): """Test read non-PTU file fails.""" filename = DATA / 'Settings.pfs' with pytest.raises(PqFileError): # noqa: SIM117 with PtuFile(filename): pass def test_pq_fastload(): """Test read tags using fastload.""" filename = ( DATA / 'napari_flim_phasor_plotter/hazelnut_FLIM_single_image.ptu' ) with PqFile(filename, fastload=True) as pq: str(pq) assert pq.tags['File_GUID'] == '{4f6e5f68-8289-483d-9d9a-7974b77ef8b8}' assert 'HW_ExternalRefClock' not in pq.tags def test_pck(): """Test read PCK file.""" filename = DATA / 'Tutorials.sptw/IRF_Fluorescein.pck' with PqFile(filename) as pq: str(pq) assert pq.type == PqFileType.PCK assert pq.version == '1.0.00' assert pq.tags['File_Comment'].startswith('Check point file of ') assert_array_equal( pq.tags['ChkHistogram'][:6], [96, 150, 151, 163, 153, 145] ) attrs = pq.attrs assert attrs['type'] == pq.type.name assert attrs['name'] == pq.name assert attrs['tags'] == pq.tags def test_pco(): """Test read PCO file.""" filename = DATA / 'Tutorials.sptw/Hyperosmotic_Shock_MDCK_Cell.pco' with PqFile(filename) as pq: str(pq) assert pq.type == PqFileType.PCO assert pq.version == '1.0.00' assert pq.tags['CreatorSW_Modules'] == 0 attrs = pq.attrs assert attrs['type'] == pq.type.name assert attrs['name'] == pq.name assert attrs['tags'] == pq.tags def test_pfs(): """Test read PFS file.""" filename = DATA / 'Settings.pfs' with PqFile(filename) as pq: str(pq) assert pq.type == PqFileType.PFS assert pq.version == '1.0.00' assert pq.tags['HW_SerialNo'] == '' assert pq.tags['Defaults_Begin'] is None attrs = pq.attrs assert attrs['type'] == pq.type.name assert attrs['name'] == pq.name assert attrs['tags'] == pq.tags def test_pqres(caplog): """Test read PQRES file.""" filename = DATA / 'Samples.sptw/AnisotropyImage.pqres' with caplog.at_level(logging.ERROR): with PqFile(filename) as pq: str(pq) # assert 'not divisible by 8' in caplog.text assert pq.type == PqFileType.PQRES assert pq.version == '00.0.1' assert pq.tags['VarStatFilterGrpIdx'].startswith(b'\xe7/\x00\x00') attrs = pq.attrs assert attrs['type'] == pq.type.name assert attrs['name'] == pq.name assert attrs['tags'] == pq.tags def test_pqdat(caplog): """Test read PQDAT file.""" filename = DATA / 'Luminosa/FRET_20230606-185222/FittedCurveIRF.pqdat' with caplog.at_level(logging.ERROR), PqFile(filename) as pq: str(pq) # assert 'not divisible by 8' in caplog.text assert pq.type == PqFileType.PQDAT assert pq.version == '1.0.00' assert len(pq.tags['LSDCurveX']) == 1254 assert len(pq.tags['LSDCurveY']) == 1254 preview = numpy.frombuffer( pq.tags['PreviewImage'][4:], dtype=numpy.uint8 ).reshape((128, 128, 4)) assert preview.shape == (128, 128, 4) attrs = pq.attrs assert attrs['type'] == pq.type.name assert attrs['name'] == pq.name assert attrs['tags'] == pq.tags def test_pquni(): """Test read PQUNI file.""" # TODO need better PqUni test data filename = DATA / 'UniHarp/MicroBeads.PqUni' with PqFile(filename) as pq: str(pq) # assert 'not divisible by 8' in caplog.text assert pq.type == PqFileType.PQUNI assert pq.version == '1.0.0.0' assert pq.comment is None assert pq.datetime == datetime.datetime( 2025, 11, 14, 12, 15, 46, 665000 ) assert pq.guid == uuid.UUID('a8210025-5de6-418a-841c-186da82e169e') tags = pq.tags assert tags['File_GUID'] == '{A8210025-5DE6-418A-841C-186DA82E169E}' assert tags['CreatorSW_Name'] == 'UniHarp' assert tags['CreatorSW_Version'] == '1.1.1.130' assert tags['File_CreatingTime'] == datetime.datetime( 2025, 11, 14, 12, 15, 46, 665000 ) assert tags['CreatorSW_Modules'] == 0 assert tags['HistoResult_NumberOfCurves'] == 0 attrs = pq.attrs assert attrs['type'] == pq.type.name assert attrs['name'] == pq.name assert attrs['tags'] == pq.tags def test_spqr(): """Test read SPQR file.""" filename = ( DATA / 'Luminosa/GattaQUant_Cells_FLIM/GattaQUant_Cells_FLIM.spqr' ) with PqFile(filename) as pq: str(pq) assert pq.type == PqFileType.SPQR assert pq.version == '1.0.00' assert pq.tags['CreatorSW_Name'] == 'NovaConvert' preview = numpy.frombuffer( pq.tags['SPQRPrevImage'], dtype=numpy.uint8 ).reshape((128, 128, 4)) assert preview.shape == (128, 128, 4) assert len(pq.tags['SPQRBinWidths']) == 128 attrs = pq.attrs assert attrs['type'] == pq.type.name assert attrs['name'] == pq.name assert attrs['tags'] == pq.tags @pytest.mark.parametrize('filetype', [str, io.BytesIO]) def test_ptu(filetype): """Test read PTU file.""" filename = ( DATA / 'napari_flim_phasor_plotter/hazelnut_FLIM_single_image.ptu' ) if filetype is not str: filename = open(filename, 'rb') try: with PtuFile(filename) as ptu: str(ptu) assert ptu.type == PqFileType.PTU assert ptu.record_type == PtuRecordType.PicoHarpT3 assert ptu.measurement_mode == PtuMeasurementMode.T3 assert ptu.measurement_submode == PtuMeasurementSubMode.IMAGE assert ptu.scanner == PtuScannerType.LSM assert ptu.measurement_ndim == 3 assert ptu.filehandle is not None if filetype is str: assert ptu.filename == str(filename.name) assert ptu.dirname == str(filename.parent) assert ptu.version == '00.0.1' assert ptu.comment == '' assert ptu.datetime is None assert str(ptu.guid) == '4f6e5f68-8289-483d-9d9a-7974b77ef8b8' assert ptu.tags['TTResultFormat_BitsPerRecord'] == 32 assert ptu.tags['\x02HWInpChan_CFDLeveld'] == [100] # corrupted? assert not ptu.tags['HW_ExternalRefClock'] attrs = ptu.attrs assert attrs['type'] == ptu.type.name assert attrs['name'] == ptu.name assert attrs['guid'] == str(ptu.guid) assert attrs['datetime'] is None assert attrs['tags'] == ptu.tags assert attrs['acquisition_time'] == ptu.acquisition_time assert attrs['active_channels'] == ptu.active_channels assert attrs['frame_time'] == ptu.frame_time assert attrs['frequency'] == ptu.frequency assert ( attrs['global_acquisition_time'] == ptu.global_acquisition_time ) assert attrs['global_frame_time'] == ptu.global_frame_time assert attrs['global_line_time'] == ptu.global_line_time assert attrs['global_pixel_time'] == ptu.global_pixel_time assert attrs['global_resolution'] == ptu.global_resolution assert attrs['line_time'] == ptu.line_time assert ( attrs['max_delaytime'] == ptu.number_bins_max ) # for PhasorPy assert attrs['measurement_mode'] == ptu.measurement_mode.name assert attrs['measurement_submode'] == ptu.measurement_submode.name assert attrs['number_bins'] == ptu.number_bins assert attrs['number_bins_in_period'] == ptu.number_bins_in_period assert attrs['number_bins_max'] == ptu.number_bins_max assert attrs['pixel_time'] == ptu.pixel_time assert attrs['record_type'] == ptu.record_type.name assert attrs['scanner'] == ptu.scanner.name assert attrs['syncrate'] == ptu.syncrate assert attrs['tcspc_resolution'] == ptu.tcspc_resolution # decoding of records is tested separately finally: if filetype is not str: filename.close() @pytest.mark.parametrize('filetype', [str, io.BytesIO]) def test_phu(filetype): """Test read PHU file.""" filename = DATA / 'TimeHarp/Decay_Coumarin_6.phu' if filetype is not str: filename = open(filename, 'rb') try: with PhuFile(filename) as phu: str(phu) assert phu.type == PqFileType.PHU assert phu.measurement_mode == PhuMeasurementMode.HISTOGRAM assert phu.measurement_submode == PhuMeasurementSubMode.INTEGRATING assert phu.version == '1.1.00' assert not phu.tags['HWTriggerOut_On'] assert phu.tcspc_resolution == 2.5e-11 assert phu.number_histograms == 4 # assert phu.histogram_resolutions == (3e-11, 3e-11, 3e-11, 3e-11) attrs = phu.attrs assert attrs['type'] == phu.type.name assert attrs['name'] == phu.name assert attrs['tags'] == phu.tags assert attrs['measurement_mode'] == phu.measurement_mode.name assert attrs['measurement_submode'] == phu.measurement_submode.name assert attrs['tcspc_resolution'] == phu.tcspc_resolution # assert attrs['histogram_resolutions']==phu.histogram_resolutions assert_array_equal( phu.tags['HistResDscr_DataOffset'], [11224, 142296, 273368, 404440], ) if xarray is not None: histograms = phu.histograms(asxarray=True) assert len(histograms) == 4 for h in histograms: assert h.shape == (32768,) assert histograms[2][1] == 3 assert_array_equal(phu.histograms(2)[0], histograms[2]) if pyplot is not None: phu.plot(show=False, verbose=False) phu.plot(show=False, verbose=True) finally: if filetype is not str: filename.close() def test_phu_baseres(): """Test read PHU file without HistResDscr_HWBaseResolution.""" # also has a non-ISO datetime string format filename = DATA / 'FluoPlot/Naphtal_BuOH_TRES.phu' with PhuFile(filename) as phu: str(phu) assert phu.type == PqFileType.PHU assert phu.measurement_mode == PhuMeasurementMode.HISTOGRAM assert phu.measurement_submode == PhuMeasurementSubMode.INTEGRATING assert phu.version == '1.0.00' assert phu.tags['File_CreatingTime'] == '29/11/06 18:51:08' assert phu.datetime == datetime.datetime(2006, 11, 29, 18, 51, 8) assert 'HistResDscr_HWBaseResolution' not in phu.tags assert phu.tcspc_resolution == 1.600000075995922e-11 assert phu.number_histograms == 42 if xarray is not None: histograms = phu.histograms(asxarray=True) assert len(histograms) == 42 for h in histograms: assert h.shape == (65536,) if pyplot is not None: phu.plot(show=False, verbose=False) phu.plot(show=False, verbose=True) def test_ptu_t3_image(): """Test decode T3 image.""" filename = ( DATA / 'napari_flim_phasor_plotter/hazelnut_FLIM_single_image.ptu' ) with PtuFile(filename) as ptu: str(ptu) assert str(ptu.guid) == '4f6e5f68-8289-483d-9d9a-7974b77ef8b8' assert ptu.version == '00.0.1' # assert ptu._data_offset == 4616 assert ptu.type == PqFileType.PTU assert ptu.record_type == PtuRecordType.PicoHarpT3 assert ptu.measurement_mode == PtuMeasurementMode.T3 assert ptu.measurement_submode == PtuMeasurementSubMode.IMAGE assert ptu.scanner == PtuScannerType.LSM assert ptu.measurement_ndim == 3 assert ptu.is_image assert ptu.is_t3 assert ptu.shape == (5, 256, 256, 1, 139) assert ptu.dims == ('T', 'Y', 'X', 'C', 'H') assert tuple(ptu.coords.keys()) == ('T', 'Y', 'X', 'C', 'H') assert ptu.active_channels == (0,) # TODO: verify coords assert ptu.frame_change_mask == 4 assert ptu.line_start_mask == 1 assert ptu.line_stop_mask == 2 assert ptu.acquisition_time == 2.074774673160728 assert ptu.frame_time == 0.4149549346321456 assert ptu.frequency == 78020000.0 assert ptu.global_frame_time == 32374784 assert ptu.global_line_time == 126464 assert ptu.global_pixel_time == 494 assert ptu.global_resolution == 1.281722635221738e-8 assert ptu.pixel_time == pytest.approx( ptu.global_pixel_time * ptu.global_resolution, rel=1e-3 ) assert ptu.line_time == pytest.approx( ptu.global_line_time * ptu.global_resolution, rel=1e-3 ) assert ptu.lines_in_frame == 256 assert ptu.number_bins == 139 assert ptu.number_bins_in_period == 132 assert ptu.number_bins_max == 4096 assert ptu.number_channels == 1 assert ptu.number_channels_max == 4 assert ptu.number_lines == 1280 assert ptu.number_markers == 2565 assert ptu.number_photons == 6065123 assert ptu.number_records == 6070158 assert ptu.pixels_in_frame == 65536 assert ptu.pixels_in_line == 256 assert ptu.syncrate == 78020000 assert ptu.tcspc_resolution == 9.696969697e-11 assert len(ptu.read_records()) == ptu.number_records # assert ptu.decode_records im0 = ptu.decode_image() assert im0.shape == (5, 256, 256, 1, 139) assert im0.dtype == numpy.uint16 im = ptu.decode_image(channel=0, frame=2, dtime=-1, dtype='uint32') assert im.shape == (1, 256, 256, 1, 1) assert im.dtype == numpy.uint32 assert_array_equal(im[0, ..., 0, 0], im0[2, :, :, 0].sum(axis=-1)) im = ptu.decode_image(channel=0, frame=2, dtime=99, dtype='uint32') assert im.shape == (1, 256, 256, 1, 99) assert_array_equal( im[0, :, :, 0].sum(axis=-1), im0[2, :, :, 0, :99].sum(axis=-1) ) im = ptu.decode_image(channel=0, frame=2, dtime=199, dtype='uint32') assert im.shape == (1, 256, 256, 1, 199) assert_array_equal( im[0, :, :, 0].sum(axis=-1), im0[2, :, :, 0].sum(axis=-1) ) im = ptu.decode_image( [2, slice(0, 32), slice(100, 132), None, slice(None, None, -1)] ) assert im.shape == (1, 32, 32, 1, 1) assert_array_equal(im[..., 0], im0[2:3, :32, 100:132].sum(axis=-1)) im = ptu.decode_image( [slice(1, None, 2)], # bin by 2 frames starting from second dtime=-1, ) assert im.shape == (2, 256, 256, 1, 1) assert_array_equal( im[1, :, :, :, 0], im0[3:5].sum(axis=0).sum(axis=-1) ) # TODO: verify values with pytest.raises(ValueError): ptu.decode_image(dtype='int16') def test_ptu_channels(): """Test decode T3 image with empty leading channels.""" filename = DATA / 'Tutorials.sptw/Kidney _Cell_FLIM.ptu' with PtuFile(filename) as ptu: str(ptu) assert str(ptu.guid) == 'b767c46e-9693-4ad9-9fcf-7fab5e4377fc' assert ptu.version == '1.0.00' assert ptu.comment.startswith('SPAD-CH1523') assert ptu.datetime == datetime.datetime(2020, 4, 7, 18, 8, 44, 860000) assert ptu.type == PqFileType.PTU assert ptu.record_type == PtuRecordType.GenericT3 assert ptu.measurement_mode == PtuMeasurementMode.T3 assert ptu.measurement_submode == PtuMeasurementSubMode.IMAGE assert ptu.scanner == PtuScannerType.FLIMBEE assert ptu.measurement_ndim == 3 assert ptu.is_image assert ptu.is_t3 assert ptu.shape == (3, 512, 512, 2, 501) assert ptu.dims == ('T', 'Y', 'X', 'C', 'H') assert tuple(ptu.coords.keys()) == ('T', 'Y', 'X', 'C', 'H') assert ptu.active_channels == (2, 3) assert_array_equal(ptu.coords['C'], (2, 3)) # TODO: verify coords assert ptu.frame_change_mask == 4 assert ptu.line_start_mask == 1 assert ptu.line_stop_mask == 2 assert ptu.acquisition_time == 47.26384024428878 assert ptu.frame_time == 15.754613414762927 assert ptu.frequency == 24999920.0 assert ptu.global_frame_time == 393864075 assert ptu.global_line_time == 384000 assert ptu.global_pixel_time == 750 assert ptu.global_resolution == 4.00001280004096e-8 assert ptu.pixel_time == pytest.approx( ptu.global_pixel_time * ptu.global_resolution, rel=1e-3 ) assert ptu.line_time == pytest.approx( ptu.global_line_time * ptu.global_resolution, rel=1e-3 ) assert ptu.lines_in_frame == 512 assert ptu.number_bins == 501 assert ptu.number_bins_in_period == 500 assert ptu.number_bins_max == 32768 assert ptu.number_channels == 2 assert ptu.number_channels_max == 64 assert ptu.number_lines == 1536 assert ptu.number_markers == 3074 assert ptu.number_photons == 41039565 assert ptu.number_records == 42196537 assert ptu.pixels_in_frame == 262144 assert ptu.pixels_in_line == 512 assert ptu.syncrate == 24999920 assert ptu.tcspc_resolution == 7.999999968033578e-11 if xarray is None: return # skip xarray tests if not installed histogram = ptu.decode_histogram(asxarray=True) assert histogram.shape == (2, 501) assert_array_equal(histogram.coords['C'], (2, 3)) assert_array_equal(histogram[1].coords['C'], (3,)) histogram = ptu.decode_histogram(asxarray=True, dtime=0) assert histogram.shape == (2, 500) histogram = ptu.decode_histogram(asxarray=True, dtime=100) assert histogram.shape == (2, 100) image = ptu.decode_image(asxarray=True, dtime=-1) assert_array_equal(image.coords['C'], (2, 3)) assert_array_equal(image[..., 1, :].coords['C'], (3,)) assert_array_equal(ptu._coords_c, (2, 3)) image = ptu.decode_image(asxarray=True, channel=1, dtime=-1) assert_array_equal(image.coords['C'], (3,)) image = ptu.decode_image( asxarray=True, channel=1, dtime=-1, keepdims=False ) assert 'C' not in image.coords with PtuFile(filename, trimdims='') as ptu: str(ptu) assert ptu.shape == (3, 512, 512, 64, 32768) assert ptu.dims == ('T', 'Y', 'X', 'C', 'H') assert tuple(ptu.coords.keys()) == ('T', 'Y', 'X', 'C', 'H') assert_array_equal(ptu.coords['C'][[0, -1]], (0, 63)) assert_array_equal(ptu._coords_c[[0, -1]], (0, 63)) if xarray is not None: image = ptu.decode_image(asxarray=True, dtime=-1, channel=2) assert_array_equal(image.coords['C'], (2,)) def test_ptu_t3_sinusoidal(): """Test decode T3 image with sinusoidal correction.""" filename = DATA / 'tttrlib/5kDa_1st_1_1_1.ptu' with PtuFile(filename) as ptu: str(ptu) assert ptu.version == '1.0.00' # assert ptu._data_offset == 4616 assert ptu.type == PqFileType.PTU assert ptu.record_type == PtuRecordType.PicoHarpT3 assert ptu.measurement_mode == PtuMeasurementMode.T3 assert ptu.measurement_submode == PtuMeasurementSubMode.IMAGE assert ptu.scanner == PtuScannerType.LSM assert ptu.measurement_ndim == 3 assert ptu.is_t3 assert ptu.is_image assert ptu.is_sinusoidal assert not ptu.is_bidirectional assert ptu.tags['ImgHdr_SinCorrection'] == 80 assert ptu.shape == (122, 512, 512, 1, 3216) assert ptu.dims == ('T', 'Y', 'X', 'C', 'H') assert tuple(ptu.coords.keys()) == ('T', 'Y', 'X', 'C', 'H') assert ptu.active_channels == (0,) # TODO: verify coords assert ptu.frame_change_mask == 4 assert ptu.line_start_mask == 1 assert ptu.line_stop_mask == 2 assert ptu.acquisition_time == 118.88305798296687 assert ptu.frame_time == 0.9744512873563691 assert ptu.frequency == 38898320.0 assert ptu.global_acquisition_time == 4624351232 assert ptu.global_frame_time == 37904518 assert ptu.global_line_time == 18995 assert ptu.global_pixel_time == 37 assert ptu.global_resolution == 2.5708051144625268e-8 assert ptu.pixel_time == pytest.approx( ptu.global_pixel_time * ptu.global_resolution, rel=1e-3 ) assert ptu.line_time == pytest.approx( ptu.global_line_time * ptu.global_resolution, rel=1e-3 ) assert ptu.lines_in_frame == 512 assert ptu.number_bins == 3216 assert ptu.number_bins_in_period == 3213 assert ptu.number_bins_max == 4096 assert ptu.number_channels == 1 assert ptu.number_channels_max == 4 assert ptu.number_images == 122 assert ptu.number_lines == 62464 assert ptu.number_markers == 125050 assert ptu.number_photons == 8664782 assert ptu.number_records == 8860394 assert ptu.pixels_in_frame == 262144 assert ptu.pixels_in_line == 512 assert ptu.syncrate == 38898320 assert ptu.tcspc_resolution == 7.999999968033578e-12 records = ptu.read_records() assert len(records) == ptu.number_records im = ptu.decode_image(frame=-1, dtime=-1, channel=0, keepdims=False) assert im.shape == (512, 512) assert im[399, 18] == 37 with pytest.raises(ValueError): ptu.decode_image(pixel_time=0) def test_ptu_t3_bidirectional(): """Test decode T3 image acquired with bidirectional scanning.""" filename = DATA / 'fastFLIM/A2_Shep2_26.ptu' with PtuFile(filename) as ptu: str(ptu) assert ptu.version == '1.0.00' # assert ptu._data_offset == 4616 assert ptu.type == PqFileType.PTU assert ptu.record_type == PtuRecordType.GenericT3 assert ptu.measurement_mode == PtuMeasurementMode.T3 assert ptu.measurement_submode == PtuMeasurementSubMode.IMAGE assert ptu.scanner == PtuScannerType.FLIMBEE assert ptu.measurement_ndim == 3 assert ptu.is_t3 assert ptu.is_image assert ptu.is_bidirectional assert not ptu.is_sinusoidal assert ptu.tags['ImgHdr_BiDirect'] assert ptu.shape == (1, 1024, 1024, 2, 502) assert ptu.dims == ('T', 'Y', 'X', 'C', 'H') assert tuple(ptu.coords.keys()) == ('T', 'Y', 'X', 'C', 'H') assert ptu.active_channels == (0, 1) # TODO: verify coords assert ptu.frame_change_mask == 4 assert ptu.line_start_mask == 1 assert ptu.line_stop_mask == 2 assert ptu.acquisition_time == 192.27363031561703 assert ptu.frame_time == 192.27363031561703 assert ptu.frequency == 24999920.0 assert ptu.global_acquisition_time == 4806825376 assert ptu.global_frame_time == 4806825376 assert ptu.global_line_time == 3200000 assert ptu.global_pixel_time == 3125 assert ptu.global_resolution == 4.00001280004096e-8 assert ptu.pixel_time == pytest.approx( ptu.global_pixel_time * ptu.global_resolution, rel=1e-3 ) assert ptu.line_time == pytest.approx( ptu.global_line_time * ptu.global_resolution, rel=1e-3 ) assert ptu.lines_in_frame == 1024 assert ptu.number_bins == 502 assert ptu.number_bins_in_period == 500 assert ptu.number_bins_max == 32768 assert ptu.number_channels == 2 assert ptu.number_channels_max == 64 assert ptu.number_images == 1 assert ptu.number_lines == 1024 assert ptu.number_markers == 2049 assert ptu.number_photons == 239232451 assert ptu.number_records == 242488255 assert ptu.pixels_in_frame == 1048576 assert ptu.pixels_in_line == 1024 assert ptu.syncrate == 24999920 assert ptu.tcspc_resolution == 7.999999968033578e-11 records = ptu.read_records() assert len(records) == ptu.number_records im = ptu.decode_image(frame=-1, dtime=-1, channel=0, keepdims=False) assert im.shape == (1024, 1024) # TODO: compare to ground truth image from SymPhoTime assert im[430, 430] == 1057 # even line assert im[431, 431] == 1050 # odd line # selection m, n = 421, 440 selection = (0, slice(m, n), slice(m, n), 0, slice(None, None, -1)) im1 = ptu.decode_image(selection, records=records, keepdims=False) assert_array_equal(im[m:n, m:n], im1) # x-shift by one pixel im = ptu.decode_image( records=records, frame=-1, dtime=-1, channel=0, bishift=-ptu.global_pixel_time, keepdims=False, ) assert im[430, 430] == 1057 # even line is same assert im[431, 430] == 1050 # odd line shifted def test_ptu_t3_bidirectional_sinusoidal(): """Test decode T3 image acquired with bidirectional sinusoidal scanning.""" filename = DATA / 'tttr-data/imaging/leica/sp8/d0/G-28_S1_1_1.ptu' with PtuFile(filename) as ptu: str(ptu) assert ptu.version == '1.0.00' # assert ptu._data_offset == 5816 assert ptu.type == PqFileType.PTU assert ptu.record_type == PtuRecordType.PicoHarpT3 assert ptu.measurement_mode == PtuMeasurementMode.T3 assert ptu.measurement_submode == PtuMeasurementSubMode.IMAGE assert ptu.scanner == PtuScannerType.LSM assert ptu.measurement_ndim == 3 assert ptu.is_t3 assert ptu.is_image assert ptu.is_bidirectional assert ptu.is_sinusoidal assert ptu.tags['ImgHdr_BiDirect'] assert ptu.shape == (26, 512, 512, 1, 3212) assert ptu.dims == ('T', 'Y', 'X', 'C', 'H') assert tuple(ptu.coords.keys()) == ('T', 'Y', 'X', 'C', 'H') assert ptu.active_channels == (0,) # TODO: verify coords assert ptu.frame_change_mask == 4 assert ptu.line_start_mask == 1 assert ptu.line_stop_mask == 2 assert ptu.acquisition_time == 14.697189081520644 assert ptu.frame_time == 0.5652764873231677 assert ptu.frequency == 19459120.0 assert ptu.global_acquisition_time == 285994366 assert ptu.global_frame_time == 10999783 assert ptu.global_line_time == 9534 assert ptu.global_pixel_time == 19 assert ptu.global_resolution == 5.138978535514453e-8 assert ptu.pixel_time == pytest.approx( ptu.global_pixel_time * ptu.global_resolution, rel=1e-3 ) assert ptu.line_time == pytest.approx( ptu.global_line_time * ptu.global_resolution, rel=1e-3 ) assert ptu.lines_in_frame == 512 assert ptu.number_bins == 3212 assert ptu.number_bins_in_period == 3211 assert ptu.number_bins_max == 4096 assert ptu.number_channels == 1 assert ptu.number_channels_max == 4 assert ptu.number_images == 26 assert ptu.number_lines == 13658 assert ptu.number_markers == 27341 assert ptu.number_photons == 5585391 assert ptu.number_records == 5617095 assert ptu.pixels_in_frame == 262144 assert ptu.pixels_in_line == 512 assert ptu.syncrate == 19459120 assert ptu.tcspc_resolution == 1.5999999936067155e-11 records = ptu.read_records() assert len(records) == ptu.number_records im = ptu.decode_image(frame=-1, dtime=-1, channel=0, keepdims=False) assert im.shape == (512, 512) # TODO: compare to ground truth image from SymPhoTime assert im[430, 430] == 38 # even line assert im[431, 431] == 32 # odd line # selection m, n = 421, 440 selection = ( slice(None, None, -1), slice(m, n), slice(m, n), 0, slice(None, None, -1), ) im1 = ptu.decode_image(selection, records=records, keepdims=False) assert_array_equal(im[m:n, m:n], im1) # x-shift by one pixel im = ptu.decode_image( records=records, frame=-1, dtime=-1, channel=0, bishift=-ptu.global_pixel_time, keepdims=False, ) assert im[430, 430] == 38 # even line is same assert im[431, 430] == 32 # odd line shifted @pytest.mark.skip('no test file available') def test_ptu_t3_line(): """Test decode T3 line scan.""" def test_ptu_t3_point(): """Test decode T3 point scan.""" filename = DATA / '1XEGFP_1.ptu' with PtuFile(filename) as ptu: str(ptu) assert str(ptu.guid) == 'dec6a033-99a9-482d-afbd-5b5743a25133' assert ptu.version == '1.0.00' assert ptu.type == PqFileType.PTU assert ptu.record_type == PtuRecordType.PicoHarpT3 assert ptu.measurement_mode == PtuMeasurementMode.T3 assert ptu.measurement_submode == PtuMeasurementSubMode.POINT assert ptu.scanner == PtuScannerType.PI_E710 assert ptu.measurement_ndim == 1 assert not ptu.is_image assert not ptu.is_bidirectional assert not ptu.is_sinusoidal assert ptu.is_t3 assert ptu.shape == (287, 2, 1564) assert ptu.dims == ('T', 'C', 'H') assert tuple(ptu.coords.keys()) == ('T', 'C', 'H') assert ptu.active_channels == (0, 1) # TODO: verify coords assert ptu.frame_change_mask == 0 assert ptu.line_start_mask == 0 assert ptu.line_stop_mask == 0 assert ptu.acquisition_time == 59.998948937161735 assert ptu.frame_time == 0.0010000110003960143 assert ptu.frequency == 39998560.0 assert ptu.global_frame_time == 39999 assert ptu.global_line_time == 39999 assert ptu.global_pixel_time == 39999 assert ptu.global_resolution == 2.5000900032401165e-8 assert ptu.pixel_time == pytest.approx( ptu.global_pixel_time * ptu.global_resolution, rel=1e-3 ) assert ptu.line_time == pytest.approx( ptu.global_line_time * ptu.global_resolution, rel=1e-3 ) assert ptu.lines_in_frame == 1 assert ptu.number_bins == 1564 assert ptu.number_bins_in_period == 1562 assert ptu.number_bins_max == 4096 assert ptu.number_channels == 2 assert ptu.number_channels_max == 4 assert ptu.number_lines == 0 assert ptu.number_markers == 0 assert ptu.number_photons == 11516799 assert ptu.number_records == 11553418 assert ptu.pixels_in_frame == 1 assert ptu.pixels_in_line == 1 assert ptu.syncrate == 39998560 assert ptu.tcspc_resolution == 1.5999999936067155e-11 assert len(ptu.read_records()) == ptu.number_records # assert ptu.decode_records im0 = ptu.decode_image() assert im0.shape == (287, 2, 1564) assert im0.dtype == numpy.uint16 im = ptu.decode_image(channel=0, frame=2, dtime=-1, dtype='uint32') assert im.shape == (1, 1, 1) assert im.dtype == numpy.uint32 # TODO: verify values @pytest.mark.parametrize('filename', FILES) def test_ptu_decode_records(filename): """Test decode records.""" with PtuFile(DATA / filename) as ptu: decoded = ptu.decode_records() assert decoded.dtype == ( T3_RECORD_DTYPE if ptu.is_t3 else T2_RECORD_DTYPE ) assert decoded.size == ptu.number_records assert decoded['time'][-1] == ptu.global_acquisition_time assert decoded['channel'].max() + 1 == ptu.number_channels assert decoded[decoded['channel'] >= 0].size == ptu.number_photons assert decoded[decoded['marker'] > 0].size == ptu.number_markers nframes = decoded[decoded['marker'] & ptu.frame_change_mask > 0].size if ptu.shape and ptu.tags['Measurement_SubMode'] > 0: assert abs(nframes - ptu.shape[0]) < 2 if ptu.is_t3: assert decoded['dtime'].max() + 1 == ptu.number_bins assert decoded[decoded['dtime'] >= 0].size == ptu.number_photons # TODO: verify values @pytest.mark.parametrize('asxarray', [False, True]) @pytest.mark.parametrize('filename', FILES) def test_ptu_decode_histogram(filename, asxarray): """Test decode histograms.""" if asxarray and xarray is None: pytest.skip('xarray not installed') with PtuFile(DATA / filename) as ptu: ptu.decode_histogram(asxarray=asxarray) # TODO: verify values with pytest.raises(ValueError): ptu.decode_histogram(dtype='int32') @pytest.mark.skipif(pyplot is None, reason='matplotlib not installed') @pytest.mark.parametrize('verbose', [False, True]) @pytest.mark.parametrize('filename', FILES) def test_ptu_plot(filename, verbose): """Test plot methods.""" with PtuFile(DATA / filename) as ptu: ptu.plot(show=False, verbose=verbose) def test_ptu_read_records(): """Test PTU read_records method.""" # the file is tested in test_issue_skip_frame filename = DATA / 'Samples.sptw/GUVs.ptu' with PtuFile(filename, mode='r+') as ptu: # use cached memory map of records records = ptu.read_records(memmap='r+') assert ptu.cache_records assert isinstance(records, numpy.memmap), type(records) assert records.size == ptu.number_records assert records is ptu.read_records() # retrieve from cache im0 = ptu.decode_image(records=records, frame=1, channel=1, dtime=-1) del records # disable caching ptu.cache_records = False assert not ptu.cache_records assert ptu._records is None records = ptu.read_records() assert ptu._records is None assert isinstance(records, numpy.ndarray), type(records) assert ptu.read_records() is not records # not from cache im1 = ptu.decode_image(records=records, frame=1, channel=1, dtime=-1) assert_array_equal(im0, im1) # memory map without caching records = ptu.read_records(memmap=True) assert isinstance(records, numpy.memmap), type(records) im1 = ptu.decode_image(records=records, frame=1, channel=1, dtime=-1) assert_array_equal(im0, im1) del records with pytest.raises(ValueError): ptu.read_records(memmap='abc') @pytest.mark.parametrize('output', ['ndarray', 'memmap', 'filename']) def test_ptu_output(output): """Test PTU decoding to different output.""" # the file is tested in test_issue_skip_frame filename = DATA / 'Samples.sptw/GUVs.ptu' with PtuFile(filename) as ptu: assert ptu.shape == (100, 512, 512, 2, 4096) selection = ( slice(11, 66, 3), Ellipsis, slice(1, 2), slice(None, None, -1), ) shape = (19, 512, 512, 1, 1) if output == 'ndarray': out = numpy.zeros(shape, 'uint32') elif output == 'filename': out = tempfile.TemporaryFile() else: out = 'memmap' im = ptu.decode_image(selection, out=out) if output == 'ndarray': im = out else: assert isinstance(im, numpy.memmap) assert im[:, 281, 373, 0, 0].sum(axis=0) == 157 if output == 'filename': out.close() def test_ptu_getitem(): """Test slice PTU.""" # the file is tested in test_issue_skip_frame filename = DATA / 'Samples.sptw/GUVs.ptu' with PtuFile(filename) as ptu: assert ptu.shape == (100, 512, 512, 2, 4096) assert ptu.dims == ('T', 'Y', 'X', 'C', 'H') assert not ptu.use_xarray with pytest.raises(ValueError): ptu.dtype = 'int32' # not an unsigned integer assert ptu.dtype == 'uint16' ptu.dtype = 'uint32' assert ptu.dtype == 'uint32' # decode im0 = ptu.decode_image( (slice(11, 66, 3), Ellipsis, slice(1, 2), slice(None, None, -1)), keepdims=False, ) assert im0.shape == (19, 512, 512, 1) assert im0.dtype == 'uint32' assert im0[:, 281, 373, 0].sum(axis=0) == 157 # slice im = ptu[-89:-34:3, ..., :, 1:2, ::-1] assert im0.dtype == 'uint32' assert_array_equal(im, im0) ptu.dtype = 'uint16' # slice uint16 im = ptu[11:66:3, ..., -1, ::-1] assert im.shape == (19, 512, 512) assert im.dtype == 'uint16' assert_array_equal(im, im0.squeeze()) if xarray is None: return # slice with xarray ptu.use_xarray = True assert ptu.use_xarray im = ptu[11:66:3, ..., 1:2, ::-1] assert isinstance(im, xarray.DataArray) assert tuple(im.coords.keys()) == ('T', 'Y', 'X', 'C') assert im.coords['T'].values[0] == 20.62217778751141 # sum all ptu.dtype = 'uint64' im = ptu[::-1, ::-1, ::-1, ::-1, ::-1] assert isinstance(im, xarray.DataArray) assert im.shape == () assert tuple(im.coords.keys()) == () photons = ptu.decode_image([slice(None, None, -1)] * 5).sum() assert im.values == photons # 21243427 with pytest.raises(IndexError): im = ptu[0, 0, 0, 0, 0, 0] # too many indices with pytest.raises(IndexError): im = ptu[0, ..., 0, ..., 0] # more than one Ellipsis with pytest.raises(IndexError): im = ptu[101] # index out of range with pytest.raises(IndexError): im = ptu[50:49] # stop < start with pytest.raises(IndexError): im = ptu[101:102] # start out of range with pytest.raises(IndexError): im = ptu[1.0] # invalid type def test_issue_datetime(): """Test file with datetime stored as float, not str or TDateTime.""" filename = DATA / 'Zenodo_7656540/2a_FLIM_single_image.ptu' with PtuFile(filename) as ptu: assert ptu.version == '00.0.1' assert ptu.tags['File_CreatingTime'] == 13301655831.929 # raises OverflowError assert ptu.datetime is None def test_issue_falcon_point(): """Test PTU from FALCON with no ImgHdr_PixY.""" # file produced by FALCON in image mode but no ImgHdr_PixX/Y filename = DATA / 'FALCON_ptu_examples/40MHz_example.ptu' with PtuFile(filename) as ptu: assert ptu.version == '00.0.1' assert ptu.type == PqFileType.PTU assert ptu.record_type == PtuRecordType.PicoHarpT3 assert ptu.measurement_mode == PtuMeasurementMode.T3 assert ptu.measurement_submode == PtuMeasurementSubMode.IMAGE assert ptu.scanner == PtuScannerType.LSM assert ptu.measurement_ndim == 1 assert ptu.shape == (3, 1, 269) assert ptu.dims == ('T', 'C', 'H') assert tuple(ptu.coords.keys()) == ('T', 'C', 'H') assert ptu.active_channels == (0,) # TODO: verify coords assert ptu._info.skip_first_frame == 0 assert ptu._info.skip_last_frame == 0 assert ptu.frame_change_mask == 0 assert ptu.line_start_mask == 0 assert ptu.line_stop_mask == 0 assert ptu.acquisition_time == 15.4790509824 assert ptu.frame_time == 15.4790509824 assert ptu.frequency == 312500000.0 assert ptu.global_frame_time == 4837203432 assert ptu.global_line_time == 312500 assert ptu.global_pixel_time == 312500 assert ptu.global_resolution == 3.2e-9 assert ptu.pixel_time == pytest.approx( ptu.global_pixel_time * ptu.global_resolution, rel=1e-3 ) assert ptu.line_time == pytest.approx( ptu.global_line_time * ptu.global_resolution, rel=1e-3 ) assert ptu.lines_in_frame == 1 assert ptu.number_bins == 269 assert ptu.number_bins_in_period == 32 assert ptu.number_bins_max == 4096 assert ptu.number_channels == 1 assert ptu.number_channels_max == 4 assert ptu.number_lines == 0 assert ptu.number_markers == 0 assert ptu.number_photons == 1016546 assert ptu.number_records == 1090355 assert ptu.pixels_in_frame == 1 assert ptu.pixels_in_line == 1 assert ptu.syncrate == 0 assert ptu.tcspc_resolution == 9.696969697e-11 im = ptu.decode_image(channel=0, keepdims=False, dtype='uint32') assert im.shape == (3, 269) assert im.dtype == numpy.uint32 assert im[1, 23] == 1 def test_issue_skip_frame(): """Test PTU with incomplete last frame.""" filename = DATA / 'Samples.sptw/GUVs.ptu' with PtuFile(filename) as ptu: assert ptu.version == '00.0.0' assert ptu.type == PqFileType.PTU assert ptu.record_type == PtuRecordType.PicoHarpT3 assert ptu.measurement_mode == PtuMeasurementMode.T3 assert ptu.measurement_submode == PtuMeasurementSubMode.IMAGE assert ptu.scanner == PtuScannerType.LSM assert ptu.measurement_ndim == 3 assert ptu.shape == (100, 512, 512, 2, 4096) assert ptu.dims == ('T', 'Y', 'X', 'C', 'H') assert tuple(ptu.coords.keys()) == ('T', 'Y', 'X', 'C', 'H') assert ptu.active_channels == (0, 1) # TODO: verify coords assert ptu._info.skip_first_frame == 0 assert ptu._info.skip_last_frame == 1 assert ptu.frame_change_mask == 4 assert ptu.line_start_mask == 1 assert ptu.line_stop_mask == 2 assert ptu.acquisition_time == 174.96876283164778 assert ptu.frame_time == 1.7496875403137495 assert ptu.frequency == 9999690.0 assert ptu.global_frame_time == 17496333 assert ptu.global_line_time == 20480 assert ptu.global_pixel_time == 40 assert ptu.global_resolution == 1.0000310009610297e-7 assert ptu.pixel_time == pytest.approx( ptu.global_pixel_time * ptu.global_resolution, rel=1e-3 ) assert ptu.line_time == pytest.approx( ptu.global_line_time * ptu.global_resolution, rel=1e-3 ) assert ptu.lines_in_frame == 512 assert ptu.number_bins == 4096 assert ptu.number_bins_in_period == 6250 # > number_bins_max ! assert ptu.number_bins_max == 4096 assert ptu.number_channels == 2 assert ptu.number_channels_max == 4 assert ptu.number_lines == 51204 assert ptu.number_markers == 102509 assert ptu.number_photons == 32976068 assert ptu.number_records == 33105274 assert ptu.pixels_in_frame == 262144 assert ptu.pixels_in_line == 512 assert ptu.syncrate == 9999690 assert ptu.tcspc_resolution == 1.5999999936067155e-11 im = ptu.decode_image( 92, channel=1, dtime=-1, keepdims=False, dtype='uint32' ) assert im.shape == (512, 512) assert im.dtype == numpy.uint32 assert im[281, 373] == 3 def test_issue_marker_order(): """Test PTU with strange marker order.""" # the file has `[ | ][][][ | ]`` instead of `[] | [][] | []`` markers # first and last frame are incomplete filename = DATA / 'Samples.sptw/CENP-labelled_cells_for_FRET.ptu' with PtuFile(filename, trimdims='CH') as ptu: assert ptu.version == '00.0.0' # assert ptu._data_offset == 4616 assert ptu.type == PqFileType.PTU assert ptu.record_type == PtuRecordType.PicoHarpT3 assert ptu.measurement_mode == PtuMeasurementMode.T3 assert ptu.measurement_submode == PtuMeasurementSubMode.IMAGE assert ptu.scanner == PtuScannerType.LSM assert ptu.measurement_ndim == 3 assert ptu.shape == (191, 512, 512, 3, 3126) assert ptu.dims == ('T', 'Y', 'X', 'C', 'H') assert tuple(ptu.coords.keys()) == ('T', 'C', 'H') assert ptu.active_channels == (0, 1, 2) # TODO: verify coords assert ptu._info.skip_first_frame == 0 assert ptu._info.skip_last_frame == 0 assert ptu._info.lines == 97596 assert ptu.frame_change_mask == 4 assert ptu.line_start_mask == 1 assert ptu.line_stop_mask == 2 assert ptu.acquisition_time == 213.87296482079424 assert ptu.frame_time == 1.1197537179119068 assert ptu.frequency == 20001617.0 assert ptu.global_frame_time == 22396885 assert ptu.global_line_time == 20446 assert ptu.global_pixel_time == 40 assert ptu.global_resolution == 4.99959578268097e-8 assert ptu.pixel_time == pytest.approx( ptu.global_pixel_time * ptu.global_resolution, rel=1e-3 ) assert ptu.line_time == pytest.approx( ptu.global_line_time * ptu.global_resolution, rel=1e-3 ) assert ptu.lines_in_frame == 512 assert ptu.number_bins == 3126 assert ptu.number_bins_in_period == 3124 assert ptu.number_bins_max == 4096 assert ptu.number_channels == 3 assert ptu.number_channels_max == 4 assert ptu.number_lines == 97596 assert ptu.number_markers == 195384 assert ptu.number_photons == 17601306 assert ptu.number_records == 17861964 assert ptu.pixels_in_frame == 262144 assert ptu.pixels_in_line == 512 assert ptu.syncrate == 20001617 assert ptu.tcspc_resolution == 1.5999999936067155e-11 # image of shape (3, 191, 512, 512, 3126) too large 875 GiB im = ptu.decode_image(channel=0, frame=92, dtime=-1, dtype='uint32') assert im.shape == (1, 512, 512, 1, 1) assert im.dtype == numpy.uint32 assert im[0, 390, 277] == 6 del im with PtuFile(filename, trimdims='T') as ptu: # trim only time dimension assert ptu.shape == (189, 512, 512, 4, 4096) assert ptu._info.skip_first_frame == 1 assert ptu._info.skip_last_frame == 1 im = ptu.decode_image( channel=0, frame=91, dtime=-1, dtype='uint8', keepdims=False ) assert im.shape == (512, 512) assert im[390, 277] == 6 def test_issue_empty_line(): """Test line not empty when first record is start marker.""" filename = DATA / 'ExampleFLIM/Example_image.sc.ptu' with PtuFile(filename) as ptu: str(ptu) assert ptu.version == '00.0.1' assert ptu.type == PqFileType.PTU assert ptu.record_type == PtuRecordType.PicoHarpT3 assert ptu.measurement_mode == PtuMeasurementMode.T3 assert ptu.measurement_submode == PtuMeasurementSubMode.IMAGE assert ptu.scanner == PtuScannerType.LSM assert ptu.measurement_ndim == 3 assert ptu.shape == (1, 256, 256, 1, 133) assert ptu.dims == ('T', 'Y', 'X', 'C', 'H') assert tuple(ptu.coords.keys()) == ('T', 'Y', 'X', 'C', 'H') assert ptu.coords['H'][1] == 9.696969697e-11 # 97 ps assert ptu.active_channels == (0,) assert ptu._info.skip_first_frame == 0 assert ptu._info.skip_last_frame == 0 assert ptu.frame_change_mask == 4 assert ptu.line_start_mask == 1 assert ptu.line_stop_mask == 2 assert ptu.acquisition_time == 0.27299666752114843 assert ptu.frame_time == 0.27299666752114843 assert ptu.frequency == 78020000.0 assert ptu.syncrate == 78020000 assert ptu.number_markers == 513 assert ptu.number_photons == 722402 assert ptu.number_records == 723240 assert ptu.global_pixel_time == 325 # 324 assert ptu.pixel_time == pytest.approx( ptu.global_pixel_time * ptu.global_resolution, rel=1e-3 ) assert ptu.decode_records()['marker'][0] == 1 # start marker assert ptu[0, 0, 100, 0, ::-1] == 40 # first line not empty def test_issue_pixeltime_zero(): """Test PTU with zero ImgHdr_TimePerPixel.""" filename = DATA / 'nc.picoquant.com/DaisyPollen1.ptu' with PtuFile(filename) as ptu: assert ptu.version == '1.0.00' assert ptu.type == PqFileType.PTU assert ptu.record_type == PtuRecordType.GenericT3 assert ptu.measurement_mode == PtuMeasurementMode.T3 assert ptu.measurement_submode == PtuMeasurementSubMode.IMAGE assert ptu.scanner is None assert ptu.measurement_ndim == 3 assert ptu.tags['ImgHdr_TimePerPixel'] == 0 # nasty assert ptu.global_pixel_time == 160 assert ptu.shape == (10, 512, 512, 2, 2510) assert ptu.dims == ('T', 'Y', 'X', 'C', 'H') assert tuple(ptu.coords.keys()) == ('T', 'Y', 'X', 'C', 'H') assert ptu.active_channels == (0, 1) # TODO: verify coords assert ptu._info.skip_first_frame == 1 assert ptu._info.skip_last_frame == 1 assert ptu.frame_change_mask == 4 assert ptu.line_start_mask == 1 assert ptu.line_stop_mask == 2 assert ptu.acquisition_time == 17.6701460317873 assert ptu.frame_time == 1.767014600678785 assert ptu.frequency == 40000880.0 assert ptu.global_frame_time == 70682139 assert ptu.global_line_time == 81920 assert ptu.global_pixel_time == 160 assert ptu.global_resolution == 2.4999450012099732e-8 assert ptu.pixel_time == pytest.approx( ptu.global_pixel_time * ptu.global_resolution, rel=1e-3 ) assert ptu.line_time == pytest.approx( ptu.global_line_time * ptu.global_resolution, rel=1e-3 ) assert ptu.lines_in_frame == 512 assert ptu.number_bins == 2510 assert ptu.number_bins_in_period == 2499 assert ptu.number_bins_max == 32768 assert ptu.number_channels == 2 assert ptu.number_channels_max == 64 assert ptu.number_lines == 5499 assert ptu.number_markers == 11009 assert ptu.number_photons == 37047472 assert ptu.number_records == 37748736 assert ptu.pixels_in_frame == 262144 assert ptu.pixels_in_line == 512 assert ptu.syncrate == 40000880 assert ptu.tcspc_resolution == 9.999999960041972e-12 im = ptu.decode_image( 9, channel=0, dtime=-1, keepdims=False, dtype='uint32' ) assert im.shape == (512, 512) assert im.dtype == numpy.uint32 assert im[281, 373] == 25 im = ptu.decode_image( 9, channel=0, dtime=-1, pixel_time=0, keepdims=False, dtype='u4' ) assert im.shape == (512, 512) assert im.dtype == numpy.uint32 assert im[281, 373] == 25 def test_issue_pixeltime_off(): """Test PTU with ImgHdr_TimePerPixel metadata slightly off.""" filename = DATA / 'UNC/805_1.ptu' with PtuFile(filename) as ptu: assert ptu.version == '1.0.00' assert ptu.type == PqFileType.PTU assert ptu.record_type == PtuRecordType.TimeHarp260PT3 assert ptu.measurement_mode == PtuMeasurementMode.T3 assert ptu.measurement_submode == PtuMeasurementSubMode.IMAGE assert ptu.scanner == PtuScannerType.PI_E727 assert ptu.measurement_ndim == 3 assert ptu.tags['ImgHdr_TimePerPixel'] == 2.0 # slightly off assert ptu.global_pixel_time == 40000 assert ptu.shape == (1, 256, 256, 2, 2002) assert ptu.dims == ('T', 'Y', 'X', 'C', 'H') assert tuple(ptu.coords.keys()) == ('T', 'Y', 'X', 'C', 'H') assert ptu.active_channels == (0, 1) assert ptu._info.line_time == 10232923 assert ptu._info.skip_first_frame == 0 assert ptu._info.skip_last_frame == 0 assert ptu.frame_change_mask == 0 # ! assert ptu.line_start_mask == 4 assert ptu.line_stop_mask == 8 assert ptu.acquisition_time == 172.07485649999998 assert ptu.frame_time == 172.07485649999998 assert ptu.frequency == 20000000.0 assert ptu.global_frame_time == 3441497130 assert ptu.global_line_time == 10240000 assert ptu.global_pixel_time == 40000 assert ptu.global_resolution == 5e-08 assert ptu.pixel_time == pytest.approx( ptu.global_pixel_time * ptu.global_resolution, rel=1e-3 ) assert ptu.line_time == pytest.approx( ptu.global_line_time * ptu.global_resolution, rel=1e-3 ) assert ptu.lines_in_frame == 256 assert ptu.number_bins == 2002 assert ptu.number_bins_in_period == 1999 assert ptu.number_bins_max == 32768 assert ptu.number_channels == 2 assert ptu.number_channels_max == 64 assert ptu.number_lines == 256 assert ptu.number_markers == 512 assert ptu.number_photons == 56142925 assert ptu.number_records == 59504234 assert ptu.pixels_in_frame == 65536 assert ptu.pixels_in_line == 256 assert ptu.syncrate == 20000000 assert ptu.tcspc_resolution == 2.50000003337858e-11 data = binread(DATA / 'UNC/805.bin')[0].sum(axis=-1, dtype=numpy.int32) # use pixel time from metadata im = ptu.decode_image(channel=1, frame=0, keepdims=False) im = im[..., :2000].sum(axis=-1, dtype=numpy.int32) assert numpy.abs(im - data).max() == 298 # use pixel time from average line time im = ptu.decode_image( channel=1, frame=0, pixel_time=ptu._info.line_time / 256 * ptu.global_resolution, keepdims=False, ) im = im[..., :2000].sum(axis=-1, dtype=numpy.int32) assert numpy.abs(im - data).max() == 8 # use pixel time from line markers im = ptu.decode_image(channel=1, frame=0, pixel_time=0, keepdims=False) im = im[..., :2000].sum(axis=-1, dtype=numpy.int32) assert numpy.abs(im - data).max() == 8 def test_issue_number_records_zero(caplog): """Test PTU with zero TTResult_NumberOfRecords.""" # https://github.com/cgohlke/ptufile/issues/2 filename = DATA / 'FLIM_number_records_zero.ptu' with PtuFile(filename) as ptu: ptu.cache_records = False with caplog.at_level(logging.WARNING): assert ptu.number_records == 12769472 assert ptu.tags['TTResult_NumberOfRecords'] == 0 assert 'invalid TTResult_NumberOfRecords' in caplog.text assert len(ptu.read_records()) == 12769472 def test_issue_number_records_negative(caplog): """Test PTU with negative TTResult_NumberOfRecords.""" # file >4 GB produced by LAS X software. Received by email on Oct 27, 2025. filename = DATA / 'i3S/AlessandroSlide_10x_488nm.ptu' with PtuFile(filename) as ptu: ptu.cache_records = False with caplog.at_level(logging.WARNING): assert ptu.number_records == 3167584182 assert ptu.tags['TTResult_NumberOfRecords'] == -1127383114 assert 'invalid TTResult_NumberOfRecords' in caplog.text assert len(ptu.read_records()) == 3167584182 def test_issue_record_number(caplog): """Test PTU with too few records.""" filename = DATA / 'Samples.sptw/Cy5_immo_FLIM+Pol-Imaging.ptu' with PtuFile(filename) as ptu: assert ptu.version == '00.0.0' with caplog.at_level(logging.ERROR): records = ptu.read_records() assert 'expected 3409856 records, got 3364091' in caplog.text assert len(records) == 3364091 decoded = ptu.decode_records() assert decoded.size == 3364091 assert decoded['time'][-1] == ptu.global_acquisition_time assert decoded['channel'].max() + 1 == ptu.number_channels assert decoded[decoded['channel'] >= 0].size == ptu.number_photons assert decoded[decoded['marker'] > 0].size == ptu.number_markers str(ptu) def test_issue_tag_index_order(caplog): """Test tag index out of order.""" filename = DATA / 'picoquant-sample-data/hydraharp/v10_t2.ptu' with caplog.at_level(logging.ERROR): # noqa: SIM117 with PqFile(filename) as pq: str(pq) assert 'tag index out of order' in caplog.text assert 'UsrHeadName' in caplog.text assert pq.type == PqFileType.PTU assert pq.version == '1.0.00' assert pq.tags['UsrHeadName'] == [ '405.0nm (DC405)', '485.0nm (DC485)', ] @pytest.mark.skipif(xarray is None, reason='xarray not installed') @pytest.mark.parametrize( ('dtime', 'size'), [ (None, 139), # last bin with non-zero photons (0, 132), # last bin matching frequency (-1, 1), # integrate bins (32, 32), # specified number of bins (145, 145), ], ) def test_issue_dtime(dtime, size): """Test dtime parameter.""" filename = ( DATA / 'napari_flim_phasor_plotter/hazelnut_FLIM_single_image.ptu' ) im = imread( filename, frame=0, channel=0, dtime=dtime, dtype=numpy.uint8, asxarray=True, ) assert im.dtype == numpy.uint8 assert im.shape == (1, 256, 256, 1, size) assert im.dims == ('T', 'Y', 'X', 'C', 'H') assert tuple(im.coords.keys()) == ('T', 'Y', 'X', 'C', 'H') def test_issue_line_markers(): """Test nonsense line markers.""" # This file apparently omits line stop markers if no photons were # recorded in the line. # The line stop markers don't match the timing expected from pixel time # and number of pixels in lines. # There are many more lines than expected from the frame height. # In conclusion, the line time from the `info` object is not reliable. filename = DATA / 'Tutorials.sptw/MicroBeads.ptu' with PtuFile(filename) as ptu: str(ptu) assert ptu._info.lines == 300 assert ptu._info.line_time == 9000 # should be 18000 assert ptu.global_line_time == 18000 data = ptu.decode_image(pixel_time=0) assert data.shape == (2, 150, 150, 2, 626) assert data.sum(dtype=numpy.uint32) == 778696 def test_issue_imspector(caplog): """Test file written by Seidel group using Abberior Imspector software.""" # This file stores wrong marker masks and twice as many lines per frame. # Frames and channels need to be "deinterlaced". filename = DATA / 'HHU/PQSpcm_2021-12-13_17-53-45.ptu' with PtuFile(filename) as ptu: assert ptu.tags['CreatorSW_Name'] == 'Imspector' assert ptu.tags['ImgHdr_LineStart'] == 0 # invalid assert ptu.tags['ImgHdr_LineStop'] == 1 # wrong assert ptu.tags['ImgHdr_Frame'] == 2 # wrong assert ptu.tags['ImgHdr_PixY'] == 100 # wrong assert ptu._info.frames == 0 assert ptu._info.lines == 0 str(ptu) image = ptu.decode_image(dtime=-1, frame=-1, keepdims=False) assert 'invalid line_start' in caplog.text assert image.sum() == 0 # empty with PtuFile(filename) as ptu: # overwrite invalid header values before inspecting or decoding ptu.tags['ImgHdr_LineStart'] = 1 ptu.tags['ImgHdr_LineStop'] = 2 ptu.tags['ImgHdr_Frame'] = 3 ptu.tags['ImgHdr_PixY'] *= 2 assert ptu._info.frames == 61 assert ptu._info.lines == 12200 image = ptu.decode_image(dtime=-1, frame=-1, keepdims=False) assert image.sum() == 24953 # deinterlace lines and channels channel = [ image[::2, :, 0] + image[::2, :, 2], image[::2, :, 1] + image[::2, :, 3], image[1::2, :, 0] + image[1::2, :, 2], image[1::2, :, 1] + image[1::2, :, 3], ] assert channel[0].shape == (100, 100) assert channel[0].sum() == 7272 assert channel[1].sum() == 6658 assert channel[2].sum() == 3789 assert channel[3].sum() == 7234 @pytest.mark.skipif(xarray is None, reason='xarray not installed') def test_imread(): """Test imread function.""" filename = ( DATA / 'napari_flim_phasor_plotter/hazelnut_FLIM_single_image.ptu' ) im = imread( filename, [slice(1), None], # first frame channel=0, frame=None, dtime=0, pixel_time=0, dtype=numpy.uint8, asxarray=True, ) assert im.dtype == numpy.uint8 assert im.shape == (1, 256, 256, 1, 132) assert im.dims == ('T', 'Y', 'X', 'C', 'H') assert tuple(im.coords.keys()) == ('T', 'Y', 'X', 'C', 'H') @pytest.mark.parametrize('pixel_time', [None, 0.1]) @pytest.mark.parametrize('counts', [0, 1, 87]) @pytest.mark.parametrize( 'shape', [(31, 33, 63), (5, 31, 33, 63), (31, 33, 2, 63), (5, 31, 33, 2, 63)], ) @pytest.mark.parametrize( 'record_type', [PtuRecordType.PicoHarpT3, PtuRecordType.GenericT3] ) def test_imwrite(record_type, pixel_time, shape, counts): """Test imwrite function.""" if counts: data = RNG.integers(0, counts, shape, numpy.uint8) else: data = numpy.zeros(shape, numpy.uint8) has_frames = data.shape[0] == 5 if data.ndim == 3: shape = (1, 31, 33, 1, 63) if counts: data[..., -1] = 1 elif data.ndim == 4: if has_frames: shape = (5, 31, 33, 1, 63) if counts: data[..., -1] = 1 else: shape = (1, 31, 33, 2, 63) if counts: data[..., :, -1] = 1 else: shape = data.shape if counts: data[..., :, -1] = 1 guid = '{b767c46e-9693-4ad9-9fcf-7fab5e4377fc}' comment = f'{shape=} \u2764\ufe0f' buf = io.BytesIO() imwrite( buf, data, global_resolution=4e-8, tcspc_resolution=8e-11, record_type=record_type, pixel_time=pixel_time, pixel_resolution=0.5, has_frames=has_frames, comment=comment, guid=guid, tags={ 'HW_Markers': 4, 'HWMarkers_Enabled': [True, True, True, True], 'TTResult_MDescWarningFlags': PtuMeasurementWarnings(2), }, ) buf.seek(0) data = data.reshape(shape) if counts == 0: data = data[..., :1, :1] with PtuFile(buf) as ptu: str(ptu) assert ptu.version == '1.0.00' assert ptu.type == PqFileType.PTU assert ptu.record_type == record_type assert ptu.measurement_mode == PtuMeasurementMode.T3 assert ptu.measurement_submode == PtuMeasurementSubMode.IMAGE assert ptu.scanner == PtuScannerType.LSM assert ptu.measurement_ndim == 3 assert ptu.is_image assert ptu.is_t3 assert ptu.comment == comment assert ptu.shape == data.shape assert str(ptu.guid) == guid[1:-1] assert ptu.global_resolution == 4e-8 if pixel_time is not None: assert ptu.pixel_time == pixel_time assert ptu.global_line_time == pixel_time / 4e-8 * shape[2] assert ptu._info.line_time == ptu.global_line_time assert ptu.tags['ImgHdr_MaxFrames'] == shape[0] assert ptu.tags['HW_InpChannels'] == shape[3] + 1 assert ptu.tags['ImgHdr_PixResol'] == 0.5 assert ptu.tags['HW_Markers'] == 4 assert ptu.tags['HWMarkers_Enabled'] == [True, True, True, True] assert ptu.tags['TTResult_MDescWarningFlags'] == 2 data2 = ptu.decode_image() numpy.testing.assert_array_equal(data2, data) data2 = ptu.decode_image(pixel_time=0.0) numpy.testing.assert_array_equal(data2, data) @pytest.mark.parametrize( 'record_type', [PtuRecordType.PicoHarpT3, PtuRecordType.GenericT3] ) def test_imwrite_rewrite(record_type): """Test imwrite function with real data.""" filename = DATA / 'Tutorials.sptw/Kidney _Cell_FLIM.ptu' buf = io.BytesIO() with PtuFile(filename) as ptu0: data = ptu0.decode_image() assert data.shape == (3, 512, 512, 2, 501) imwrite( buf, data, global_resolution=ptu0.global_resolution, # 4.00001280004096e-8 tcspc_resolution=ptu0.tcspc_resolution, # 7.999999968033578e-11 record_type=record_type, pixel_time=ptu0.pixel_time, # 3e-5 pixel_resolution=ptu0.tags['ImgHdr_PixResol'], # 0.504453125 guid=ptu0.guid, datetime=ptu0.datetime, comment=ptu0.comment, tags={'File_RawData_GUID': [ptu0.guid]}, ) buf.seek(0) with PtuFile(buf) as ptu: str(ptu) assert ptu.version == '1.0.00' assert ptu.type == PqFileType.PTU assert ptu.record_type == record_type assert ptu.measurement_mode == PtuMeasurementMode.T3 assert ptu.measurement_submode == PtuMeasurementSubMode.IMAGE assert ptu.scanner == PtuScannerType.LSM assert ptu.measurement_ndim == 3 assert ptu.is_image assert ptu.is_t3 assert ptu.shape == (3, 512, 512, 2, 501) assert ptu.dims == ('T', 'Y', 'X', 'C', 'H') assert tuple(ptu.coords.keys()) == ('T', 'Y', 'X', 'C', 'H') assert ptu.active_channels == (0, 1) assert_array_equal(ptu.coords['C'], (0, 1)) # TODO: verify coords assert ptu.guid == ptu0.guid assert ptu.comment == ptu0.comment assert ptu.datetime == ptu0.datetime assert ptu.line_start_mask == 1 assert ptu.line_stop_mask == 2 assert ptu.frame_change_mask == 4 assert ptu._info.line_time == 384000 assert ptu.acquisition_time == 23.593035497713593 assert ptu.frame_time == 7.864345165904531 assert ptu.frequency == 24999920.0 assert ptu.global_frame_time == 196608000 assert ptu.global_line_time == 384000 assert ptu.global_pixel_time == 750 assert ptu.global_resolution == 4.00001280004096e-8 assert ptu.pixel_time == pytest.approx( ptu.global_pixel_time * ptu.global_resolution, rel=1e-3 ) assert ptu.line_time == pytest.approx( ptu.global_line_time * ptu.global_resolution, rel=1e-3 ) assert ptu.lines_in_frame == 512 assert ptu.number_bins == 501 assert ptu.number_bins_in_period == 500 assert ptu.number_channels == 2 assert ptu.number_lines == 1536 assert ptu.number_markers == 3075 assert ptu.number_photons == data.sum(dtype=numpy.uint32) assert ptu.number_records in {21116856, 21683856} assert ptu.pixels_in_frame == 262144 assert ptu.pixels_in_line == 512 assert ptu.syncrate == 24999920 assert ptu.tcspc_resolution == 7.999999968033578e-11 assert ptu.tags['File_RawData_GUID'][0] == ( '{b767c46e-9693-4ad9-9fcf-7fab5e4377fc}' ) assert ptu.tags['ImgHdr_PixResol'] == 0.504453125 data2 = ptu.decode_image() numpy.testing.assert_array_equal(data2, data) @pytest.mark.parametrize('count', [0, 1, 87]) @pytest.mark.parametrize( 'record_type', [PtuRecordType.PicoHarpT3, PtuRecordType.GenericT3] ) def test_imwrite_pixel(record_type, count): """Test imwrite function with one pixel.""" data = numpy.full((1, 1, 1, 1, 1), count, numpy.uint8) buf = io.BytesIO() imwrite(buf, data, 4e-8, 8e-11, record_type=record_type) buf.seek(0) with PtuFile(buf) as ptu: str(ptu) assert ptu.version == '1.0.00' assert ptu.type == PqFileType.PTU assert ptu.record_type == record_type assert ptu.measurement_mode == PtuMeasurementMode.T3 assert ptu.measurement_submode == PtuMeasurementSubMode.IMAGE assert ptu.scanner == PtuScannerType.LSM assert ptu.measurement_ndim == 3 assert ptu.is_image assert ptu.is_t3 assert ptu.shape == (1, 1, 1, 1, 1) assert ptu.number_photons == count assert ptu.number_records == count + 3 records = ptu.decode_records() histogram = ptu.decode_histogram() image = ptu.decode_image() assert records.size == count + 3 assert histogram.shape == (1, 1) numpy.testing.assert_array_equal(image, data) def test_write_none(): """Test PtuWriter with no data.""" buf = io.BytesIO() with PtuWriter(buf, (2, 3, 4, 5, 6), 4e-8, 8e-11, 1e-6) as ptu: pass buf.seek(0) with PtuFile(buf) as ptu: str(ptu) assert ptu.version == '1.0.00' assert ptu.type == PqFileType.PTU assert ptu.record_type == PtuRecordType.GenericT3 assert ptu.measurement_mode == PtuMeasurementMode.T3 assert ptu.measurement_submode == PtuMeasurementSubMode.IMAGE assert ptu.scanner == PtuScannerType.LSM assert ptu.measurement_ndim == 3 assert ptu.is_image assert ptu.is_t3 assert ptu.shape == (1, 3, 4, 1, 1) assert ptu.number_photons == 0 assert ptu.number_records == 0 records = ptu.decode_records() assert len(records) == 0 histogram = ptu.decode_histogram() numpy.testing.assert_array_equal( histogram, numpy.zeros((1, 1), numpy.uint32) ) image = ptu.decode_image() numpy.testing.assert_array_equal( image, numpy.zeros((1, 3, 4, 1, 1), numpy.uint16) ) def test_write_iterate(): """Test PtuWriter.write iteratively.""" filename = DATA / 'Samples.sptw/GUVs.ptu' with tempfile.NamedTemporaryFile(suffix='.ptu', delete=False) as tmp: fnout = tmp.name try: with PtuFile(filename) as ptu_in: assert ptu_in.shape == (100, 512, 512, 2, 4096) with PtuWriter( fnout, ptu_in.shape[1:], global_resolution=ptu_in.global_resolution, tcspc_resolution=ptu_in.tcspc_resolution, pixel_time=ptu_in.pixel_time, ) as ptu_out: for i in range(2): frame = ptu_in.decode_image(frame=i) assert frame.shape == (1, 512, 512, 2, 4096) # 4 GB ptu_out.write(frame) with PtuFile(fnout) as ptu: assert ptu.version == '1.0.00' assert ptu.type == PqFileType.PTU assert ptu.record_type == PtuRecordType.PicoHarpT3 assert ptu.measurement_mode == PtuMeasurementMode.T3 assert ptu.measurement_submode == PtuMeasurementSubMode.IMAGE assert ptu.scanner == PtuScannerType.LSM assert ptu.measurement_ndim == 3 assert ptu.is_image assert ptu.is_t3 assert ptu.tags['ImgHdr_MaxFrames'] == 2 assert ptu.number_records == 685208 assert ptu.shape == (2, 512, 512, 2, 4096) assert_array_equal( ptu.decode_image(frame=1, dtime=-1, dtype=numpy.uint16), frame.sum(axis=-1, dtype=numpy.uint16, keepdims=True), ) finally: if os.path.exists(fnout): os.remove(fnout) def test_imwrite_exceptions(): """Test imwrite function exceptions.""" buf = io.BytesIO() kwargs = {'global_resolution': 4e-8, 'tcspc_resolution': 8e-11} with pytest.raises(ValueError): # cannot write to imwrite([], numpy.empty((31, 33, 1), numpy.uint8), **kwargs) with pytest.raises(ValueError): # not unsigned int imwrite(buf, numpy.empty((31, 33, 1), numpy.int8), **kwargs) with pytest.raises(ValueError): # not enough dimensions imwrite(buf, numpy.empty((31, 33), numpy.uint8), **kwargs) with pytest.raises(ValueError): # too many dimensions imwrite(buf, numpy.empty((1, 31, 33, 1, 1, 1), numpy.uint8), **kwargs) with pytest.raises(ValueError): # too many channels imwrite(buf, numpy.empty((1, 31, 33, 64, 1), numpy.uint8), **kwargs) with pytest.raises(ValueError): # too many bins imwrite(buf, numpy.empty((1, 1, 1, 1, 32769), numpy.uint8), **kwargs) with pytest.raises(ValueError): # invalid record_type imwrite( buf, numpy.empty((31, 33, 1), numpy.uint8), record_type=PtuRecordType.GenericT2, **kwargs, ) with pytest.raises(ValueError): # invalid global_resolution imwrite( buf, numpy.empty((31, 33, 1), numpy.uint8), global_resolution=0.0, tcspc_resolution=8e-11, ) with pytest.raises(ValueError): # tcspc_resolution > global_resolution imwrite( buf, numpy.empty((31, 33, 1), numpy.uint8), global_resolution=4e-8, tcspc_resolution=8e-6, ) with pytest.raises(ValueError): # pixel_time < global_resolution imwrite( buf, numpy.empty((31, 33, 1), numpy.uint8), global_resolution=4e-8, tcspc_resolution=8e-11, pixel_time=1e-9, ) # with pytest.raises(ValueError): # # global_pixel_time=250 < photons_in_pixel=255 # imwrite( # buf, # numpy.full((31, 33, 1), 255, numpy.uint8), # pixel_time=1e-5, # **kwargs, # ) with pytest.raises(ValueError): # invalid guid imwrite(buf, numpy.empty((31, 33, 1), numpy.uint8), guid='-', **kwargs) def test_signal_from_ptu(): """Test PhasorPy signal_from_ptu function.""" try: from phasorpy.io import signal_from_ptu except ImportError: pytest.skip('PhasorPy not installed') filename = ( DATA / 'napari_flim_phasor_plotter/hazelnut_FLIM_single_image.ptu' ) signal = signal_from_ptu( filename, frame=-1, channel=0, dtime=0, keepdims=False ) assert signal.values.sum(dtype=numpy.uint64) == 6064854 assert signal.dtype == numpy.uint16 assert signal.shape == (256, 256, 132) assert signal.dims == ('Y', 'X', 'H') assert_almost_equal( signal.coords['H'].data[[1, -1]], [0.0969697, 12.7030303], decimal=4 ) assert signal.attrs['frequency'] == 78.02 assert signal.attrs['ptu_tags']['HW_Type'] == 'PicoHarp' signal = signal_from_ptu( filename, frame=-1, channel=0, dtime=None, keepdims=True, trimdims='TC', ) assert signal.values.sum(dtype=numpy.uint64) == 6065123 assert signal.dtype == numpy.uint16 assert signal.shape == (1, 256, 256, 1, 4096) assert signal.dims == ('T', 'Y', 'X', 'C', 'H') assert_almost_equal( signal.coords['H'].data[[1, -1]], [0.0969697, 397.09091], decimal=4 ) assert signal.attrs['frequency'] == 78.02 def test_signal_from_ptu_irf(): """Test read PhasorPy signal_from_ptu function with IRF.""" try: from phasorpy.io import signal_from_ptu except ImportError: pytest.skip('PhasorPy not installed') filename = DATA / 'Samples.sptw/Cy5_diff_IRF+FLCS-pattern.ptu' signal = signal_from_ptu(filename, channel=None, keepdims=True) assert signal.values.sum(dtype=numpy.uint64) == 13268548 assert signal.dtype == numpy.uint32 assert signal.shape == (1, 1, 1, 2, 6250) assert signal.dims == ('T', 'Y', 'X', 'C', 'H') assert_almost_equal( signal.coords['H'].data[[1, -1]], [0.007999, 49.991999], decimal=4 ) assert pytest.approx(signal.attrs['frequency'], abs=1e-4) == 19.999732 assert signal.attrs['ptu_tags']['HW_Type'] == 'PicoHarp 300' signal = signal_from_ptu(filename, channel=0, keepdims=True) assert signal.values.sum(dtype=numpy.uint64) == 6984849 assert signal.shape == (1, 1, 1, 1, 6250) assert signal.dims == ('T', 'Y', 'X', 'C', 'H') with pytest.raises(ValueError): signal_from_ptu(filename, dtime=-1) signal = signal_from_ptu(filename, channel=0, dtime=None, keepdims=False) assert signal.values.sum(dtype=numpy.uint64) == 6984849 assert signal.shape == (1, 1, 4096) assert signal.dims == ('Y', 'X', 'H') assert_almost_equal( signal.coords['H'].data[[1, -1]], [0.007999, 32.759999], decimal=4 ) @pytest.mark.parametrize( 'filename', itertools.chain.from_iterable( glob.glob(f'**/*{ext}', root_dir=DATA, recursive=True) for ext in FILE_EXTENSIONS ), ) def test_glob(filename): """Test read all PicoQuant files.""" filename = str(DATA / filename) if 'htmlcov' in filename or 'url' in filename or 'defective' in filename: pytest.skip() with PqFile(filename) as pq: str(pq) is_ptu = pq.type == PqFileType.PTU if is_ptu: with PtuFile(filename) as ptu: str(ptu) @pytest.mark.parametrize( ('trimdims', 'dtime', 'size'), [('TC', None, 4096), ('TCH', 0, 132)] ) def test_ptu_zip_sequence(trimdims, dtime, size): """Test read Z-stack with imread and tifffile.FileSequence.""" # requires ~28GB. Do not trim H dimensions such that files match from tifffile import FileSequence filename = DATA / 'napari_flim_phasor_plotter/hazelnut_FLIM_z_stack.zip' with FileSequence(imread, '*.ptu', container=filename) as ptus: assert ptus.shape == (11,) stack = ptus.asarray( channel=0, trimdims=trimdims, dtime=dtime, ioworkers=1 ) assert stack.shape == (11, 5, 256, 256, 1, size) # 11 files, 5 frames each assert stack.dtype == 'uint16' def test_ptu_leica_sequence(): """Test read Leica TZ-stack with imread and tifffile.imread.""" # 410 files. Requires ~16GB. import tifffile # >= 2024.2.12 filename = DATA / 'Flipper TR time series.sptw/*.ptu' stack = tifffile.imread( str(filename), # glob pattern needs to be str pattern=r'_(t)(\d+)_(z)(\d+)', imread=imread, chunkshape=(512, 512, 132), # shape returned by imread chunkdtype='uint8', # dtype returned by imread ioworkers=None, # use multi-threading imreadargs={ 'frame': 0, 'channel': 0, 'dtime': 132, # fix number of bins to 132 'dtype': 'uint8', # request uint8 output 'keepdims': False, }, ) assert stack.shape == (41, 10, 512, 512, 132) assert stack.dtype == 'uint8' assert stack[24, 4, 228, 279, 16] == 3 def test_ptu_numcodecs(): """Test Leica TZ-stack with tifffile.ZarrFileSequenceStore and fsspec.""" # 410 files. Requires ~16GB. try: import tifffile import tifffile.zarr import zarr from kerchunk.utils import refs_as_store except ImportError: pytest.skip('fsspec, tifffile, or zarr not installed') pathname = DATA / 'Flipper TR time series.sptw' url = str(pathname).replace('\\', '/') jsonfile = str(pathname / 'FLIPPER.json') filename = str(pathname / '*.ptu') # glob pattern needs to be str store = tifffile.imread( filename, pattern=r'_(t)(\d+)_(z)(\d+)', imread=imread, chunkshape=(512, 512), # shape returned by imread chunkdtype='uint8', # dtype returned by imread imreadargs={ 'frame': 0, 'channel': 0, 'dtime': -1, 'pixel_time': None, 'dtype': 'uint8', # request uint8 output 'trimdims': None, 'keepdims': False, }, aszarr=True, ) assert isinstance(store, tifffile.zarr.ZarrFileSequenceStore) store.write_fsspec( jsonfile, url=url, version=1, codec_id='ptufile', quote=False, ) store.close() ptufile.numcodecs.register_codec() stack = zarr.open(refs_as_store(jsonfile), mode='r') assert stack.shape == (41, 10, 512, 512) assert stack.dtype == 'uint8' assert stack[24, 4, 228, 279] == 18 @pytest.mark.skipif( not hasattr(sys, '_is_gil_enabled'), reason='Python < 3.13' ) def test_gil_enabled(): """Test that GIL state is consistent with build configuration.""" assert sys._is_gil_enabled() != sysconfig.get_config_var('Py_GIL_DISABLED') if __name__ == '__main__': import warnings # warnings.simplefilter('always') warnings.filterwarnings('ignore', category=ImportWarning) argv = sys.argv argv.append('--cov-report=html') argv.append('--cov=ptufile') argv.append('--verbose') sys.exit(pytest.main(argv)) # mypy: allow-untyped-defs # mypy: check-untyped-defs=False