Skip to content

Python Interfacing for PDFmorph (Clean Branch) #191

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
24 changes: 24 additions & 0 deletions news/morphpy.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
**Added:**

* Python interfacing to call PDFmorph
* Returns dictionary of morph metrics (dict) and the r, gr pair for plotting or further manipulation

**Changed:**

* <news item>

**Deprecated:**

* <news item>

**Removed:**

* <news item>

**Fixed:**

* <news item>

**Security:**

* <news item>
35 changes: 26 additions & 9 deletions src/diffpy/morph/morphapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import sys
from pathlib import Path

import numpy

import diffpy.morph.morph_helpers as helpers
import diffpy.morph.morph_io as io
import diffpy.morph.morphs as morphs
Expand Down Expand Up @@ -403,22 +405,31 @@ def custom_error(self, msg):
return parser


def single_morph(parser, opts, pargs, stdout_flag=True):
def single_morph(parser, opts, pargs, stdout_flag=True, python_wrap=False):
if len(pargs) < 2:
parser.error("You must supply FILE1 and FILE2.")
elif len(pargs) > 2:
elif len(pargs) > 2 and not python_wrap:
parser.error(
"Too many arguments. Make sure you only supply FILE1 and FILE2."
)
elif not (len(pargs) == 2 or len(pargs) == 6) and python_wrap:
parser.error("Python wrapper error.")

# Get the PDFs
x_morph, y_morph = getPDFFromFile(pargs[0])
x_target, y_target = getPDFFromFile(pargs[1])
# If we get from python, we may wrap, which has input size 4
if len(pargs) == 6 and python_wrap:
x_morph = pargs[2]
y_morph = pargs[3]
x_target = pargs[4]
y_target = pargs[5]
else:
x_morph, y_morph = getPDFFromFile(pargs[0])
x_target, y_target = getPDFFromFile(pargs[1])

if y_morph is None:
parser.error(f"No data table found in file: {pargs[0]}.")
parser.error(f"No data table found in: {pargs[0]}.")
if y_target is None:
parser.error(f"No data table found in file: {pargs[1]}.")
parser.error(f"No data table found in: {pargs[1]}.")

# Get configuration values
scale_in = "None"
Expand Down Expand Up @@ -614,10 +625,16 @@ def single_morph(parser, opts, pargs, stdout_flag=True):
l_width=l_width,
)

return morph_results
# Return different things depending on whether it is python interfaced
if python_wrap:
morph_info = morph_results
morph_table = numpy.array([chain.x_morph_out, chain.y_morph_out]).T
return morph_info, morph_table
else:
return morph_results


def multiple_targets(parser, opts, pargs, stdout_flag=True):
def multiple_targets(parser, opts, pargs, stdout_flag=True, python_wrap=False):
# Custom error messages since usage is distinct when --multiple tag is
# applied
if len(pargs) < 2:
Expand Down Expand Up @@ -800,7 +817,7 @@ def multiple_targets(parser, opts, pargs, stdout_flag=True):
return morph_results


def multiple_morphs(parser, opts, pargs, stdout_flag=True):
def multiple_morphs(parser, opts, pargs, stdout_flag=True, python_wrap=False):
# Custom error messages since usage is distinct when --multiple tag is
# applied
if len(pargs) < 2:
Expand Down
135 changes: 135 additions & 0 deletions src/diffpy/morph/morphpy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
#!/usr/bin/env python

import numpy as np

from diffpy.morph.morphapp import create_option_parser, single_morph


def get_args(parser, params, kwargs):
inputs = []
for key, value in params.items():
if value is not None:
inputs.append(f"--{key}")
inputs.append(f"{value}")
for key, value in kwargs.items():
key = key.replace("_", "-")
inputs.append(f"--{key}")
inputs.append(f"{value}")
(opts, pargs) = parser.parse_args(inputs)
return opts, pargs


def morph(
morph_file,
target_file,
scale=None,
stretch=None,
smear=None,
plot=False,
**kwargs,
):
"""Run diffpy.morph at Python level.
Parameters
----------
morph_file: str
Path-like object to the file to be morphed.
target_file: str
Path-like object to the target file.
scale: float, optional
Initial guess for the scaling parameter.
Refinement is done only for parameter that are not None.
stretch: float, optional
Initial guess for the stretching parameter.
smear: float, optional
Initial guess for the smearing parameter.
plot: bool
Show a plot of the morphed and target functions as well as the
difference curve (default: False).
kwargs: dict
See the diffpy.morph website for full list of options.
Returns
-------
morph_info: dict
Summary of morph parameters (e.g. scale, stretch, smear, rmin, rmax)
and results (e.g. Pearson, Rw).
morph_table: list
Function after morph where morph_table[:,0] is the abscissa and
morph_table[:,1] is the ordinate.
"""

parser = create_option_parser()
params = {
"scale": scale,
"stretch": stretch,
"smear": smear,
"noplot": True if not plot else None,
}
opts, pargs = get_args(parser, params, kwargs)

pargs = [morph_file, target_file]

return single_morph(
parser, opts, pargs, stdout_flag=False, python_wrap=True
)


