Skip to content

Commit 227db6c

Browse files
authored
Merge pull request #1 from cadenmyers13/pdf-pack
pdf code from diffpy.srfit
2 parents 299cfc8 + a6dc73e commit 227db6c

18 files changed

+18502
-74
lines changed

news/pdf-pack.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
**Added:**
2+
3+
* Add source code from ``diffpy.srfit.pdf`` to ``diffpy.cmipdf``.
4+
5+
**Changed:**
6+
7+
* <news item>
8+
9+
**Deprecated:**
10+
11+
* <news item>
12+
13+
**Removed:**
14+
15+
* <news item>
16+
17+
**Fixed:**
18+
19+
* <news item>
20+
21+
**Security:**
22+
23+
* <news item>

requirements/conda.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
numpy
2+
diffpy.srfit
3+
diffpy.structure

src/diffpy/cmipdf/__init__.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
# (c) 2025 Simon Billinge.
55
# All rights reserved.
66
#
7-
# File coded by: Caden Myers, Simon Billinge, and members of the Billinge group.
7+
# File coded by: Caden Myers, Simon Billinge, and members of the Billinge
8+
# group.
89
#
910
# See GitHub contributions for a more detailed list of contributors.
1011
# https://github.com/diffpy/diffpy.cmipdf/graphs/contributors
@@ -14,9 +15,16 @@
1415
##############################################################################
1516
"""The code of the PDF pack of the diffpy.cmi package."""
1617

18+
from diffpy.cmipdf.debyepdfgenerator import DebyePDFGenerator
19+
from diffpy.cmipdf.pdfcontribution import PDFContribution
20+
from diffpy.cmipdf.pdfgenerator import PDFGenerator
21+
from diffpy.cmipdf.pdfparser import PDFParser
22+
1723
# package version
1824
from diffpy.cmipdf.version import __version__ # noqa
1925

