Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions news/fix-pdfparser-import.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
**Added:**

* No news added: Restores a module the package __init__ already referenced; no user-facing change since diffpy.cmipdf has not been released.

**Changed:**

* <news item>

**Deprecated:**

* <news item>

**Removed:**

* <news item>

**Fixed:**

* <news item>

**Security:**

* <news item>
171 changes: 171 additions & 0 deletions src/diffpy/cmipdf/pdfparser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
#!/usr/bin/env python
##############################################################################
#
# (c) 2025 Simon Billinge.
# All rights reserved.
#
# File coded by: Caden Myers, Simon Billinge, and members of the Billinge
# group.
#
# See GitHub contributions for a more detailed list of contributors.
# https://github.com/diffpy/diffpy.cmipdf/graphs/contributors
#
# See LICENSE.rst for license information.
#
##############################################################################
"""This module contains parsers for PDF data.

PDFParser is suitable for parsing data generated from PDFGetN and
PDFGetX.

See the class documentation for more information.
"""

__all__ = ["PDFParser"]

import re
from pathlib import Path

from diffpy.srfit.fitbase.profileparser import ProfileParser

_FLOAT_RX = r"[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?"


class PDFParser(ProfileParser):
"""Parser for PDF diffraction pattern data.

PDFgetX and PDFgetN write their header as plain ``name = value``
pairs, including ``stype = X`` or ``stype = N`` for the scattering
type, so this class parses files identically to ``ProfileParser``
for those. Some facilities instead prepend a
free-text instrument comment, so this class also falls back to
scanning that comment for the scattering type, ``qmin``, ``qmax``,
``qdamp``, and ``qbroad`` when they are not already present as
``name = value`` pairs.

Attributes
----------
_format
The name of the data format that this parses (string, default
``""``). The format string is a unique identifier for the data
format handled by the parser.
_banks
The data from each bank. Each bank contains a
(x, y, dx, dy) tuple:
x
A numpy array containing the independent
variable read from the file.
y
A numpy array containing the profile
from the file.
dx
A numpy array containing the uncertainty in x
read from the file. This is None if the
uncertainty cannot be read.
dy
A numpy array containing the uncertainty read
from the file. This is None if the uncertainty
cannot be read.
_x
The independent variable from the chosen bank.
_y
The profile from the chosen bank.
_dx
The uncertainty in independent variable from the chosen bank.
_dy
The uncertainty in profile from the chosen bank.
_meta
A dictionary containing metadata read from the file.

General Metadata
-----------------
filename
The name of the file from which data was parsed. This key
will not exist if data was not read from file.
nbanks
The number of banks parsed.
bank
The chosen bank number.

Metadata
--------
stype
The scattering type ("X", "N").
qmin
The minimum scattering vector (float).
qmax
The maximum scattering vector (float).
qdamp
The Q-resolution damping factor (float).
qbroad
The Q-resolution broadening factor (float).

These, along with any other ``name = value`` pairs in the header,
may appear in the metadata dictionary.
"""

_format = "PDF"

def _parse_metadata(self, filename):
"""Return the metadata read from a PDFgetX or PDFgetN header.

This calls ``ProfileParser``'s ``name = value`` based parsing
first, then falls back to scanning the free-text instrument
comments some facilities prepend to their files for the
scattering type and Q-resolution parameters that
such comments are not already covered by a ``name = value``
pair.

Parameters
----------
filename : str or Path
The name of the file to parse.

Returns
-------
dict
The metadata read from the file header.
"""
metadata = super()._parse_metadata(filename)
self._parse_comment_metadata(Path(filename).read_text(), metadata)
return metadata

@staticmethod
def _parse_comment_metadata(header_text, metadata):
"""Fill in stype, qmin, qmax, qdamp, and qbroad from free-text
instrument comments, without overwriting values already found by
the ``name = value`` based parsing.

Parameters
----------
header_text : str
The full text of the file being parsed.
meta : dict
The metadata dictionary to update in place.

Returns
-------
dict
The updated metadata dictionary.
"""
if "stype" not in metadata:
if re.search(r"(x-?ray|PDFgetX)", header_text, re.I):
metadata["stype"] = "X"
elif re.search(r"(neutron|PDFgetN)", header_text, re.I):
metadata["stype"] = "N"
regexes = {
"qmin": r"\bqmin *= *(%s)\b" % _FLOAT_RX,
"qmax": r"\bqmax *= *(%s)\b" % _FLOAT_RX,
"qdamp": r"\b(?:qdamp|qsig) *= *(%s)\b" % _FLOAT_RX,
"qbroad": r"\b(?:qbroad|qalp) *= *(%s)\b" % _FLOAT_RX,
}
for key, pattern in regexes.items():
if key in metadata:
continue
res = re.search(pattern, header_text, re.I)
if res:
metadata[key] = float(res.group(1))
return metadata


# End of PDFParser
9 changes: 9 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,12 @@ def _datafile(filename):
return importlib.resources.files("tests.testdata").joinpath(filename)

return _datafile


@pytest.fixture(scope="session")
def as_list():
def _as_list(values):
"""Unavailable uncertainties are None rather than an array."""
return None if values is None else values.tolist()

return _as_list
139 changes: 0 additions & 139 deletions tests/test_parser.py

This file was deleted.

Loading
Loading