def morphpy(
morph_table,
target_table,
scale=None,
stretch=None,
smear=None,
plot=False,
**kwargs,
):
"""Run diffpy.morph at Python level.
Parameters
----------
morph_table: numpy.array
Two-column array of (r, gr) for morphed function.
target_table: numpy.array
Two-column array of (r, gr) for target function.
scale: float, optional
Initial guess for the scaling parameter.
Refinement is done only for parameter that are not None.
stretch: float, optional
Initial guess for the stretching parameter.
smear: float, optional
Initial guess for the smearing parameter.
plot: bool
Show a plot of the morphed and target functions as well as the
difference curve (default: False).
kwargs: dict
See the diffpy.morph website for full list of options.
Returns
-------
morph_info: dict
Summary of morph parameters (e.g. scale, stretch, smear, rmin, rmax)
and results (e.g. Pearson, Rw).
morph_table: list
Function after morph where morph_table[:,0] is the abscissa and
morph_table[:,1] is the ordinate.
"""

parser = create_option_parser()
params = {
"scale": scale,
"stretch": stretch,
"smear": smear,
"noplot": True if not plot else None,
}
opts, pargs = get_args(parser, params, kwargs)

morph_table = np.array(morph_table)
target_table = np.array(target_table)

x_morph = morph_table[:, 0]
y_morph = morph_table[:, 1]
x_target = target_table[:, 0]
y_target = target_table[:, 1]

pargs = ["Morph", "Target", x_morph, y_morph, x_target, y_target]

return single_morph(
parser, opts, pargs, stdout_flag=False, python_wrap=True
)
120 changes: 120 additions & 0 deletions tests/test_morphpy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#!/usr/bin/env python

from pathlib import Path

import numpy
import pytest

from diffpy.morph.morphapp import create_option_parser, single_morph
from diffpy.morph.morphpy import morph, morphpy
from diffpy.morph.tools import getRw

thisfile = locals().get("__file__", "file.py")
tests_dir = Path(thisfile).parent.resolve()
testdata_dir = tests_dir.joinpath("testdata")
testsequence_dir = testdata_dir.joinpath("testsequence")

nickel_PDF = testdata_dir.joinpath("nickel_ss0.01.cgr")
serial_JSON = testdata_dir.joinpath("testsequence_serialfile.json")

testsaving_dir = testsequence_dir.joinpath("testsaving")
test_saving_succinct = testsaving_dir.joinpath("succinct")
test_saving_verbose = testsaving_dir.joinpath("verbose")
tssf = testdata_dir.joinpath("testsequence_serialfile.json")


class TestMorphpy:
@pytest.fixture
def setup_morph(self):
self.parser = create_option_parser()
filenames = [
"g_174K.gr",
"f_180K.gr",
"e_186K.gr",
"d_192K.gr",
"c_198K.gr",
"b_204K.gr",
"a_210K.gr",
]
self.testfiles = []
self.morphapp_results = {}

# Parse arguments sorting by field
(opts, pargs) = self.parser.parse_args(
[
"--scale",
"1",
"--stretch",
"0",
"-n",
"--sort-by",
"temperature",
]
)
for filename in filenames:
self.testfiles.append(testsequence_dir.joinpath(filename))

# Run multiple single morphs
morph_file = self.testfiles[0]
for target_file in self.testfiles[1:]:
pargs = [morph_file, target_file]
# store in same format of dictionary as multiple_targets
self.morphapp_results.update(
{
target_file.name: single_morph(
self.parser, opts, pargs, stdout_flag=False
)
}
)
return

def test_morph(self, setup_morph):
morph_results = {}
morph_file = self.testfiles[0]
for target_file in self.testfiles[1:]:
mr, grm = morph(
morph_file,
target_file,
scale=1,
stretch=0,
sort_by="temperature",
)
_, grt = morph(target_file, target_file)
morph_results.update({target_file.name: mr})

class Chain:
xyallout = grm[:, 0], grm[:, 1], grt[:, 0], grt[:, 1]

chain = Chain()
rw = getRw(chain)
del chain
assert numpy.allclose(
[rw], [self.morphapp_results[target_file.name]["Rw"]]
)
assert morph_results == self.morphapp_results

def test_morphpy(self, setup_morph):
morph_results = {}
morph_file = self.testfiles[0]
for target_file in self.testfiles[1:]:
_, grm0 = morph(morph_file, morph_file)
_, grt = morph(target_file, target_file)
mr, grm = morphpy(
grm0, grt, scale=1, stretch=0, sort_by="temperature"
)
morph_results.update({target_file.name: mr})

class Chain:
xyallout = grm[:, 0], grm[:, 1], grt[:, 0], grt[:, 1]

chain = Chain()
rw = getRw(chain)
del chain
assert numpy.allclose(
[rw], [self.morphapp_results[target_file.name]["Rw"]]
)
assert morph_results == self.morphapp_results


if __name__ == "__main__":
TestMorphpy()

Check warning on line 120 in tests/test_morphpy.py

View check run for this annotation

Codecov / codecov/patch

tests/test_morphpy.py#L120

Added line #L120 was not covered by tests
Loading