Skip to content

Commit

Permalink
feat: implement FDTD/Meep reader for data from ODTbrain paper
Browse files Browse the repository at this point in the history
  • Loading branch information
paulmueller committed Feb 1, 2022
1 parent e2d6439 commit c8f83f2
Show file tree
Hide file tree
Showing 6 changed files with 219 additions and 15 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
0.11.0
- feat: implement FDTD/Meep reader for data from ODTbrain paper
0.10.9
- ref: minor code cleanup
- setup: remove unneccessary requirements from setup.py
Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'default'
html_theme = 'sphinx_rtd_theme'

# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
Expand Down
14 changes: 13 additions & 1 deletion docs/qpformat.bib
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
% This file was created with JabRef 2.10.
% Encoding: UTF-8
Expand Down Expand Up @@ -30,3 +29,16 @@ @Article{Migunov2015
Publisher = {{AIP} Publishing}
}

@Article{Mueller2015,
author = {Müller, Paul and Schürmann, Mirjam and Guck, Jochen},
title = {{ODTbrain: a Python library for full-view, dense diffraction tomography}},
journal = {BMC Bioinformatics},
year = {2015},
volume = {16},
number = {1},
pages = {1--9},
issn = {1471-2105},
doi = {10.1186/s12859-015-0764-0},
}

@Comment{jabref-meta: databaseType:bibtex;}
25 changes: 14 additions & 11 deletions qpformat/file_formats/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from .dataset import SeriesData, hash_obj
from .dataset import SingleData # noqa:F401 (user convenience)
from .series_hdf5_generic import SeriesHDF5SinogramMeep
from .series_hdf5_hyperspy import SeriesHdf5HyperSpy
from .series_hdf5_qpimage import SeriesHdf5Qpimage, SeriesHdf5QpimageSubjoined
from .series_zip_tif_holo import SeriesZipTifHolo
Expand Down Expand Up @@ -193,17 +194,19 @@ def verify(path):


# the order is important
formats = [SeriesFolder,
SingleHdf5Qpimage,
SingleTifPhasics,
SingleTifHolo,
SingleNpyNumpy,
SeriesHdf5HyperSpy,
SeriesHdf5Qpimage,
SeriesHdf5QpimageSubjoined,
SeriesZipTifPhasics,
SeriesZipTifHolo, # after phasics, b/c phasics has extra keywords
]
formats = [
SeriesFolder,
SingleHdf5Qpimage,
SingleTifPhasics,
SingleTifHolo,
SingleNpyNumpy,
SeriesHDF5SinogramMeep,
SeriesHdf5HyperSpy,
SeriesHdf5Qpimage,
SeriesHdf5QpimageSubjoined,
SeriesZipTifPhasics,
SeriesZipTifHolo, # after phasics, b/c phasics has extra keywords
]

# convenience dictionary
formats_dict = {}
Expand Down
19 changes: 17 additions & 2 deletions qpformat/file_formats/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,12 @@ class SeriesData(object):
__meta__ = abc.ABCMeta
is_series = True

def __init__(self, path, meta_data={}, holo_kw={}, as_type="float32"):
def __init__(self, path, meta_data=None, holo_kw=None, as_type="float32"):
#: Enforced dtype via keyword arguments
if holo_kw is None:
holo_kw = {}
if meta_data is None:
meta_data = {}
self.as_type = as_type
if isinstance(path, io.IOBase):
# io.IOBase
Expand Down Expand Up @@ -148,6 +152,17 @@ def identifier(self):
self.background_identifier])
return idsum

@property
@functools.lru_cache()
def shape(self):
"""Return dataset shape (lenght, image0, image1).
This should be overridden by the subclass, because by default
the first qpimage is used for that.
"""
qpi0 = self.get_qpimage_raw(0)
return len(self), qpi0.shape[0], qpi0.shape[1]

