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