Skip to content

Commit b350d85

Browse files
Python Interfacing for PDFmorph (Clean Branch) (#191)
* Add morphpy and tests * [pre-commit.ci] auto fixes from pre-commit hooks * Fix pre-commit * Rename testmorph * Add funcy support to morphpy * Formatting tests * Remove whitespace * Add online documentation for morphpy * Documentation formatting --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent a8f53ae commit b350d85

File tree

10 files changed

+823
-35
lines changed

10 files changed

+823
-35
lines changed

doc/source/morphpy.rst

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
.. _morphpy:
2+
3+
Using diffpy.morph in Python
4+
############################
5+
6+
On top of the command-line usage described in the `quickstart tutorial <quickstart.html>`__,
7+
``diffpy.morph`` also supports Python integration.
8+
This functionality is intended for those acquainted with the basic morphs
9+
described in the aforementioned quickstart tutorial who want to use ``diffpy.morph`` in their
10+
Python scripts.
11+
12+
Python Morphing Functions
13+
=========================
14+
15+
1. In the quickstart tutorial, you were asked to try a combined scale, stretch, and smear
16+
morph on the files `darkSub_rh20_C_01.gr` and `darkSub_rh20_C_44.gr` using the command-line
17+
command ::
18+
19+
diffpy.morph --scale=0.8 --smear=-0.08 --stretch=0.5 --rmin=1.5 --rmax=30 darkSub_rh20_C_01.gr darkSub_rh20_C_44.gr
20+
21+
2. To do the same on Python, we must first create a new Python script in the same directory as the
22+
data files `darkSub_rh20_C_01.gr` and `darkSub_rh20_C_44.gr`.
23+
3. Then, in that script, import ::
24+
25+
from diffpy.morph.morphpy import morph
26+
27+
3. Finally, we run the ``morph`` function ::
28+
29+
morph_info, morph_table = morph("darkSub_rh20_C_01.gr", "darkSub_rh20_C_44.gr", scale=0.8, smear=-0.08, stretch=0.5, rmin=1.5, rmax=30)
30+
31+
* The ``morph`` function takes in two file names (or paths). You can also provide various parameters
32+
for morphing (see the Full Parameter List below).
33+
* If, let's say, the file `darkSub_rh20_C_01.gr` is in a subdirectory `subdir/darkSub_rh20_C_01.gr`,
34+
you should replace ``"darkSub_rh20_C_01.gr"`` in the above example with ``"subdir/darkSub_rh20_C_01.gr"``.
35+
36+
4. The ``morph`` function returns a dictionary ``morph_info`` and a numpy array ``morph_table``.
37+
38+
* ``morph_info`` contains all morphs as keys (e.g. ``"scale"``, ``"stretch"``, ``"smear"``) with
39+
the optimized morphing parameters found by ``diffpy.morph`` as values. ``morph_info`` also contains
40+
the Rw and Pearson correlation coefficients found post-morphing.
41+
* ``morph_table`` is a two-column array of the morphed function interpolated onto the grid of the
42+
target function (e.g. in our example, it returns the contents of `darkSub_rh20_C_01.gr` after
43+
the morphs are applied interpolated onto the grid of `darkSub_rh20_C_44.gr`).
44+
5. Notice that most parameters you are able to use are the same as the options provided in the command-line
45+
interface version of ``diffpy.morph``. For example, the ``--apply`` option becomes the ``apply=True`` parameter.
46+
6. With that, you have already mastered the basics of using ``diffpy.morph`` on Python!
47+
7. Note that instead of passing two files to ``diffpy.morph``, you might instead want to directly
48+
pass arrays. For example, rather than passing `darkSub_rh20_C_01.gr`, I may want to pass
49+
a two-column array named ``ds_rh20_c_01_array`` containing the data table contents of the file
50+
`darkSub_rh20_C_01.gr`. In this case, we have a separate function ::
51+
52+
from diffpy.morph.morphpy import morph_arrays
53+
54+
8. Assuming we have loaded the data in `darkSub_rh20_C_01.gr` into ``ds_rh20_c_01_array`` and
55+
`darkSub_rh20_C_44.gr` into ``ds_rh20_c_44_array``, we can apply the same morph as step 3
56+
by running ::
57+
58+
morph_info, morph_table = morph_arrays(ds_rh20_c_01_array, ds_rh20_c_44_array, scale=0.8, smear=-0.08, stretch=0.5, rmin=1.5, rmax=30)
59+
60+
9. Notice that the two-column format of the input to ``morph_arrays`` is the same as the
61+
output of ``morph`` and ``morph_arrays``. It is VERY IMPORTANT that the data is in two-column format
62+
rather than the traditional two-row format. This is to reflect the file formats conventionally
63+
used to store PDFs.
64+
10. For a full list of parameters used by (both) ``morph`` and ``morph_arrays``, see the Full Parameter List
65+
section below.
66+
67+
Full Parameter List
68+
===================
69+
70+
General Parameters
71+
------------------
72+
73+
save: str or path
74+
Save the morphed function to a the file passed to save. Use '-' for stdout.
75+
verbose: bool
76+
Print additional header details to saved files. These include details about the morph
77+
inputs and outputs.
78+
rmin: float
79+
Minimum r-value (abscissa) to use for function comparisons.
80+
rmax: float
81+
Maximum r-value (abscissa) to use for function comparisons.
82+
tolerance: float
83+
Specify least squares refiner tolerance when optimizing for morph parameters. Default: 10e-8.
84+
pearson: bool
85+
The refiner instead maximizes agreement in the Pearson function
86+
(default behavior is to minimize the residual).
87+
Note that this is insensitive to scale.
88+
addpearson: bool
89+
Maximize agreement in the Pearson function as well as minimizing the residual.
90+
91+
Manipulations
92+
-------------
93+
These parameters select the manipulations that are to be applied to the
94+
function. The passed values will be refined unless specifically
95+
excluded with the apply or exclude parameters.
96+
97+
apply: bool
98+
Apply morphs but do not refine.
99+
exclude: str
100+
Exclude a manipulation from refinement by name.
101+
scale: float
102+
Apply scale factor. This multiplies the function ordinate by scale.
103+
stretch: float
104+
Stretch function grid by a fraction stretch. Specifically, this multiplies the function grid by 1+stretch.
105+
squeeze: list of float
106+
Squeeze function grid given a polynomial
107+
p(x) = squeeze[0]+squeeze[1]*x+...+squeeze[n]*x^n. n is dependent on the number
108+
of values in the user-inputted comma-separated list.
109+
The morph transforms the function grid from x to x+p(x).
110+
When this parameter is given, hshift is disabled.
111+
When n>1, stretch is disabled.
112+
smear: float
113+
Smear the peaks with a Gaussian of width smear. This
114+
is done by convolving the function with a Gaussian
115+
with standard deviation smear. If both smear and
116+
smear_pdf are used, only smear_pdf will be
117+
applied.
118+
smear_pdf: float
119+
Convert PDF to RDF. Then, smear peaks with a Gaussian
120+
of width smear_pdf. Convert back to PDF. If both smear and
121+
smear_pdf are used, only smear_pdf will be
122+
applied.
123+
slope: float
124+
Slope of the baseline used in converting from PDF to RDF.
125+
This is used with the option smear_pdf. The slope will
126+
be estimated if not provided.
127+
hshift: float
128+
Shift the function horizontally by hshift to the right.
129+
vshift: float
130+
Shift the function vertically by vshift upward.
131+
qdamp: float
132+
Dampen PDF by a factor qdamp.
133+
radius: float
134+
Apply characteristic function of sphere with radius
135+
given by parameter radius. If pradius is also specified, instead apply
136+
characteristic function of spheroid with equatorial
137+
radius radius and polar radius pradius.
138+
pradius: float
139+
Apply characteristic function of spheroid with
140+
equatorial radius given by above parameter radius and polar radius pradius.
141+
If only pradius is specified, instead apply
142+
characteristic function of sphere with radius pradius.
143+
iradius: float
144+
Apply inverse characteristic function of sphere with
145+
radius iradius. If ipradius is also specified, instead
146+
apply inverse characteristic function of spheroid with
147+
equatorial radius iradius and polar radius ipradius.
148+
ipradius: float
149+
Apply inverse characteristic function of spheroid with
150+
equatorial radius iradius and polar radius ipradius.
151+
If only ipradius is specified, instead apply inverse
152+
characteristic function of sphere with radius ipradius.
153+
funcy: tuple (function, dict)
154+
See Python-Specific Morphs below.
155+
156+
Python-Specific Morphs
157+
======================
158+
159+
Some morphs in ``diffpy.morph`` are supported only in Python. Here, we detail
160+
how they are used and how to call them.
161+
162+
funcy: tuple (function, dict)
163+
This morph applies the function funcy[0] with parameters given in funcy[1].
164+
The function funcy[0] must be a function of both the abscissa and ordinate
165+
(e.g. take in at least two inputs with as many additional parameters as needed).
166+
For example, let's start with a two-column table with abscissa x and ordinate y.
167+
let us say we want to apply the function ::
168+
169+
def linear(x, y, a, b, c):
170+
return a * x + b * y + c
171+
172+
This function takes in both the abscissa and ordinate on top of three additional
173+
parameters a, b, and c. To use the funcy parameter with initial guesses
174+
a=1.0, b=2.0, c=3.0, we would pass ``funcy=(linear, {a: 1.0, b: 2.0, c: 3.0})``.

doc/source/quickstart.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ diffpy.morph Tutorial
44
#####################
55

66
Welcome! This will be a quick tutorial to accquaint users with ``diffpy.morph``
7-
and some of what it can do. To see more details and definitions about
7+
and some of what it can do on the command-line.
8+
For those wishing to integrate ``diffpy.morph`` into their Python scripts,
9+
see the `morphpy tutorial <morphpy.html>`__.
10+
11+
To see more details and definitions about
812
the morphs please see the publication describing ``diffpy.morph``.
913

1014
To be published:

news/morphpy.rst

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
**Added:**
2+
3+
* Python interfacing to call PDFmorph
4+
* Returns dictionary of morph metrics (dict) and the r, gr pair for plotting or further manipulation
5+
6+
**Changed:**
7+
8+
* <news item>
9+
10+
**Deprecated:**
11+
12+
* <news item>
13+
14+
**Removed:**
15+
16+
* <news item>
17+
18+
**Fixed:**
19+
20+
* <news item>
21+
22+
**Security:**
23+
24+
* <news item>

src/diffpy/morph/morph_io.py

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from __future__ import print_function
1818

19+
import inspect
1920
import sys
2021
from pathlib import Path
2122

@@ -66,29 +67,55 @@ def single_morph_output(
6667
+ "\n"
6768
)
6869

70+
mr_copy = morph_results.copy()
6971
morphs_out = "# Optimized morphing parameters:\n"
70-
# Handle special inputs
71-
if "squeeze" in morph_results:
72-
sq_dict = morph_results.pop("squeeze")
73-
rw_pos = list(morph_results.keys()).index("Rw")
74-
morph_results_list = list(morph_results.items())
72+
# Handle special inputs (numerical)
73+
if "squeeze" in mr_copy:
74+
sq_dict = mr_copy.pop("squeeze")
75+
rw_pos = list(mr_copy.keys()).index("Rw")
76+
morph_results_list = list(mr_copy.items())
7577
for idx, _ in enumerate(sq_dict):
7678
morph_results_list.insert(
7779
rw_pos + idx, (f"squeeze a{idx}", sq_dict[f"a{idx}"])
7880
)
79-
morph_results = dict(morph_results_list)
81+
mr_copy = dict(morph_results_list)
82+
funcy_function = None
83+
if "function" in mr_copy:
84+
funcy_function = mr_copy.pop("function")
85+
print(funcy_function)
86+
if "funcy" in mr_copy:
87+
fy_dict = mr_copy.pop("funcy")
88+
rw_pos = list(mr_copy.keys()).index("Rw")
89+
morph_results_list = list(mr_copy.items())
90+
for idx, key in enumerate(fy_dict):
91+
morph_results_list.insert(
92+
rw_pos + idx, (f"funcy {key}", fy_dict[key])
93+
)
94+
mr_copy = dict(morph_results_list)
8095
# Normal inputs
8196
morphs_out += "\n".join(
82-
f"# {key} = {morph_results[key]:.6f}" for key in morph_results.keys()
97+
f"# {key} = {mr_copy[key]:.6f}" for key in mr_copy.keys()
8398
)
99+
# Special inputs (functional)
100+
if funcy_function is not None:
101+
morphs_in += '# funcy function =\n"""\n'
102+
f_code, _ = inspect.getsourcelines(funcy_function)
103+
n_leading = len(f_code[0]) - len(f_code[0].lstrip())
104+
for idx, f_line in enumerate(f_code):
105+
f_code[idx] = f_line[n_leading:]
106+
morphs_in += "".join(f_code)
107+
morphs_in += '"""\n'
84108

85109
# Printing to terminal
86110
if stdout_flag:
87111
print(f"{morphs_in}\n{morphs_out}\n")
88112

89113
# Saving to file
90114
if save_file is not None:
91-
path_name = str(Path(morph_file).resolve())
115+
if not Path(morph_file).exists():
116+
path_name = "NO FILE PATH PROVIDED"
117+
else:
118+
path_name = str(Path(morph_file).resolve())
92119
header = "# PDF created by diffpy.morph\n"
93120
header += f"# from {path_name}"
94121

0 commit comments

Comments
 (0)