def get_identifier(self, idx):
"""Return an identifier for the data at index `idx`
Expand Down Expand Up @@ -211,7 +226,7 @@ def get_qpimage_raw(self, idx):

def saveh5(self, h5file, qpi_slice=None, series_slice=None,
time_interval=None, count=None, max_count=None):
"""Save the data set as an hdf5 file (qpimage.QPSeries format)
"""Save the data set as an HDF5 file (qpimage.QPSeries format)
Parameters
----------
Expand Down
172 changes: 172 additions & 0 deletions qpformat/file_formats/series_hdf5_generic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import functools
import warnings

import h5py
import numpy as np
import qpimage

from .dataset import SeriesData


class NoSinogramDataFoundError(BaseException):
pass


class SeriesHDF5GenericWarning(UserWarning):
pass


class SeriesHDF5SinogramGeneric(SeriesData):
"""Base class for HDF5-based raw sinogram QPI data
"""

def __len__(self):
return len(self._get_data_indices())

@functools.lru_cache()
def _get_data_indices(self):
"""Get all experiments from the hdf5 file"""
with h5py.File(name=self.path, mode="r") as h5:
if "sinogram" not in h5:
raise NoSinogramDataFoundError(
f"Group 'sinogram' not found in '{self.path}'!")
indices = sorted(h5["sinogram"].keys(), key=lambda x: int(x))

if not indices:
# if this error is raised, the signal_type is probably not
# set to "hologram".
raise NoSinogramDataFoundError(
f"No sinogram data found in '{self.path}'!")
return indices

def _get_metadata(self, dataset):
"""Return the metadata of the specified `h5py.Dataset`"""
meta = qpimage.meta.MetaDict()
for key in qpimage.META_KEYS:
if key in dataset.attrs:
meta[key] = dataset.attrs[key]
return meta

@staticmethod
def verify(path):
"""Verify that `path` has generic sinogram data"""
valid = False
try:
h5 = h5py.File(path, mode="r")
except (OSError, IsADirectoryError):
pass
else:
if ("file_format" in h5.attrs
and "qpformat" in h5.attrs["file_format"].lower()
and "sinogram" in h5):
valid = True
h5.close()
return valid

@property
@functools.lru_cache()
def shape(self):
with h5py.File(name=self.path, mode="r") as h5:
group = h5["sinogram"]["0"]
for key in ["field", "phase", "amplitude", "intensity"]:
if key in group:
return len(self), group[key].shape[0], group[key].shape[1]
else:
# Fallback to expensive shape computation
warnings.warn(f"Using fallback `shape` for '{self.path}'!",
SeriesHDF5GenericWarning)
return super(SeriesHDF5SinogramGeneric, self).shape


class SeriesHDF5SinogramMeep(SeriesHDF5SinogramGeneric):
"""sinograms extracted from Meep/FDTD simulations
I introduced this format in 2022 as part of my efforts to make
the finite-difference time domain simulations from the ODTbrain
manuscript :cite:`Mueller2015` publicly available.
The HDF5 file contains a "background" and a "sinogram" group.
The subgroups of "sinogram" are enumerated starting with "0".
Each of them contain the complex "field" at a plane behind the
scattering phantom as an HDF5 Dataset. The location of the plane
(and all other relevant metadata) is stored in the attributes
of this Dataset. In the same group, there are also the C++
"simulation_code" and the log "simulation_output" which can
be used to reproduce the simulation.
"""
storage_type = "field"

def __init__(self, path, meta_data=None, *args, **kwargs):
"""Initialize with default wavelength of 500nm"""
if meta_data is None:
meta_data = {}
if "wavelength" not in meta_data:
meta_data["wavelength"] = 500e-9
super(SeriesHDF5SinogramMeep, self).__init__(
path=path, meta_data=meta_data, *args, **kwargs)

# set background data
with h5py.File(path, "r") as h5:
if "background" in h5:
bgds = h5["background"]["field"]
meta_data = self._get_metadata(bgds)
qpi_bg = qpimage.QPImage(data=bgds[:],
which_data="field",
meta_data=meta_data,
h5dtype=self.as_type)
self.set_bg(qpi_bg)

def _get_metadata(self, dataset):
"""Return simulation-specific metadata
This uses the metadata previously extracted
from the simulation and set in `dataset.attrs`
to populate the QPI metadata.
"""
meta = super(SeriesHDF5SinogramMeep, self)._get_metadata(dataset)
meta.update(self.meta_data)
if "ACQUISITION_PHI" in dataset.attrs:
meta["angle"] = dataset.attrs["ACQUISITION_PHI"]

if "MEDIUM_RI" in dataset.attrs:
meta["medium index"] = dataset.attrs["MEDIUM_RI"]

if "SAMPLING" in dataset.attrs:
meta["pixel size"] = meta["wavelength"] / dataset.attrs["SAMPLING"]

if "extraction focus distance [px]" in dataset.attrs:
focus_px = dataset.attrs["extraction focus distance [px]"]
meta["focus"] = focus_px * meta["pixel size"]

meta["sim center"] = np.array(dataset.shape) / 2
meta["sim model"] = "fdtd"

return meta

def get_qpimage_raw(self, idx=0):
"""Return QPImage without background correction"""
name = self._get_data_indices()[idx]
with h5py.File(name=self.path, mode="r") as h5:
dataset = h5["sinogram"][name]["field"]
meta_data = self._get_metadata(dataset)
meta_data["time"] = float(idx)
qpi = qpimage.QPImage(data=dataset[:],
which_data="field",
meta_data=meta_data,
h5dtype=self.as_type)
# set identifier
qpi["identifier"] = self.get_identifier(idx)
return qpi

@staticmethod
def verify(path):
"""Verify the file format
The "file_format" attribute of the HDF5 file must contain
the strings "qpformat" and "meep".
"""
valid = SeriesHDF5SinogramGeneric.verify(path)
if valid:
with h5py.File(path, "r") as h5:
valid = "meep" in h5.attrs["file_format"].lower()
return valid

0 comments on commit c8f83f2

Please sign in to comment.