import logging
import pathlib
from .. import errors
from .. import meta
from .fmt_hdf5 import recipe_hdf5
from .fmt_igor import recipe_ibw
from .fmt_jpk import (
recipe_jpk_force,
recipe_jpk_force_map,
recipe_jpk_force_qi_data,
recipe_jpk_force_qi_series,
)
from .fmt_tab import recipe_tab
from .fmt_chiaro_txt import recipe_chiaro_txt
from .fmt_ntmdt_txt import recipe_ntmdt_txt
from .fmt_workshop import recipe_workshop_single, recipe_workshop_map
from ..mod_force_distance import AFMForceDistance
from ..mod_creep_compliance import AFMCreepCompliance
from ..mod_stress_relaxation import AFMStressRelaxation
__all__ = ["AFMFormatRecipe", "find_data", "get_recipe", "load_data",
"default_data_classes_by_modality", "formats_available",
"formats_by_suffix", "formats_by_modality", "supported_extensions"]
logger = logging.getLogger(__name__)
[docs]
def find_data(path, modality=None):
"""Recursively find valid AFM data files
Parameters
----------
path: str or pathlib.Path
file or directory
modality: str
modality of the measurement ("force-distance")
Returns
-------
file_list: list of pathlib.Path
list of valid AFM data files
"""
path = pathlib.Path(path)
file_list = []
if path.is_dir():
# recurse into subdirectories
for pp in path.rglob("*"):
if pp.is_file():
file_list += find_data(pp, modality=modality)
else:
try:
get_recipe(path=path, modality=modality)
except errors.FileFormatNotSupportedError:
# not a valid file format
logger.debug("Skipping unsupported file '%s'", path)
else:
# valid file format
file_list.append(path)
return file_list
[docs]
def get_recipe(path, modality=None):
"""Return the file format recipe for a given path
Parameters
----------
path: str or pathlib.Path
file or directory
modality: str
modality of the measurement (see :const:`IMAGING_MODALITIES`)
Returns
-------
recipe: AFMFormatRecipe
file format recipe
"""
path = pathlib.Path(path)
if path.suffix not in formats_by_suffix:
raise errors.FileFormatNotSupportedError(
f"No recipe for suffix '{path.suffix}' (file '{path}')!")
recipes = formats_by_suffix[path.suffix]
for rec in recipes:
try:
supported = rec.detect(path)
except BaseException:
logger.debug(
"Detect failed for '%s' using recipe '%s'. Traceback follows.",
path, rec, exc_info=True)
supported = False
if ((modality is None or modality in rec.modalities) and supported):
break
logger.debug(
"Recipe '%s' did not match '%s' for modality '%s'",
rec, path, modality)
else:
logger.debug(
"No recipe matched '%s' for modality '%s'. Tried: %s",
path, modality, [r.descr for r in recipes])
raise errors.FileFormatNotSupportedError(
f"Could not determine file format recipe for '{path}'!")
return rec
[docs]
def load_data(path, meta_override=None, modality=None,
data_classes_by_modality=None, diskcache=False,
callback=None):
"""Load AFM data
Parameters
----------
path: str or pathlib.Path
Path to AFM data file
meta_override: dict
Metadata dictionary that overrides experimental metadata
modality: str
Which acquisition modality to use (e.g. "force-distance")
data_classes_by_modality: dict
Override the default AFMData class to use for managing the data
(see :data:`default_data_classes_by_modality`): This is e.g.
used by :ref:`nanite:index` to pass `Indentation` (which is a
subclass of the default `AFMForceDistance`) for handling
"force-indentation" data.
diskcache: bool
Whether to use caching (not implemented)
callback: callable
A method that accepts a float between 0 and 1
to externally track the process of loading the data
Returns
-------
afm_list: list of afmformats.afm_data.AFMData
List where each element is on AFMData curve
"""
if meta_override is None:
meta_override = {}
if data_classes_by_modality is None:
data_classes_by_modality = {}
path = pathlib.Path(path)
if path.suffix in formats_by_suffix:
afmdata = []
cur_recipe = get_recipe(path, modality=modality)
loader = cur_recipe.loader
if modality is None:
modality = cur_recipe.get_modality(path)
fix_modality = False
else:
fix_modality = True
if modality in data_classes_by_modality:
afm_data_class = data_classes_by_modality[modality]
else:
afm_data_class = default_data_classes_by_modality[modality]
try:
for dd in loader(path,
callback=callback,
meta_override=meta_override):
dd["metadata"]["format"] = "{} ({})".format(
cur_recipe["maker"], cur_recipe["descr"])
if fix_modality and dd["metadata"]["imaging mode"] != modality:
# The user explicitly requested this modality.
logger.debug(
"Skipping dataset with modality '%s' (expected '%s') "
"from '%s'",
dd["metadata"]["imaging mode"], modality, path)
continue
ddi = afm_data_class(data=dd["data"],
metadata=dd["metadata"],
diskcache=diskcache)
afmdata.append(ddi)
except BaseException:
logger.exception(
"Loader failed for '%s' using recipe '%s'. Traceback follows.",
path, cur_recipe)
raise
logger.debug(
"Loaded %d dataset(s) from '%s' using '%s'",
len(afmdata), path, cur_recipe.descr)
else:
logger.debug(
"Loader failed for '%s' as the extension '%s' is not recognised.",
path, path.suffix)
raise ValueError("Unsupported file extension: '{}'!".format(path))
return afmdata
def register_format(recipe):
"""Registers a file format from a recipe dictionary"""
afr = AFMFormatRecipe(recipe)
formats_available.append(afr)
# suffix
if afr.suffix not in formats_by_suffix:
formats_by_suffix[afr.suffix] = []
formats_by_suffix[afr.suffix].append(afr)
# modality
for modality in afr.modalities:
if modality not in formats_by_modality:
formats_by_modality[modality] = []
formats_by_modality[modality].append(afr)
# supported extensions
if afr.suffix not in supported_extensions: # avoid duplicates
supported_extensions.append(afr.suffix)
supported_extensions.sort()
#: dictionary with default data classes for each modality
default_data_classes_by_modality = {
"force-distance": AFMForceDistance,
"creep-compliance": AFMCreepCompliance,
"stress-relaxation": AFMStressRelaxation,
}
#: available/supported file formats
formats_available = []
#: available file formats in a dictionary with suffix keys
formats_by_suffix = {}
#: available file formats in a dictionary for each modality
formats_by_modality = {}
#: list of supported extensions
supported_extensions = []
for _recipe in [
recipe_hdf5,
recipe_ibw,
recipe_jpk_force,
recipe_jpk_force_map,
recipe_jpk_force_qi_data,
recipe_jpk_force_qi_series,
recipe_ntmdt_txt,
recipe_chiaro_txt,
recipe_tab,
recipe_workshop_map,
recipe_workshop_single,
]:
register_format(_recipe)