Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
woutdenolf committed Apr 23, 2024
1 parent d6ab4ff commit b276756
Show file tree
Hide file tree
Showing 25 changed files with 766 additions and 49 deletions.
7 changes: 7 additions & 0 deletions doc/howtoguides.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
How-to Guides
=============

.. toctree::

howtoguides/install
howtoguides/convert_files
8 changes: 8 additions & 0 deletions doc/howtoguides/convert_files.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Convert file formats
====================

Convert all files in the *xdi_files* and *xas_beamline_data* to *HDF5/NeXus* format

.. code-block:: bash
nxxas-convert xdi_files/*.* xas_beamline_data/*.* data.h5
6 changes: 6 additions & 0 deletions doc/howtoguides/install.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Install
=======

.. code-block:: bash
pip install pynxxas
3 changes: 3 additions & 0 deletions doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@ Library for reading and writing XAS data in `NeXus format <https://www.nexusform
.. toctree::
:hidden:

install
howtoguides
tutorials
api
6 changes: 6 additions & 0 deletions doc/tutorials.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Tutorials
=========

.. toctree::

tutorials/models
34 changes: 34 additions & 0 deletions doc/tutorials/models.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
Data models
===========

Data from different data formats are represented in memory as a *pydantic* models.
You can convert between different models and save/load models from file.

NeXus models
------------

Build an *NXxas* model instance in steps

.. code-block:: python
from pynxxas.models import NxXasModel
nxxas_model = NxXasModel(element="Fe", absorption_edge="K", mode="transmission")
nxxas_model.energy = [7, 7.1], "keV"
nxxas_model.intensity = [10, 20]
Create an *NXxas* model instance from a dictionary and convert back to a dictionary

.. code-block:: python
data_in = {
"NX_class": "NXsubentry",
"mode": "transmission",
"element": "Fe",
"absorption_edge": "K",
"energy": [[7, 7.1], "keV"],
"intensity": [10, 20],
}
nxxas_model = NxXasModel(**data_in)
data_out = nxxas_model.model_dump()
4 changes: 3 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@ package_dir=
packages=find:
python_requires = >=3.8
install_requires =
typing_extensions; python_version < "3.9"
strenum; python_version < "3.11"
numpy
h5py
pydantic >=2.6
pint
typing_extensions; python_version < "3.9"
periodictable

[options.packages.find]
where=src
Expand Down
2 changes: 2 additions & 0 deletions src/pynxxas/apps/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
"""Command-Line Interface (CLI)
"""
90 changes: 82 additions & 8 deletions src/pynxxas/apps/nxxas_convert.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import sys
import pathlib
import logging
import argparse
from glob import glob

from ..io.xdi import read_xdi
from .. import io
from .. import models
from ..models import convert

logger = logging.getLogger(__name__)


def main(argv=None):
Expand All @@ -14,20 +19,89 @@ def main(argv=None):
prog="nxxas_convert", description="Convert data to NXxas format"
)

parser.add_argument("--output", type=str, default=None, help="Path to HDF5 file")
parser.add_argument(
"patterns",
"--output-format",
type=str,
nargs="+",
help="Glob file name patterns",
default="nexus",
choices=list(models.MODELS),
help="Output format",
)

parser.add_argument(
"file_patterns",
type=str,
nargs="*",
help="Files to convert",
)

parser.add_argument(
"output_filename", type=pathlib.Path, help="Convert destination filename"
)

args = parser.parse_args(argv[1:])
logging.basicConfig()

for pattern in args.patterns:
for filename in glob(pattern):
read_xdi(filename)
output_filename = args.output_filename
model_type = models.MODELS[args.output_format]

if output_filename.exists():
result = input(f"Overwrite {output_filename}? (y/[n])")
if not result.lower() in ("y", "yes"):
return 0
output_filename.unlink()

filenames = list()
for pattern in args.file_patterns:
filenames.extend(glob(pattern))

ndigitsfile = len(str(len(filenames) - 1))
return_code = 0
for file_number, filename in enumerate(filenames, 1):
try:
models_in = io.load_models(filename)
except NotImplementedError as e:
return_code = 1
logger.warning("Error when loading '%s': %s", filename, e)
continue
except Exception:
return_code = 1
logger.error("Error when loading '%s'", filename, exc_info=True)
continue

try:
models_out = [convert.convert_model(m, model_type) for m in models_in]
except NotImplementedError as e:
return_code = 1
logger.warning("Error when loading '%s': %s", filename, e)
continue
except Exception:
return_code = 1
logger.error("Error when converting '%s'", filename, exc_info=True)
continue

ndigitsscan = len(str(len(models_out) - 1))
for scan_number, model_out in enumerate(models_out, 1):
if args.output_format == "nexus":
url = f"{output_filename}?path=/{file_number}.{scan_number}"
else:
nrfmt = f"{{:0{ndigitsfile}d}}_{{:0{ndigitsscan}d}}"
scan_number = nrfmt.format(file_number, scan_number)
url = (
output_filename.parent
/ f"{output_filename.stem}_{scan_number:02}{output_filename.suffix}"
)

try:
io.save_model(model_out, url)
except NotImplementedError as e:
return_code = 1
logger.warning("Error when saving '%s' in '%s': %s", filename, url, e)
except Exception:
return_code = 1
logger.error(
"Error when saving '%s' in '%s'", filename, url, exc_info=True
)
return return_code


if __name__ == "__main__":
Expand Down
30 changes: 30 additions & 0 deletions src/pynxxas/io/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""File formats
"""

from typing import List

import pydantic

from .url_utils import UrlType
from . import xdi
from . import nexus
from .. import models


def load_models(url: UrlType) -> List[pydantic.BaseModel]:
if xdi.is_xdi_file(url):
return xdi.load_xdi_file(url)
if nexus.is_nexus_file(url):
return nexus.load_nexus_file(url)
raise NotImplementedError(f"File format not supported: {url}")


def save_model(model_instance: pydantic.BaseModel, url: UrlType) -> None:
if isinstance(model_instance, models.NxXasModel):
nexus.save_nexus_file(model_instance, url)
elif isinstance(model_instance, models.XdiModel):
xdi.save_xdi_file(model_instance, url)
else:
raise NotImplementedError(
f"Saving of {type(model_instance).__name__} not implemented"
)
41 changes: 41 additions & 0 deletions src/pynxxas/io/hdf5_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import os
from typing import Optional, Union

import h5py


def create_hdf5_link(
h5group: h5py.Group,
target_name: str,
target_filename: Optional[str],
absolute: bool = False,
) -> Union[h5py.SoftLink, h5py.ExternalLink]:
"""Create HDF5 soft link (supports relative down paths) or external link (supports relative paths)."""
this_name = h5group.name
this_filename = h5group.file.filename

target_filename = target_filename or this_filename

if os.path.isabs(target_filename):
rel_target_filename = os.path.relpath(target_filename, this_filename)
else:
rel_target_filename = target_filename
target_filename = os.path.abs(os.path.join(this_filename, target_filename))

if "." not in target_name:
rel_target_name = os.path.relpath(target_name, this_name)
else:
rel_target_name = target_name
target_name = os.path.abspath(os.path.join(this_name, target_name))

# Internal link
if rel_target_filename == ".":
if absolute or ".." in rel_target_name:
# h5py.SoftLink does not support relative links upwards
return h5py.SoftLink(target_name)
return h5py.SoftLink(rel_target_name)

# External link
if absolute:
return h5py.ExternalLink(target_filename, target_name)
return h5py.ExternalLink(rel_target_filename, target_name)
Loading

0 comments on commit b276756

Please sign in to comment.