|
| 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