26+
__all__ = ["PDFGenerator", "DebyePDFGenerator", "PDFContribution", "PDFParser"]
27+
2028
# silence the pyflakes syntax checker
2129
assert __version__ or True
2230

Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
#!/usr/bin/env python
2+
##############################################################################
3+
#
4+
# (c) 2025 Simon Billinge.
5+
# All rights reserved.
6+
#
7+
# File coded by: Caden Myers, Simon Billinge, and members of the Billinge
8+
# group.
9+
#
10+
# See GitHub contributions for a more detailed list of contributors.
11+
# https://github.com/diffpy/diffpy.cmipdf/graphs/contributors
12+
#
13+
# See LICENSE.rst for license information.
14+
#
15+
##############################################################################
16+
"""PDF profile generator base class.
17+
18+
The BasePDFGenerator class interfaces with SrReal PDF calculators and is
19+
used as a base for the PDFGenerator and DebyePDFGenerator classes.
20+
"""
21+
22+
__all__ = ["BasePDFGenerator"]
23+
24+
import numpy
25+
26+
from diffpy.srfit.exceptions import SrFitError
27+
from diffpy.srfit.fitbase import ProfileGenerator
28+
from diffpy.srfit.fitbase.parameter import ParameterAdapter
29+
from diffpy.srfit.structure import struToParameterSet
30+
31+
# FIXME - Parameter creation will have to be smarter once deeper calculator
32+
# configuration is enabled.
33+
34+
35+
class BasePDFGenerator(ProfileGenerator):
36+
"""Base class for calculating PDF profiles using SrReal.
37+
38+
This works with diffpy.structure.Structure, pyobjcryst.crystal.Crystal and
39+
pyobjcryst.molecule.Molecule instances. Note that the managed Parameters
40+
are not created until the structure is added.
41+
42+
Attributes
43+
----------
44+
_calc
45+
PDFCalculator or DebyePDFCalculator instance for calculating
46+
the PDF.
47+
_phase
48+
The structure ParameterSet used to calculate the profile.
49+
stru
50+
The structure objected adapted by _phase.
51+
_lastr
52+
The last value of r over which the PDF was calculated. This is
53+
used to configure the calculator when r changes.
54+
_pool
55+
A multiprocessing.Pool for managing parallel computation.
56+
57+
Managed Parameters
58+
------------------
59+
scale
60+
Scale factor
61+
delta1
62+
Linear peak broadening term
63+
delta2
64+
Quadratic peak broadening term
65+
qbroad
66+
Resolution peak broadening term
67+
qdamp
68+
Resolution peak dampening term
69+
Managed ParameterSets
70+
The structure ParameterSet (SrRealParSet instance) used to
71+
calculate the profile is named by the user.
72+
73+
Usable Metadata
74+
---------------
75+
stype
76+
The scattering type "X" for x-ray, "N" for neutron (see
77+
'setScatteringType').
78+
qmax
79+
The maximum scattering vector used to generate the PDF (see
80+
setQmax).
81+
qmin
82+
The minimum scattering vector used to generate the PDF (see
83+
setQmin).
84+
scale
85+
See Managed Parameters.
86+
delta1
87+
See Managed Parameters.
88+
delta2
89+
See Managed Parameters.
90+
qbroad
91+
See Managed Parameters.
92+
qdamp
93+
See Managed Parameters.
94+
"""
95+
96+
def __init__(self, name="pdf"):
97+
"""Initialize the generator."""
98+
ProfileGenerator.__init__(self, name)
99+
100+
self._phase = None
101+
self.stru = None
102+
self.meta = {}
103+
self._lastr = numpy.empty(0)
104+
self._calc = None
105+
106+
self._pool = None
107+
108+
return
109+
110+
_parnames = ["delta1", "delta2", "qbroad", "scale", "qdamp"]
111+
112+
def _setCalculator(self, calc):
113+
"""Set the SrReal calculator instance.
114+
115+
Setting the calculator creates Parameters from the variable
116+
attributes of the SrReal calculator.
117+
"""
118+
self._calc = calc
119+
for pname in self.__class__._parnames:
120+
self.addParameter(ParameterAdapter(pname, self._calc, attr=pname))
121+
self.processMetaData()
122+
return
123+
124+
def parallel(self, ncpu, mapfunc=None):
125+
"""Run calculation in parallel.
126+
127+
Attributes
128+
----------
129+
ncpu
130+
Number of parallel processes. Revert to serial mode when 1.
131+
mapfunc
132+
A mapping function to use. If this is None (default),
133+
multiprocessing.Pool.imap_unordered will be used.
134+
135+
No return value.
136+
"""
137+
from diffpy.srreal.parallel import createParallelCalculator
138+
139+
calc_serial = self._calc
140+
if hasattr(calc_serial, "pqobj"):
141+
calc_serial = calc_serial.pqobj
142+
# revert to serial calculator for ncpu <= 1
143+
if ncpu <= 1:
144+
self._calc = calc_serial
145+
self._pool = None
146+
return
147+
# Why don't we let the user shoot his foot or test on single CPU?
148+
# ncpu = min(ncpu, multiprocessing.cpu_count())
149+
if mapfunc is None:
150+
import multiprocessing
151+
152+
self._pool = multiprocessing.Pool(ncpu)
153+
mapfunc = self._pool.imap_unordered
154+
155+
self._calc = createParallelCalculator(calc_serial, ncpu, mapfunc)
156+
return
157+
158+
def processMetaData(self):
159+
"""Process the metadata once it gets set."""
160+
ProfileGenerator.processMetaData(self)
161+
162+
stype = self.meta.get("stype")
163+
if stype is not None:
164+
self.setScatteringType(stype)
165+
166+
qmax = self.meta.get("qmax")
167+
if qmax is not None:
168+
self.setQmax(qmax)
169+
170+
qmin = self.meta.get("qmin")
171+
if qmin is not None:
172+
self.setQmin(qmin)
173+
174+
for name in self.__class__._parnames:
175+
val = self.meta.get(name)
176+
if val is not None:
177+
par = self.get(name)
178+
par.setValue(val)
179+
180+
return
181+
182+
def setScatteringType(self, stype="X"):
183+
"""Set the scattering type.
184+
185+
Attributes
186+
----------
187+
stype
188+
"X" for x-ray, "N" for neutron, "E" for electrons,
189+
or any registered type from diffpy.srreal from
190+
ScatteringFactorTable.getRegisteredTypes().
191+
192+
Raises ValueError for unknown scattering type.
193+
"""
194+
self._calc.setScatteringFactorTableByType(stype)
195+
# update the meta dictionary only if there was no exception
196+
self.meta["stype"] = self.getScatteringType()
197+
return
198+
199+
def getScatteringType(self):
200+
"""Get the scattering type.
201+
202+
See 'setScatteringType'.
203+
"""
204+
return self._calc.getRadiationType()
205+
206+
def setQmax(self, qmax):
207+
"""Set the qmax value."""
208+
self._calc.qmax = qmax
209+
self.meta["qmax"] = self.getQmax()
210+
return
211+
212+
def getQmax(self):
213+
"""Get the qmax value."""
214+
return self._calc.qmax
215+
216+
def setQmin(self, qmin):
217+
"""Set the qmin value."""
218+
self._calc.qmin = qmin
219+
self.meta["qmin"] = self.getQmin()
220+
return
221+
222+
def getQmin(self):
223+
"""Get the qmin value."""
224+
return self._calc.qmin
225+
226+
def setStructure(self, stru, name="phase", periodic=True):
227+
"""Set the structure that will be used to calculate the PDF.
228+
229+
This creates a DiffpyStructureParSet, ObjCrystCrystalParSet or
230+
ObjCrystMoleculeParSet that adapts stru to a ParameterSet interface.
231+
See those classes (located in diffpy.srfit.structure) for how they are
232+
used. The resulting ParameterSet will be managed by this generator.
233+
234+
Attributes
235+
----------
236+
stru
237+
diffpy.structure.Structure, pyobjcryst.crystal.Crystal or
238+
pyobjcryst.molecule.Molecule instance. Default None.
239+
name
240+
A name to give to the managed ParameterSet that adapts stru
241+
(default "phase").
242+
periodic
243+
The structure should be treated as periodic (default
244+
True). Note that some structures do not support
245+
periodicity, in which case this will have no effect on the
246+
PDF calculation.
247+
"""
248+
249+
# Create the ParameterSet
250+
parset = struToParameterSet(name, stru)
251+
252+
# Set the phase
253+
self.setPhase(parset, periodic)
254+
return
255+
256+
def setPhase(self, parset, periodic=True):
257+
"""Set the phase that will be used to calculate the PDF.
258+
259+
Set the phase directly with a DiffpyStructureParSet,
260+
ObjCrystCrystalParSet or ObjCrystMoleculeParSet that adapts a structure
261+
object (from diffpy or pyobjcryst). The passed ParameterSet will be
262+
managed by this generator.
263+
264+
Attributes
265+
----------
266+
parset
267+
A SrRealParSet that holds the structural information.
268+
This can be used to share the phase between multiple
269+
BasePDFGenerators, and have the changes in one reflect in
270+
another.
271+
periodic
272+
The structure should be treated as periodic (default True).
273+
Note that some structures do not support periodicity, in
274+
which case this will be ignored.
275+
"""
276+
# Store the ParameterSet for easy access
277+
self._phase = parset
278+
self.stru = self._phase.stru
279+
280+
# Put this ParameterSet in the ProfileGenerator.
281+
self.addParameterSet(parset)
282+
283+
# Set periodicity
284+
self._phase.useSymmetry(periodic)
285+
return
286+
287+
def _prepare(self, r):
288+
"""Prepare the calculator when a new r-value is passed."""
289+
self._lastr = r.copy()
290+
lo, hi = r.min(), r.max()
291+
ndiv = max(len(r) - 1, 1)
292+
self._calc.rstep = (hi - lo) / ndiv
293+
self._calc.rmin = lo
294+
self._calc.rmax = hi + 0.5 * self._calc.rstep
295+
return
296+
297+
def _validate(self):
298+
"""Validate my state.
299+
300+
This validates that the phase is not None. This performs
301+
ProfileGenerator validations.
302+
303+
Raises SrFitError if validation fails.
304+
"""
305+
if self._calc is None:
306+
raise SrFitError("_calc is None")
307+
if self._phase is None:
308+
raise SrFitError("_phase is None")
309+
ProfileGenerator._validate(self)
310+
return
311+
312+
def __call__(self, r):
313+
"""Calculate the PDF.
314+
315+
This ProfileGenerator will be used in a fit equation that will
316+
be optimized to fit some data. By the time this function is
317+
evaluated, the crystal has been updated by the optimizer via the
318+
ObjCrystParSet created in setCrystal. Thus, we need only call
319+
pdf with the internal structure object.
320+
"""
321+
if not numpy.array_equal(r, self._lastr):
322+
self._prepare(r)
323+
324+
rcalc, y = self._calc(self._phase._getSrRealStructure())
325+
326+
if numpy.isnan(y).any():
327+
y = numpy.zeros_like(r)
328+
else:
329+
y = numpy.interp(r, rcalc, y)
330+
return y
331+
332+
333+
# End class BasePDFGenerator

0 commit comments

Comments
 (0)