diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0e4a84d..f189a94 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: submodules: false repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v6.0.0 hooks: - id: check-yaml - id: end-of-file-fixer @@ -21,45 +21,45 @@ repos: - id: check-toml - id: check-added-large-files - repo: https://github.com/psf/black - rev: 24.4.2 + rev: 26.3.1 hooks: - id: black - repo: https://github.com/pycqa/flake8 - rev: 7.0.0 + rev: 7.3.0 hooks: - id: flake8 - repo: https://github.com/pycqa/isort - rev: 5.13.2 + rev: 8.0.1 hooks: - id: isort args: ["--profile", "black"] - repo: https://github.com/kynan/nbstripout - rev: 0.7.1 + rev: 0.9.1 hooks: - id: nbstripout - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v6.0.0 hooks: - id: no-commit-to-branch name: Prevent Commit to Main Branch args: ["--branch", "main"] stages: [pre-commit] - repo: https://github.com/codespell-project/codespell - rev: v2.3.0 + rev: v2.4.2 hooks: - id: codespell additional_dependencies: - tomli # prettier - multi formatter for .json, .yml, and .md files - repo: https://github.com/pre-commit/mirrors-prettier - rev: f12edd9c7be1c20cfa42420fd0e6df71e42b51ea # frozen: v4.0.0-alpha.8 + rev: v4.0.0-alpha.8 hooks: - id: prettier additional_dependencies: - "prettier@^3.2.4" # docformatter - PEP 257 compliant docstring formatter - - repo: https://github.com/s-weigand/docformatter - rev: 5757c5190d95e5449f102ace83df92e7d3b06c6c + - repo: https://github.com/PyCQA/docformatter + rev: v1.7.7 hooks: - id: docformatter additional_dependencies: [tomli] diff --git a/news/migrate-changes.rst b/news/migrate-changes.rst new file mode 100644 index 0000000..caff897 --- /dev/null +++ b/news/migrate-changes.rst @@ -0,0 +1,23 @@ +**Added:** + +* No news added: updated all functions to be snake_case. + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/requirements/conda.txt b/requirements/conda.txt index 6b826ba..6b1daa8 100644 --- a/requirements/conda.txt +++ b/requirements/conda.txt @@ -1,3 +1,4 @@ numpy +diffpy.srreal diffpy.srfit diffpy.structure diff --git a/requirements/pip.txt b/requirements/pip.txt index 24ce15a..6b1daa8 100644 --- a/requirements/pip.txt +++ b/requirements/pip.txt @@ -1 +1,4 @@ numpy +diffpy.srreal +diffpy.srfit +diffpy.structure diff --git a/src/diffpy/cmipdf/basepdfgenerator.py b/src/diffpy/cmipdf/basepdfgenerator.py index e3ab3e8..14ea083 100644 --- a/src/diffpy/cmipdf/basepdfgenerator.py +++ b/src/diffpy/cmipdf/basepdfgenerator.py @@ -46,7 +46,7 @@ class BasePDFGenerator(ProfileGenerator): the PDF. _phase The structure ParameterSet used to calculate the profile. - stru + structure The structure objected adapted by _phase. _lastr The last value of r over which the PDF was calculated. This is @@ -72,15 +72,15 @@ class BasePDFGenerator(ProfileGenerator): Usable Metadata --------------- - stype + stype : str The scattering type "X" for x-ray, "N" for neutron (see - 'setScatteringType'). + 'set_scattering_type'). qmax The maximum scattering vector used to generate the PDF (see - setQmax). + set_qmax). qmin The minimum scattering vector used to generate the PDF (see - setQmin). + set_qmin). scale See Managed Parameters. delta1 @@ -98,7 +98,7 @@ def __init__(self, name="pdf"): ProfileGenerator.__init__(self, name) self._phase = None - self.stru = None + self.structure = None self.meta = {} self._lastr = numpy.empty(0) self._calc = None @@ -109,7 +109,7 @@ def __init__(self, name="pdf"): _parnames = ["delta1", "delta2", "qbroad", "scale", "qdamp"] - def _setCalculator(self, calc): + def _set_calculator(self, calc): """Set the SrReal calculator instance. Setting the calculator creates Parameters from the variable @@ -118,13 +118,13 @@ def _setCalculator(self, calc): self._calc = calc for pname in self.__class__._parnames: self.addParameter(ParameterAdapter(pname, self._calc, attr=pname)) - self.processMetaData() + self._process_metadata() return def parallel(self, ncpu, mapfunc=None): """Run calculation in parallel. - Attributes + Parameters ---------- ncpu Number of parallel processes. Revert to serial mode when 1. @@ -155,91 +155,120 @@ def parallel(self, ncpu, mapfunc=None): self._calc = createParallelCalculator(calc_serial, ncpu, mapfunc) return - def processMetaData(self): + def _process_metadata(self): """Process the metadata once it gets set.""" - ProfileGenerator.processMetaData(self) + ProfileGenerator._process_metadata(self) stype = self.meta.get("stype") if stype is not None: - self.setScatteringType(stype) + self.set_scattering_type(stype) qmax = self.meta.get("qmax") if qmax is not None: - self.setQmax(qmax) + self.set_qmax(qmax) qmin = self.meta.get("qmin") if qmin is not None: - self.setQmin(qmin) + 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.setValue(val) + par.set_value(val) return - def setScatteringType(self, stype="X"): + def set_scattering_type(self, stype="X"): """Set the scattering type. - Attributes + Parameters ---------- - stype - "X" for x-ray, "N" for neutron, "E" for electrons, + 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 for unknown scattering type. + 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.getScatteringType() + self.meta["stype"] = self.get_scattering_type() return - def getScatteringType(self): + def get_scattering_type(self): """Get the scattering type. - See 'setScatteringType'. + See 'set_scattering_type'. """ return self._calc.getRadiationType() - def setQmax(self, qmax): - """Set the qmax value.""" + 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.getQmax() + self.meta["qmax"] = self.get_qmax() return - def getQmax(self): - """Get the qmax value.""" + def get_qmax(self): + """Get the qmax value. + + Returns + ------- + float + The maximum scattering vector used to generate the PDF. + """ return self._calc.qmax - def setQmin(self, qmin): - """Set the qmin value.""" + 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.getQmin() + self.meta["qmin"] = self.get_qmin() return - def getQmin(self): - """Get the qmin value.""" + def get_qmin(self): + """Get the qmin value. + + Returns + ------- + float + The minimum scattering vector used to generate the PDF. + """ return self._calc.qmin - def setStructure(self, stru, name="phase", periodic=True): + 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 stru to a ParameterSet interface. + 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. - Attributes + Parameters ---------- - stru - diffpy.structure.Structure, pyobjcryst.crystal.Crystal or - pyobjcryst.molecule.Molecule instance. Default None. - name - A name to give to the managed ParameterSet that adapts stru + 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 + 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 @@ -247,13 +276,13 @@ def setStructure(self, stru, name="phase", periodic=True): """ # Create the ParameterSet - parset = struToParameterSet(name, stru) + parset = struToParameterSet(name, structure) # Set the phase - self.setPhase(parset, periodic) + self.set_structure_from_parset(parset, periodic) return - def setPhase(self, parset, periodic=True): + 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, @@ -261,24 +290,24 @@ def setPhase(self, parset, periodic=True): object (from diffpy or pyobjcryst). The passed ParameterSet will be managed by this generator. - Attributes + Parameters ---------- - parset - A SrRealParSet that holds the structural information. + 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 + 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.stru = self._phase.stru + self.structure = self._phase.stru # Put this ParameterSet in the ProfileGenerator. - self.addParameterSet(parset) + self.add_parameter_set(parset) # Set periodicity self._phase.useSymmetry(periodic) @@ -321,7 +350,7 @@ def __call__(self, r): if not numpy.array_equal(r, self._lastr): self._prepare(r) - rcalc, y = self._calc(self._phase._getSrRealStructure()) + rcalc, y = self._calc(self._phase._get_srreal_structure()) if numpy.isnan(y).any(): y = numpy.zeros_like(r) diff --git a/src/diffpy/cmipdf/characteristicfunctions.py b/src/diffpy/cmipdf/characteristicfunctions.py index 8ad43b4..e5b73b5 100644 --- a/src/diffpy/cmipdf/characteristicfunctions.py +++ b/src/diffpy/cmipdf/characteristicfunctions.py @@ -22,17 +22,15 @@ function and Gcryst(f) is the crystal PDF. These functions are meant to be imported and added to a FitContribution -using the 'registerFunction' method of that class. +using the 'register_function' method of that class. """ __all__ = [ - "sphericalCF", - "spheroidalCF", - "spheroidalCF2", - "lognormalSphericalCF", - "sheetCF", - "shellCF", - "shellCF2", + "spherical_particle", + "spheroidal_particle", + "lognormal_spherical_distribution", + "sheet_particle", + "spherical_shell", "SASCF", ] @@ -46,86 +44,91 @@ from diffpy.srfit.fitbase.calculator import Calculator -def sphericalCF(r, psize): +def spherical_particle(radial_dist, p_diameter): """Spherical nanoparticle characteristic function. - Attributes + Parameters ---------- - r - distance of interaction - psize - The particle diameter + radial_dist : float or array-like + The distance of interaction. + p_diameter : float + The particle diameter. From Kodama et al., Acta Cryst. A, 62, 444-453 (converted from radius to diameter) """ - f = numpy.zeros(numpy.shape(r), dtype=float) - if psize > 0: - x = numpy.array(r, dtype=float) / psize + f = numpy.zeros(numpy.shape(radial_dist), dtype=float) + if p_diameter > 0: + x = numpy.array(radial_dist, dtype=float) / p_diameter inside = x < 1.0 xin = x[inside] f[inside] = 1.0 - 1.5 * xin + 0.5 * xin * xin * xin return f -def spheroidalCF(r, erad, prad): +def spheroidal_particle(radial_dist, r_equatorial, r_polar): """Spheroidal characteristic function specified using radii. - Spheroid with radii (erad, erad, prad) + Spheroid with radii (r_equatorial, r_equatorial, r_polar) - Attributes + Parameters ---------- - prad - polar radius - erad - equatorial radius - - - erad < prad equates to a prolate spheroid - erad > prad equates to a oblate spheroid - erad == prad is a sphere + radial_dist : float or array-like + The distance of interaction. + r_polar : float + The polar radius of the spheroid. + r_equatorial + The equatorial radius of the spheroid. + + + Note + ---- + - `r_equatorial < r_polar` equates to a prolate spheroid + - `r_equatorial > r_polar` equates to a oblate spheroid + - `r_equatorial == r_polar` is a sphere """ - psize = 2.0 * erad - pelpt = 1.0 * prad / erad - return spheroidalCF2(r, psize, pelpt) + d_equatorial = 2.0 * r_equatorial + pelpt = 1.0 * r_polar / r_equatorial + return _calculate_spheroidal_cf(radial_dist, d_equatorial, pelpt) -def spheroidalCF2(r, psize, axrat): - """Spheroidal nanoparticle characteristic function. +def _calculate_spheroidal_cf(r, d_equatorial, axis_ratio): + """Calculate the spheroidal nanoparticle characteristic function. - Form factor for ellipsoid with radii (psize/2, psize/2, axrat*psize/2) + Form factor for ellipsoid with radii + (d_equatorial/2, d_equatorial/2, axis_ratio*d_equatorial/2) - Attributes + Parameters ---------- - r - distance of interaction - psize + r : float or array-like + The distance of interaction + d_equatorial : float The equatorial diameter - axrat + axis_ratio : float The ratio of axis lengths From Lei et al., Phys. Rev. B, 80, 024118 (2009) """ - pelpt = 1.0 * axrat + pelpt = 1.0 * axis_ratio - if psize <= 0 or pelpt <= 0: + if d_equatorial <= 0 or pelpt <= 0: return numpy.zeros_like(r) # to simplify the equations v = pelpt - d = 1.0 * psize + d = 1.0 * d_equatorial d2 = d * d v2 = v * v if v == 1: - return sphericalCF(r, psize) + return spherical_particle(r, d_equatorial) rx = r if v < 1: - r = rx[rx <= v * psize] + r = rx[rx <= v * d_equatorial] r2 = r * r f1 = ( 1 @@ -139,7 +142,7 @@ def spheroidalCF2(r, psize, axrat): * atanh(sqrt(1 - v2)) ) - r = rx[numpy.logical_and(rx > v * psize, rx <= psize)] + r = rx[numpy.logical_and(rx > v * d_equatorial, rx <= d_equatorial)] r2 = r * r f2 = ( ( @@ -154,14 +157,14 @@ def spheroidalCF2(r, psize, axrat): / sqrt(1 - v2) ) - r = rx[rx > psize] + r = rx[rx > d_equatorial] f3 = numpy.zeros_like(r) f = numpy.concatenate((f1, f2, f3)) elif v > 1: - r = rx[rx <= psize] + r = rx[rx <= d_equatorial] r2 = r * r f1 = ( 1 @@ -175,7 +178,7 @@ def spheroidalCF2(r, psize, axrat): * atan(sqrt(v2 - 1)) ) - r = rx[numpy.logical_and(rx > psize, rx <= v * psize)] + r = rx[numpy.logical_and(rx > d_equatorial, rx <= v * d_equatorial)] r2 = r * r f2 = ( 1 @@ -195,7 +198,7 @@ def spheroidalCF2(r, psize, axrat): * (atan(sqrt(v2 - 1)) - atan(sqrt(r2 / d2 - 1))) ) - r = rx[rx > v * psize] + r = rx[rx > v * d_equatorial] f3 = numpy.zeros_like(r) f = numpy.concatenate((f1, f2, f3)) @@ -203,22 +206,24 @@ def spheroidalCF2(r, psize, axrat): return f -def lognormalSphericalCF(r, psize, psig): +def lognormal_spherical_distribution(radial_dist, p_diameter, p_sigma): """Spherical nanoparticle characteristic function with lognormal size distribution. - Attributes + Parameters ---------- - r - distance of interaction - psize - The mean particle diameter - psig - The log-normal width of the particle diameter - - - Here, r is the independent variable, mu is the mean of the distribution - (not of the particle size), and s is the width of the distribution. This is + radial_dist : float or array-like + The distance of interaction. + p_diameter : float + The mean particle diameter. + p_sigma : float + The log-normal width of the particle diameter. + + + Here, radial_dist is the independent variable, mu is the mean of the + distribution + (not of the particle size), and s is the width of the distribution. + This is the characteristic function for the lognormal distribution of particle diameter: @@ -227,94 +232,96 @@ def lognormalSphericalCF(r, psize, psig): - 0.75*r*Erfc((-mu-2*s^2+Log(r))/(sqrt(2)*s))*exp(-mu-2.5*s^2) The expectation value of the distribution gives the average particle - diameter, psize. The variance of the distribution gives psig^2. mu and s - can be expressed in terms of these as: + diameter, p_diameter. The variance of the distribution gives p_sigma^2. + mu and s can be expressed in terms of these as: - s^2 = log((psig/psize)^2 + 1) - mu = log(psize) - s^2/2 + s^2 = log((p_sigma/p_diameter)^2 + 1) + mu = log(p_diameter) - s^2/2 Source unknown """ - if psize <= 0: - return numpy.zeros_like(r) - if psig <= 0: - return sphericalCF(r, psize) + if p_diameter <= 0: + return numpy.zeros_like(radial_dist) + if p_sigma <= 0: + return spherical_particle(radial_dist, p_diameter) sqrt2 = sqrt(2.0) - s = sqrt(log(psig * psig / (1.0 * psize * psize) + 1)) - mu = log(psize) - s * s / 2 + s = sqrt(log(p_sigma * p_sigma / (1.0 * p_diameter * p_diameter) + 1)) + mu = log(p_diameter) - s * s / 2 if mu < 0: - return numpy.zeros_like(r) + return numpy.zeros_like(radial_dist) return ( - 0.5 * erfc((-mu - 3 * s * s + log(r)) / (sqrt2 * s)) + 0.5 * erfc((-mu - 3 * s * s + log(radial_dist)) / (sqrt2 * s)) + 0.25 - * r - * r - * r - * erfc((-mu + log(r)) / (sqrt2 * s)) + * radial_dist + * radial_dist + * radial_dist + * erfc((-mu + log(radial_dist)) / (sqrt2 * s)) * exp(-3 * mu - 4.5 * s * s) - 0.75 - * r - * erfc((-mu - 2 * s * s + log(r)) / (sqrt2 * s)) + * radial_dist + * erfc((-mu - 2 * s * s + log(radial_dist)) / (sqrt2 * s)) * exp(-mu - 2.5 * s * s) ) -def sheetCF(r, sthick): +def sheet_particle(r, thickness): """Nanosheet characteristic function. - Attributes + Parameters ---------- - r - distance of interaction - sthick - Thickness of nanosheet + r: float or array-like + The distance of interaction. + thickness : float + The thickness of nanosheet. From Kodama et al., Acta Cryst. A, 62, 444-453 """ - # handle zero or negative sthick. make it work for scalars and arrays. - if sthick <= 0: - return 0 * sthick + # handle zero or negative thickness. make it work for scalars and arrays. + if thickness <= 0: + return 0 * thickness # process scalar r if numpy.isscalar(r): - rv = 1 - 0.5 * r / sthick if r < sthick else 0.5 * sthick / r + rv = 1 - 0.5 * r / thickness if r < thickness else 0.5 * thickness / r return rv # handle array-type r ra = numpy.asarray(r) - lo = ra < sthick + lo = ra < thickness hi = ~lo f = numpy.empty_like(ra, dtype=float) - f[lo] = 1 - 0.5 * ra[lo] / sthick - f[hi] = 0.5 * sthick / ra[hi] + f[lo] = 1 - 0.5 * ra[lo] / thickness + f[hi] = 0.5 * thickness / ra[hi] return f -def shellCF(r, radius, thickness): +def spherical_shell(radial_dist, inner_radius, thickness): """Spherical shell characteristic function. - Attributes + Parameters ---------- - radius - Inner radius - thickness - Thickness of shell + radial_dist : float or array-like + The distance of interaction. + inner_radius : float + The inner radius of the shell. + thickness : float + The thickness of shell. - outer radius = radius + thickness + outer radius = inner_radius + thickness From Lei et al., Phys. Rev. B, 80, 024118 (2009) """ d = 1.0 * thickness - a = 1.0 * radius + d / 2.0 - return shellCF2(r, a, d) + a = 1.0 * inner_radius + d / 2.0 + return _calculate_shell_cf(radial_dist, a, d) -def shellCF2(r, a, delta): +def _calculate_shell_cf(r, a, delta): """Spherical shell characteristic function. - Attributes + Parameters ---------- a Central radius @@ -379,7 +386,7 @@ class SASCF(Calculator): def __init__(self, name, model): """Initialize the generator. - Attributes + Parameters ---------- name A name for the SASCF @@ -419,7 +426,8 @@ def __call__(self, r): # # The initial dr is somewhat arbitrary, but using dr = 0.01 allows for # the f(r) calculated from a particle of diameter 50, over r = - # arange(1, 60, 0.1) to agree with the sphericalCF with Rw < 1e-4%. + # arange(1, 60, 0.1) to agree with the spherical_particle with + # Rw < 1e-4%. # # We also have to make a q-spacing small enough to compute out to at # least the size of the signal. diff --git a/src/diffpy/cmipdf/debyepdfgenerator.py b/src/diffpy/cmipdf/debyepdfgenerator.py index e5cdf63..1825b5d 100644 --- a/src/diffpy/cmipdf/debyepdfgenerator.py +++ b/src/diffpy/cmipdf/debyepdfgenerator.py @@ -23,7 +23,9 @@ __all__ = ["DebyePDFGenerator"] + from diffpy.cmipdf.basepdfgenerator import BasePDFGenerator +from diffpy.srreal.pdfcalculator import DebyePDFCalculator class DebyePDFGenerator(BasePDFGenerator): @@ -39,7 +41,7 @@ class DebyePDFGenerator(BasePDFGenerator): DebyePDFCalculator instance for calculating the PDF _phase The structure ParameterSets used to calculate the profile. - stru + structure The structure objected adapted by _phase. _lastr The last value of r over which the PDF was calculated. This is @@ -66,13 +68,13 @@ class DebyePDFGenerator(BasePDFGenerator): --------------- stype The scattering type "X" for x-ray, "N" for neutron (see - 'setScatteringType'). + 'set_scattering_type'). qmax The maximum scattering vector used to generate the PDF (see - setQmax). + set_qmax). qmin The minimum scattering vector used to generate the PDF (see - setQmin). + set_qmin). scale See Managed Parameters. delta1 @@ -85,31 +87,32 @@ class DebyePDFGenerator(BasePDFGenerator): See Managed Parameters. """ - def setStructure(self, stru, name="phase", periodic=False): + def set_structure(self, structure, name="phase", periodic=False): """Set the structure that will be used to calculate the PDF. This creates a DiffpyStructureParSet, ObjCrystCrystalParSet or - ObjCrystMoleculeParSet that adapts stru to a ParameterSet interface. + 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. - Attributes + Parameters ---------- - stru - diffpy.structure.Structure, pyobjcryst.crystal.Crystal or - pyobjcryst.molecule.Molecule instance. Default None. - name - A name to give to the managed ParameterSet that adapts stru + structure : Structure object + The `diffpy.structure.Structure`, `pyobjcryst.crystal.Crystal` or + `pyobjcryst.molecule.Molecule` instance. + name : str, optional + A name to give to the managed ParameterSet that adapts structure (default "phase"). - periodic + periodic : bool, optional The structure should be treated as periodic (default False). Note that some structures do not support periodicity, in which case this will have no effect on the PDF calculation. """ - return BasePDFGenerator.setStructure(self, stru, name, periodic) + return BasePDFGenerator.set_structure(self, structure, name, periodic) - def setPhase(self, parset, periodic=False): + def set_structure_from_parset(self, parset, periodic=False): """Set the phase that will be used to calculate the PDF. Set the phase directly with a DiffpyStructureParSet, @@ -117,26 +120,26 @@ def setPhase(self, parset, periodic=False): object (from diffpy or pyobjcryst). The passed ParameterSet will be managed by this generator. - Attributes + Parameters ---------- - parset - A SrRealParSet that holds the structural information. + parset : SrealParSet object + 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 + 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. """ - return BasePDFGenerator.setPhase(self, parset, periodic) + return BasePDFGenerator.set_structure_from_parset( + self, parset, periodic + ) def __init__(self, name="pdf"): """Initialize the generator.""" - from diffpy.srreal.pdfcalculator import DebyePDFCalculator - BasePDFGenerator.__init__(self, name) - self._setCalculator(DebyePDFCalculator()) + self._set_calculator(DebyePDFCalculator()) return diff --git a/src/diffpy/cmipdf/pdfcontribution.py b/src/diffpy/cmipdf/pdfcontribution.py index 6cb273b..3163107 100644 --- a/src/diffpy/cmipdf/pdfcontribution.py +++ b/src/diffpy/cmipdf/pdfcontribution.py @@ -21,7 +21,9 @@ __all__ = ["PDFContribution"] -from diffpy.srfit.fitbase import FitContribution, Profile +from diffpy.cmipdf.debyepdfgenerator import DebyePDFGenerator +from diffpy.cmipdf.pdfgenerator import PDFGenerator +from diffpy.srfit.fitbase import FitContribution, Profile, ProfileParser class PDFContribution(FitContribution): @@ -30,7 +32,7 @@ class PDFContribution(FitContribution): PDFContribution is a FitContribution that is customized for PDF fits. Data and phases can be added directly to the PDFContribution. Setup of constraints and restraints requires direct interaction with the generator - attributes (see setPhase). + attributes (see set_structure_from_parset). Attributes ---------- @@ -85,7 +87,7 @@ class PDFContribution(FitContribution): def __init__(self, name): """Create the PDFContribution. - Attributes + Parameters ---------- name The name of the contribution. @@ -94,7 +96,7 @@ def __init__(self, name): self._meta = {} # Add the profile profile = Profile() - self.setProfile(profile, xname="r") + self.set_profile(profile, xname="r") # Need a parameter for the overall scale, in the case that this is a # multi-phase fit. @@ -106,34 +108,22 @@ def __init__(self, name): # Data methods - def loadData(self, data): - """Load the data in various formats. - - This uses the PDFParser to load the data and then passes it to the - built-in profile with loadParsedData. + def load_data(self, datafile): + """Load the data from a datafile. - Attributes + Parameters ---------- - data - An open file-like object, name of a file that contains data - or a string containing the data. + data : str or Path + The path to the data file. """ - # Get the data into a string - from diffpy.srfit.util.inpututils import inputToString - - datstr = inputToString(data) - - # Load data with a PDFParser - from diffpy.srfit.pdf.pdfparser import PDFParser - - parser = PDFParser() - parser.parseString(datstr) + parser = ProfileParser() + parser.parse_file(datafile) # Pass it to the profile - self.profile.loadParsedData(parser) + self.profile.load_parsed_data(parser) return - def setCalculationRange(self, xmin=None, xmax=None, dx=None): + def set_calculation_range(self, xmin=None, xmax=None, dx=None): """Set epsilon-inclusive calculation range. Adhere to the observed ``xobs`` points when ``dx`` is the same @@ -165,7 +155,7 @@ def setCalculationRange(self, xmin=None, xmax=None, dx=None): ValueError When xmin > xmax or if dx <= 0. Also if dx > xmax - xmin. """ - return self.profile.setCalculationRange(xmin, xmax, dx) + return self.profile.set_calculation_range(xmin, xmax, dx) def savetxt(self, fname, **kwargs): """Call numpy.savetxt with x, ycalc, y, dy. @@ -178,24 +168,24 @@ def savetxt(self, fname, **kwargs): # Phase methods - def addStructure(self, name, stru, periodic=True): + def add_structure(self, structure, name="phase", periodic=True): """Add a phase that goes into the PDF calculation. - Attributes + Parameters ---------- - name + structure : Structure object + `diffpy.structure.Structure`, `pyobjcryst.crystal.Crystal` or + `pyobjcryst.molecule.Molecule` instance. + name : str, optional A name to give the generator that will manage the PDF calculation from the passed structure. The adapted structure will be accessible via the name "phase" as an attribute of the generator, e.g. contribution.name.phase, where 'contribution' is this contribution and 'name' is passed name. - (default), then the name will be set as "phase". - stru - diffpy.structure.Structure, pyobjcryst.crystal.Crystal or - pyobjcryst.molecule.Molecule instance. Default None. - periodic - The structure should be treated as periodic. If this is + Default is `"phase"`. + periodic : bool, optional + The structure should be treated as periodic. If this is True (default), then a PDFGenerator will be used to calculate the PDF from the phase. Otherwise, a DebyePDFGenerator will be used. Note that some structures @@ -203,43 +193,42 @@ def addStructure(self, name, stru, periodic=True): ignored. - Returns the new phase (ParameterSet appropriate for what was passed in - stru.) + Returns + ------- + The new phase (ParameterSet appropriate for what was passed in + structure.) """ # Based on periodic, create the proper generator. if periodic: - from diffpy.srfit.pdf.pdfgenerator import PDFGenerator - gen = PDFGenerator(name) else: - from diffpy.srfit.pdf.debyepdfgenerator import DebyePDFGenerator - gen = DebyePDFGenerator(name) # Set up the generator - gen.setStructure(stru, "phase", periodic) - self._setupGenerator(gen) + gen.set_structure(structure, "phase", periodic) + self._setup_generator(gen) return gen.phase - def addPhase(self, name, parset, periodic=True): - """Add a phase that goes into the PDF calculation. + def add_structure_from_parset(self, parset, name, periodic=True): + """Add a phase that goes into the PDF calculation from a + ParameterSet. - Attributes + Parameters ---------- - name - A name to give the generator that will manage the PDF + parset : SrealParSet object + A 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. + name : str + The name to give the generator that will manage the PDF calculation from the passed parameter phase. The parset will be accessible via the name "phase" as an attribute of the generator, e.g., contribution.name.phase, where 'contribution' is this contribution and 'name' is passed name. - parset - A 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 + periodic : bool, optional The structure should be treated as periodic. If this is True (default), then a PDFGenerator will be used to calculate the PDF from the phase. Otherwise, a @@ -247,54 +236,49 @@ def addPhase(self, name, parset, periodic=True): do not support periodicity, in which case this may be ignored. - - Returns the new phase (ParameterSet appropriate for what was passed in - stru.) + Returns + ------- + The new phase (ParameterSet appropriate for what was passed in + parset.) """ # Based on periodic, create the proper generator. if periodic: - from diffpy.srfit.pdf.pdfgenerator import PDFGenerator - gen = PDFGenerator(name) else: - from diffpy.srfit.pdf.debyepdfgenerator import DebyePDFGenerator - gen = DebyePDFGenerator(name) - # Set up the generator - gen.setPhase(parset, periodic) - self._setupGenerator(gen) - + gen.set_structure_from_parset(parset, periodic) + self._setup_generator(gen) return gen.phase - def _setupGenerator(self, gen): + def _setup_generator(self, gen): """Setup a generator. The generator must already have a managed SrRealParSet, added - with setStructure or setPhase. + with set_structure or set_structure_from_parset. """ # Add the generator to this FitContribution - self.addProfileGenerator(gen) + self.add_profile_generator(gen) # Set the proper equation for the fit, depending on the number of # phases we have. gnames = self._generators.keys() eqstr = " + ".join(gnames) eqstr = "scale * (%s)" % eqstr - self.setEquation(eqstr) + self.set_equation(eqstr) # Update with our metadata gen.meta.update(self._meta) - gen.processMetaData() + gen._process_metadata() # Constrain the shared parameters - self.constrain(gen.qdamp, self.qdamp) - self.constrain(gen.qbroad, self.qbroad) + self.add_constraint(gen.qdamp, self.qdamp) + self.add_constraint(gen.qbroad, self.qbroad) return # Calculation setup methods - def _getMetaValue(self, kwd): + def _get_meta_value(self, kwd): """Get metadata according to object hierarchy.""" # Check self, then generators then profile if kwd in self._meta: @@ -305,10 +289,10 @@ def _getMetaValue(self, kwd): val = self.profile.meta.get(kwd) return val - def setScatteringType(self, type="X"): + def set_scattering_type(self, type="X"): """Set the scattering type. - Attributes + Parameters ---------- type "X" for x-ray or "N" for neutron @@ -317,37 +301,37 @@ def setScatteringType(self, type="X"): """ self._meta["stype"] = type for gen in self._generators.values(): - gen.setScatteringType(type) + gen.set_scattering_type(type) return - def getScatteringType(self): + def get_scattering_type(self): """Get the scattering type. - See 'setScatteringType'. + See 'set_scattering_type'. """ - return self._getMetaValue("stype") + return self._get_meta_value("stype") - def setQmax(self, qmax): + def set_qmax(self, qmax): """Set the qmax value.""" self._meta["qmax"] = qmax for gen in self._generators.values(): - gen.setQmax(qmax) + gen.set_qmax(qmax) return - def getQmax(self): + def get_qmax(self): """Get the qmax value.""" - return self._getMetaValue("qmax") + return self._get_meta_value("qmax") - def setQmin(self, qmin): + def set_qmin(self, qmin): """Set the qmin value.""" self._meta["qmin"] = qmin for gen in self._generators.values(): - gen.setQmin(qmin) + gen.set_qmin(qmin) return - def getQmin(self): + def get_qmin(self): """Get the qmin value.""" - return self._getMetaValue("qmin") + return self._get_meta_value("qmin") # End of file diff --git a/src/diffpy/cmipdf/pdfgenerator.py b/src/diffpy/cmipdf/pdfgenerator.py index eb9054b..9f71c97 100644 --- a/src/diffpy/cmipdf/pdfgenerator.py +++ b/src/diffpy/cmipdf/pdfgenerator.py @@ -25,7 +25,9 @@ __all__ = ["PDFGenerator"] + from diffpy.cmipdf.basepdfgenerator import BasePDFGenerator +from diffpy.srreal.pdfcalculator import PDFCalculator class PDFGenerator(BasePDFGenerator): @@ -65,13 +67,13 @@ class PDFGenerator(BasePDFGenerator): --------------- stype The scattering type "X" for x-ray, "N" for neutron (see - 'setScatteringType'). + 'set_scattering_type'). qmax The maximum scattering vector used to generate the PDF (see - setQmax). + set_qmax). qmin The minimum scattering vector used to generate the PDF (see - setQmin). + set_qmin). scale See Managed Parameters. delta1 @@ -86,10 +88,8 @@ class PDFGenerator(BasePDFGenerator): def __init__(self, name="pdf"): """Initialize the generator.""" - from diffpy.srreal.pdfcalculator import PDFCalculator - BasePDFGenerator.__init__(self, name) - self._setCalculator(PDFCalculator()) + self._set_calculator(PDFCalculator()) return diff --git a/src/diffpy/cmipdf/pdfparser.py b/src/diffpy/cmipdf/pdfparser.py deleted file mode 100644 index e1eceff..0000000 --- a/src/diffpy/cmipdf/pdfparser.py +++ /dev/null @@ -1,260 +0,0 @@ -#!/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. -# -############################################################################## -"""This module contains parsers for PDF data. - -PDFParser is suitable for parsing data generated from PDFGetN and -PDFGetX. - -See the class documentation for more information. -""" - -__all__ = ["PDFParser"] - -import re - -import numpy - -from diffpy.srfit.exceptions import ParseError -from diffpy.srfit.fitbase.profileparser import ProfileParser - - -class PDFParser(ProfileParser): - """Class for holding a diffraction pattern. - - Attributes - - Attributes - ---------- - _format - Name of the data format that this parses (string, default - ""). The format string is a unique identifier for the data - format handled by the parser. - _banks - The data from each bank. Each bank contains a - (x, y, dx, dy) tuple: - x - A numpy array containing the independent - variable read from the file. - y - A numpy array containing the profile - from the file. - dx - A numpy array containing the uncertainty in x - read from the file. This is 0 if the - uncertainty cannot be read. - dy - A numpy array containing the uncertainty read - from the file. This is 0 if the uncertainty - cannot be read. - _x - Independent variable from the chosen bank - _y - Profile from the chosen bank - _dx - Uncertainty in independent variable from the chosen bank - _dy - Uncertainty in profile from the chosen bank - _meta - A dictionary containing metadata read from the file. - - General Metadata - - Attributes - ---------- - filename - The name of the file from which data was parsed. This key - will not exist if data was not read from file. - nbanks - The number of banks parsed. - bank - The chosen bank number. - - Metadata - ---------- - stype - The scattering type ("X", "N") - qmin - Minimum scattering vector (float) - qmax - Maximum scattering vector (float) - qdamp - Resolution damping factor (float) - qbroad - Resolution broadening factor (float) - spdiameter - Nanoparticle diameter (float) - scale - Data scale (float) - temperature - Temperature (float) - doping - Doping (float) - - - These may appear in the metadata dictionary. - """ - - _format = "PDF" - - def parseString(self, patstring): - """Parse a string and set the _x, _y, _dx, _dy and _meta - variables. - - When _dx or _dy cannot be obtained in the data format it is set to 0. - - This wipes out the currently loaded data and selected bank number. - - Parameters - ---------- - patstring - A string containing the pattern - - Raises ParseError if the string cannot be parsed - """ - # useful regex patterns: - rx = {"f": r"[-+]?(\d+(\.\d*)?|\d*\.\d+)([eE][-+]?\d+)?"} - # find where does the data start - res = re.search(r"^#+ start data\s*(?:#.*\s+)*", patstring, re.M) - # start_data is position where the first data line starts - if res: - start_data = res.end() - else: - # find line that starts with a floating point number - regexp = r"^\s*%(f)s" % rx - res = re.search(regexp, patstring, re.M) - if res: - start_data = res.start() - else: - start_data = 0 - header = patstring[:start_data] - databody = patstring[start_data:].strip() - - # find where the metadata starts - metadata = "" - res = re.search(r"^#+\ +metadata\b\n", header, re.M) - if res: - metadata = header[res.end() :] - header = header[: res.start()] - - # parse header - meta = self._meta - # stype - if re.search("(x-?ray|PDFgetX)", header, re.I): - meta["stype"] = "X" - elif re.search("(neutron|PDFgetN)", header, re.I): - meta["stype"] = "N" - # qmin - regexp = r"\bqmin *= *(%(f)s)\b" % rx - res = re.search(regexp, header, re.I) - if res: - meta["qmin"] = float(res.groups()[0]) - # qmax - regexp = r"\bqmax *= *(%(f)s)\b" % rx - res = re.search(regexp, header, re.I) - if res: - meta["qmax"] = float(res.groups()[0]) - # qdamp - regexp = r"\b(?:qdamp|qsig) *= *(%(f)s)\b" % rx - res = re.search(regexp, header, re.I) - if res: - meta["qdamp"] = float(res.groups()[0]) - # qbroad - regexp = r"\b(?:qbroad|qalp) *= *(%(f)s)\b" % rx - res = re.search(regexp, header, re.I) - if res: - meta["qbroad"] = float(res.groups()[0]) - # spdiameter - regexp = r"\bspdiameter *= *(%(f)s)\b" % rx - res = re.search(regexp, header, re.I) - if res: - meta["spdiameter"] = float(res.groups()[0]) - # dscale - regexp = r"\bdscale *= *(%(f)s)\b" % rx - res = re.search(regexp, header, re.I) - if res: - meta["scale"] = float(res.groups()[0]) - # temperature - regexp = r"\b(?:temp|temperature|T)\ *=\ *(%(f)s)\b" % rx - res = re.search(regexp, header) - if res: - meta["temperature"] = float(res.groups()[0]) - # doping - regexp = r"\b(?:x|doping)\ *=\ *(%(f)s)\b" % rx - res = re.search(regexp, header) - if res: - meta["doping"] = float(res.groups()[0]) - - # parsing general metadata - if metadata: - regexp = r"\b(\w+)\ *=\ *(%(f)s)\b" % rx - while True: - res = re.search(regexp, metadata, re.M) - if res: - meta[res.groups()[0]] = float(res.groups()[1]) - metadata = metadata[res.end() :] - else: - break - - # read actual data - robs, Gobs, drobs, dGobs - inf_or_nan = re.compile("(?i)^[+-]?(NaN|Inf)\\b") - has_drobs = True - has_dGobs = True - # raise ParseError if something goes wrong - robs = [] - Gobs = [] - drobs = [] - dGobs = [] - try: - for line in databody.split("\n"): - v = line.split() - # there should be at least 2 value in the line - robs.append(float(v[0])) - Gobs.append(float(v[1])) - # drobs is valid if all values are defined and positive - has_drobs = ( - has_drobs and len(v) > 2 and not inf_or_nan.match(v[2]) - ) - if has_drobs: - v2 = float(v[2]) - has_drobs = v2 > 0.0 - drobs.append(v2) - # dGobs is valid if all values are defined and positive - has_dGobs = ( - has_dGobs and len(v) > 3 and not inf_or_nan.match(v[3]) - ) - if has_dGobs: - v3 = float(v[3]) - has_dGobs = v3 > 0.0 - dGobs.append(v3) - except (ValueError, IndexError) as err: - raise ParseError(err) - if has_drobs: - drobs = numpy.asarray(drobs) - else: - drobs = None - if has_dGobs: - dGobs = numpy.asarray(dGobs) - else: - dGobs = None - - robs = numpy.asarray(robs) - Gobs = numpy.asarray(Gobs) - - self._banks.append([robs, Gobs, drobs, dGobs]) - return - - -# End of PDFParser diff --git a/tests/test_generators.py b/tests/test_generators.py new file mode 100644 index 0000000..b440992 --- /dev/null +++ b/tests/test_generators.py @@ -0,0 +1,67 @@ +import numpy as np +import pytest + +from diffpy.cmipdf import PDFGenerator +from diffpy.srreal.pdfcalculator import PDFCalculator +from diffpy.structure import PDFFitStructure + + +def testGenerator(datafile): + qmax = 27.0 + gen = PDFGenerator() + gen.set_scattering_type("N") + assert "N" == gen.get_scattering_type() + gen.set_qmax(qmax) + assert qmax == pytest.approx(gen.get_qmax()) + + structure = PDFFitStructure() + ciffile = datafile("ni.cif") + cif_path = str(ciffile) + structure.read(cif_path) + for i in range(4): + structure[i].Bisoequiv = 1 + gen.set_structure(structure) + + calc = gen._calc + # Test parameters + for par in gen.iterate_over_parameters(recurse=False): + pname = par.name + defval = calc._getDoubleAttr(pname) + assert defval == par.getValue() + # Test setting values + par.set_value(1.0) + assert 1.0 == par.getValue() + par.set_value(defval) + assert defval == par.getValue() + + r = np.arange(0, 10, 0.1) + y = gen(r) + + # Now create a reference PDF. Since the calculator is testing its + # output, we just have to make sure we can calculate from the + # PDFGenerator interface. + + calc = PDFCalculator() + calc.rstep = r[1] - r[0] + calc.rmin = r[0] + calc.rmax = r[-1] + 0.5 * calc.rstep + calc.qmax = qmax + calc.setScatteringFactorTableByType("N") + calc.eval(structure) + yref = calc.pdf + + diff = y - yref + res = np.dot(diff, diff) + assert 0 == pytest.approx(res) + return + + +def test_set_qmin(): + """Verify qmin is propagated to the calculator object.""" + gen = PDFGenerator() + assert 0 == gen.get_qmin() + assert 0 == gen._calc.qmin + gen.set_qmin(0.93) + assert 0.93 == gen.get_qmin() + assert 0.93 == gen._calc.qmin + return diff --git a/tests/test_parser.py b/tests/test_parser.py new file mode 100644 index 0000000..3c7952c --- /dev/null +++ b/tests/test_parser.py @@ -0,0 +1,139 @@ +#!/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. +# +############################################################################## +"""Tests for pdf package.""" + + +import numpy as np +import pytest + +from diffpy.srfit.fitbase import ProfileParser + +# ---------------------------------------------------------------------------- + + +# The tests in this file are for ProfileParser which belongs to diffpy.srfit, +# but since it is used here, we will test it here on test data with modern +# diffpy format. +def testParser1(datafile): + filename = datafile("ni-q27r100-neutron.gr") + parser = ProfileParser() + parser.parse_file(filename) + + meta = parser._meta + + assert str(filename) == meta["filename"] + assert 1 == meta["nbanks"] + assert "N" == meta["stype"] + assert 27 == meta["qmax"] + assert 300 == meta.get("temperature") + assert meta.get("qdamp") is None + assert meta.get("qbroad") is None + assert meta.get("spdiameter") is None + assert meta.get("scale") is None + assert meta.get("doping") is None + + x, y, dx, dy = parser.get_data() + assert dx.tolist() == len(x) * [0] + assert dy.tolist() == len(x) * [0] + + testx = np.linspace(0.01, 100, 10000) + diff = testx - x + res = np.dot(diff, diff) + assert 0 == pytest.approx(res) + + testy = np.array( + [ + 1.144, + 2.258, + 3.312, + 4.279, + 5.135, + 5.862, + 6.445, + 6.875, + 7.150, + 7.272, + ] + ) + diff = testy - y[:10] + res = np.dot(diff, diff) + assert 0 == pytest.approx(res) + + return + + +def testParser2(datafile): + data = datafile("si-q27r60-xray.gr") + parser = ProfileParser() + parser.parse_file(data) + + meta = parser._meta + + assert str(data) == meta["filename"] + assert 1 == meta["nbanks"] + assert "X" == meta["stype"] + assert 27 == meta["qmax"] + assert 300 == meta.get("temperature") + assert meta.get("qdamp") is None + assert meta.get("qbroad") is None + assert meta.get("spdiameter") is None + assert meta.get("scale") is None + assert meta.get("doping") is None + + x, y, dx, dy = parser.get_data() + testx = np.linspace(0.01, 60, 5999, endpoint=False) + diff = testx - x + res = np.dot(diff, diff) + assert 0 == pytest.approx(res) + + testy = np.array( + [ + 0.1105784, + 0.2199684, + 0.3270088, + 0.4305913, + 0.5296853, + 0.6233606, + 0.7108060, + 0.7913456, + 0.8644501, + 0.9297440, + ] + ) + diff = testy - y[:10] + res = np.dot(diff, diff) + assert 0 == pytest.approx(res) + + testdy = np.array( + [ + 0.001802192, + 0.003521449, + 0.005079115, + 0.006404892, + 0.007440527, + 0.008142955, + 0.008486813, + 0.008466340, + 0.008096858, + 0.007416456, + ] + ) + diff = testdy - dy[:10] + res = np.dot(diff, diff) + assert 0 == pytest.approx(res) + + assert dx.tolist() == [0] * len(dx) + return diff --git a/tests/test_pdf.py b/tests/test_pdf.py deleted file mode 100644 index b64071d..0000000 --- a/tests/test_pdf.py +++ /dev/null @@ -1,321 +0,0 @@ -#!/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. -# -############################################################################## -"""Tests for pdf package.""" - -import io -import pickle -import unittest -from itertools import chain - -import numpy -import pytest - -from diffpy.cmipdf import PDFContribution, PDFGenerator, PDFParser -from diffpy.srfit.exceptions import SrFitError - -# ---------------------------------------------------------------------------- - - -def testParser1(datafile): - data = datafile("ni-q27r100-neutron.gr") - parser = PDFParser() - parser.parseFile(data) - - meta = parser._meta - - assert data == meta["filename"] - assert 1 == meta["nbanks"] - assert "N" == meta["stype"] - assert 27 == meta["qmax"] - assert 300 == meta.get("temperature") - assert meta.get("qdamp") is None - assert meta.get("qbroad") is None - assert meta.get("spdiameter") is None - assert meta.get("scale") is None - assert meta.get("doping") is None - - x, y, dx, dy = parser.getData() - assert dx is None - assert dy is None - - testx = numpy.linspace(0.01, 100, 10000) - diff = testx - x - res = numpy.dot(diff, diff) - assert 0 == pytest.approx(res) - - testy = numpy.array( - [ - 1.144, - 2.258, - 3.312, - 4.279, - 5.135, - 5.862, - 6.445, - 6.875, - 7.150, - 7.272, - ] - ) - diff = testy - y[:10] - res = numpy.dot(diff, diff) - assert 0 == pytest.approx(res) - - return - - -def testParser2(datafile): - data = datafile("si-q27r60-xray.gr") - parser = PDFParser() - parser.parseFile(data) - - meta = parser._meta - - assert data == meta["filename"] - assert 1 == meta["nbanks"] - assert "X" == meta["stype"] - assert 27 == meta["qmax"] - assert 300 == meta.get("temperature") - assert meta.get("qdamp") is None - assert meta.get("qbroad") is None - assert meta.get("spdiameter") is None - assert meta.get("scale") is None - assert meta.get("doping") is None - - x, y, dx, dy = parser.getData() - testx = numpy.linspace(0.01, 60, 5999, endpoint=False) - diff = testx - x - res = numpy.dot(diff, diff) - assert 0 == pytest.approx(res) - - testy = numpy.array( - [ - 0.1105784, - 0.2199684, - 0.3270088, - 0.4305913, - 0.5296853, - 0.6233606, - 0.7108060, - 0.7913456, - 0.8644501, - 0.9297440, - ] - ) - diff = testy - y[:10] - res = numpy.dot(diff, diff) - assert 0 == pytest.approx(res) - - testdy = numpy.array( - [ - 0.001802192, - 0.003521449, - 0.005079115, - 0.006404892, - 0.007440527, - 0.008142955, - 0.008486813, - 0.008466340, - 0.008096858, - 0.007416456, - ] - ) - diff = testdy - dy[:10] - res = numpy.dot(diff, diff) - assert 0 == pytest.approx(res) - - assert dx is None - return - - -def testGenerator( - diffpy_srreal_available, diffpy_structure_available, datafile -): - if not diffpy_structure_available: - pytest.skip("diffpy.structure package not available") - if not diffpy_srreal_available: - pytest.skip("diffpy.srreal package not available") - - from diffpy.srreal.pdfcalculator import PDFCalculator - from diffpy.structure import PDFFitStructure - - qmax = 27.0 - gen = PDFGenerator() - gen.setScatteringType("N") - assert "N" == gen.getScatteringType() - gen.setQmax(qmax) - assert qmax == pytest.approx(gen.getQmax()) - - stru = PDFFitStructure() - ciffile = datafile("ni.cif") - cif_path = str(ciffile) - stru.read(cif_path) - for i in range(4): - stru[i].Bisoequiv = 1 - gen.setStructure(stru) - - calc = gen._calc - # Test parameters - for par in gen.iterPars(recurse=False): - pname = par.name - defval = calc._getDoubleAttr(pname) - assert defval == par.getValue() - # Test setting values - par.setValue(1.0) - assert 1.0 == par.getValue() - par.setValue(defval) - assert defval == par.getValue() - - r = numpy.arange(0, 10, 0.1) - y = gen(r) - - # Now create a reference PDF. Since the calculator is testing its - # output, we just have to make sure we can calculate from the - # PDFGenerator interface. - - calc = PDFCalculator() - calc.rstep = r[1] - r[0] - calc.rmin = r[0] - calc.rmax = r[-1] + 0.5 * calc.rstep - calc.qmax = qmax - calc.setScatteringFactorTableByType("N") - calc.eval(stru) - yref = calc.pdf - - diff = y - yref - res = numpy.dot(diff, diff) - assert 0 == pytest.approx(res) - return - - -def test_setQmin(diffpy_structure_available, diffpy_srreal_available): - """Verify qmin is propagated to the calculator object.""" - if not diffpy_srreal_available: - pytest.skip("diffpy.srreal package not available") - - gen = PDFGenerator() - assert 0 == gen.getQmin() - assert 0 == gen._calc.qmin - gen.setQmin(0.93) - assert 0.93 == gen.getQmin() - assert 0.93 == gen._calc.qmin - return - - -def test_setQmax(diffpy_structure_available, diffpy_srreal_available): - """Check PDFContribution.setQmax()""" - if not diffpy_structure_available: - pytest.skip("diffpy.structure package not available") - from diffpy.structure import Structure - - if not diffpy_srreal_available: - pytest.skip("diffpy.srreal package not available") - - pc = PDFContribution("pdf") - pc.setQmax(21) - pc.addStructure("empty", Structure()) - assert 21 == pc.empty.getQmax() - pc.setQmax(22) - assert 22 == pc.getQmax() - assert 22 == pc.empty.getQmax() - return - - -def test_getQmax(diffpy_structure_available, diffpy_srreal_available): - """Check PDFContribution.getQmax()""" - if not diffpy_structure_available: - pytest.skip("diffpy.structure package not available") - from diffpy.structure import Structure - - if not diffpy_srreal_available: - pytest.skip("diffpy.srreal package not available") - - # cover all code branches in PDFContribution._getMetaValue - # (1) contribution metadata - pc1 = PDFContribution("pdf") - assert pc1.getQmax() is None - pc1.setQmax(17) - assert 17 == pc1.getQmax() - # (2) contribution metadata - pc2 = PDFContribution("pdf") - pc2.addStructure("empty", Structure()) - pc2.empty.setQmax(18) - assert 18 == pc2.getQmax() - # (3) profile metadata - pc3 = PDFContribution("pdf") - pc3.profile.meta["qmax"] = 19 - assert 19 == pc3.getQmax() - return - - -def test_savetxt( - diffpy_structure_available, diffpy_srreal_available, datafile -): - "check PDFContribution.savetxt()" - if not diffpy_structure_available: - pytest.skip("diffpy.structure package not available") - from diffpy.structure import Structure - - if not diffpy_srreal_available: - pytest.skip("diffpy.srreal package not available") - - pc = PDFContribution("pdf") - pc.loadData(datafile("si-q27r60-xray.gr")) - pc.setCalculationRange(0, 10) - pc.addStructure("empty", Structure()) - fp = io.BytesIO() - with pytest.raises(SrFitError): - pc.savetxt(fp) - pc.evaluate() - pc.savetxt(fp) - txt = fp.getvalue().decode() - nlines = len(txt.strip().split("\n")) - assert 1001 == nlines - return - - -def test_pickling( - diffpy_structure_available, diffpy_srreal_available, datafile -): - "validate PDFContribution.residual() after pickling." - if not diffpy_structure_available: - pytest.skip("diffpy.structure package not available") - from diffpy.structure import loadStructure - - if not diffpy_srreal_available: - pytest.skip("diffpy.srreal package not available") - - pc = PDFContribution("pdf") - pc.loadData(datafile("ni-q27r100-neutron.gr")) - ciffile = datafile("ni.cif") - cif_path = str(ciffile) - ni = loadStructure(cif_path) - ni.Uisoequiv = 0.003 - pc.addStructure("ni", ni) - pc.setCalculationRange(0, 10) - pc2 = pickle.loads(pickle.dumps(pc)) - res0 = pc.residual() - assert numpy.array_equal(res0, pc2.residual()) - for p in chain(pc.iterPars("Uiso"), pc2.iterPars("Uiso")): - p.value = 0.004 - res1 = pc.residual() - assert not numpy.allclose(res0, res1) - assert numpy.array_equal(res1, pc2.residual()) - return - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_pdfcontribution.py b/tests/test_pdfcontribution.py new file mode 100644 index 0000000..4931d08 --- /dev/null +++ b/tests/test_pdfcontribution.py @@ -0,0 +1,83 @@ +import io +import pickle +from itertools import chain + +import numpy as np +import pytest + +from diffpy.cmipdf import PDFContribution +from diffpy.srfit.exceptions import SrFitError +from diffpy.structure import Structure, loadStructure + + +def test_set_qmax(): + """Check PDFContribution.setQmax()""" + pc = PDFContribution("pdf") + pc.set_qmax(21) + pc.add_structure(Structure(), name="empty") + assert 21 == pc.empty.get_qmax() + pc.set_qmax(22) + assert 22 == pc.get_qmax() + assert 22 == pc.empty.get_qmax() + return + + +def test_get_qmax(): + """Check PDFContribution.get_qmax()""" + # cover all code branches in PDFContribution._get_meta_value + # (1) contribution metadata + pc1 = PDFContribution("pdf") + assert pc1.get_qmax() is None + pc1.set_qmax(17) + assert 17 == pc1.get_qmax() + # (2) contribution metadata + pc2 = PDFContribution("pdf") + pc2.add_structure(Structure(), name="empty") + pc2.empty.set_qmax(18) + assert 18 == pc2.get_qmax() + # (3) profile metadata + pc3 = PDFContribution("pdf") + pc3.profile.meta["qmax"] = 19 + assert 19 == pc3.get_qmax() + return + + +def test_savetxt(datafile): + "check PDFContribution.savetxt()" + + pc = PDFContribution("pdf") + pc.load_data(datafile("si-q27r60-xray.gr")) + pc.set_calculation_range(0, 10) + pc.add_structure(Structure(), name="empty") + fp = io.BytesIO() + with pytest.raises(SrFitError): + pc.savetxt(fp) + pc.evaluate() + pc.savetxt(fp) + txt = fp.getvalue().decode() + nlines = len(txt.strip().split("\n")) + assert 1001 == nlines + return + + +def test_pickling(datafile): + "validate PDFContribution.residual() after pickling." + pc = PDFContribution("pdf") + pc.load_data(datafile("ni-q27r100-neutron.gr")) + ciffile = datafile("ni.cif") + cif_path = str(ciffile) + ni = loadStructure(cif_path) + ni.Uisoequiv = 0.003 + pc.add_structure(ni, name="ni") + pc.set_calculation_range(0, 10) + pc2 = pickle.loads(pickle.dumps(pc)) + res0 = pc.residual() + assert np.array_equal(res0, pc2.residual()) + for p in chain( + pc.iterate_over_parameters("Uiso"), pc2.iterate_over_parameters("Uiso") + ): + p.value = 0.004 + res1 = pc.residual() + assert not np.allclose(res0, res1) + assert np.array_equal(res1, pc2.residual()) + return diff --git a/tests/testdata/ni-q27r100-neutron.gr b/tests/testdata/ni-q27r100-neutron.gr index 1b18d9a..65dada7 100644 --- a/tests/testdata/ni-q27r100-neutron.gr +++ b/tests/testdata/ni-q27r100-neutron.gr @@ -1,54 +1,32 @@ -# History written: Tue May 6 11:04:33 2008 -# produced by bozin -# ##### Run Information runCorrection=T -# prep=gsas machine=npdf -# run=npdf_03315 background=npdf_03001 -# smooth=2 smoothParam=32 32 0 backKillThresh=-1.0 -# in beam: radius=0.45325 height=4.5 -# temp=300 runTitle=Run 3315: Ni commercial, RT_stick -# -# ##### Vanadium runCorrection=T -# run=npdf_03000 background=npdf_03001 -# smooth=2 smoothParam=32 32 0 vanKillThresh=-1.0 vBackKillThresh=-1.0 -# in beam: radius=0.47625 height=4.5 -# -# ##### Container runCorrection=T -# run=npdf_03002 background=npdf_03001 -# smooth=2 smoothParam=32 32 0 cBackKillThresh=-1.0 -# wallThick=0.023 atomDensity=0.072110 -# atomic information: scattCS=5.100 absorpCS=5.080 -# -# ##### Sample Material numElements=1 NormLaue=0.00000 -# Element relAtomNum atomMass atomCoherCS atomIncoherCS atomAbsorpCS -# Ni 1.0000 58.693 13.3000 5.2000 4.49000 -# density=7.0 effDensity=2.8777 -# -# ##### Banks=4 deltaQ=0.01 matchRef=0 matchScal=T matchOffset=T -# bank angle blendQmin blendQmax (0.0 means no info) -# 1 46.6 0.87 21.63 -# 2 90.0 1.51 37.66 -# 3 119.0 1.84 45.96 -# 4 148.0 2.05 51.25 -# -# ##### Program Specific Information -# ## Ft calcError=1 (1 for true, 0 for false) -# numRpoints=10000 maxR=100.0 numDensity=0.0 intMaxR=1.5 -# ## Damp Qmin=0.87 Qmax=27.0 startDampQ=27.0 QAveMin=0.6 -# dampFuncType=0 modEqn=1.0000*S(Q) +0.0000 +0.0000*Q dampExtraToZero=0 -# ## Blend numBanks=4 banks=1,2,3,4 -# soqCorrFile= -# ## Soqd minProcOut=0 -# samPlazcek=1 vanPlazcek=1 smoothData=0 modifyData=1 -# ## Corps minProcOut=0 numBanksMiss=0 -# -# ##### prepgsas prepOutput=1 numBanksMiss=0 fileExt=gsa -# instParamFile=npdf_TL-displex_2018.iparm -# numBanksAdd=0 -# numBanksMult=0 -##### start data -#O0 rg_int sig_rg_int low_int sig_low_int rmax rhofit -#S 1 - PDF from PDFgetN -#P0 -68.04163 47.30471 0.14884 0.13136 1.50 0.1091 +# xPDFsuite Configuration # + +#### NOTE: The metadata values are NOT actual metadata values from the data and are strictly used for testing +[PDF] +wavelength = 0.1 +dataformat = QA +inputfile = input.iq +backgroundfile = backgroundfile.iq +mode = neutron +bgscale = 1.0 +composition = TiSe2 +outputtype = gr +qmaxinst = 25.0 +qmin = 0.1 +qmax = 27.0 +rmax = 100.0 +rmin = 0.0 +rstep = 0.01 +rpoly = 0.7 +stype = N +temperature = 300 + +[Misc] +inputdir = /my/data/dir +savedir = /my/save/dir +backgroundfilefull = /my/data/dir/backgroundfile.iq + +#### start data +#S 1 #L r G(r) dr dG(r) 0.010 1.144 0.020 2.258 diff --git a/tests/testdata/si-q27r60-xray.gr b/tests/testdata/si-q27r60-xray.gr index e25d579..bbc9fd7 100644 --- a/tests/testdata/si-q27r60-xray.gr +++ b/tests/testdata/si-q27r60-xray.gr @@ -1,130 +1,30 @@ -History written: Mon Apr 21 20:48:28 2008 -Produced by -####### Get_XPDF ####### +# xPDFsuite Configuration # -##### General_Setting -title=X-ray PDF -workingdirectory=e:\Ahmad\MUCAT0804\standards\pdfgetx2 -sourcedir=C:\Program Files\PDFgetX2\ -logfile=.pdfgetx2.log -quiet=0 debug=0 autosave_isa=1 savefilenamebase=si325_mesh_300k_nor_4-8 -iqfilesurfix=.iq sqfilesurfix=.sq fqfilesurfix=.fq grfilesurfix=.gr +#### NOTE: The metadata values are NOT actual metadata values from the data and are strictly used for testing +[PDF] +wavelength = 0.1 +dataformat = QA +inputfile = input.iq +backgroundfile = backgroundfile.iq +mode = neutron +bgscale = 1.0 +composition = TiSe2 +outputtype = gr +qmaxinst = 25.0 +qmin = 0.1 +qmax = 27.0 +rmax = 60.0 +rmin = 0.0 +rstep = 0.01 +rpoly = 0.7 +stype = X +temperature = 300 -##### DataFileFormat -datatype=1 (0:SPEC, 1:CHI, 2:nxm column, 3:unknown) -num_skiplines=3 comment_id=# delimiter= -### SPEC Format scan_id=#S scan_delimiter= -columnname_id=#L columnname_delimiter= -data_id= data_delimiter= -### CHI Format -### nxm column Format -### End of file format +[Misc] +inputdir = /my/data/dir +savedir = /my/save/dir +backgroundfilefull = /my/data/dir/backgroundfile.iq -##### Data&Background -samfile=si325_mesh_300k_nor_4-8.chi num_sams=1 -sambkgfile=kapton_bgrd_300k_nor_2-3.chi num_sambkgs=1 -confile= num_cons=1 -conbkgfile= num_conbkgs=1 -det# used xcol detcol deterrcol xmin xmax add_det mul_det add_bkg mul_bkg add_con mul_con add_conbkg mul_conbkg - 0 1 0 1 3 0.600000 32.0000 0.000000 1.00000 0.000000 1.00000 0.000000 1.00000 0.000000 1.00000 - -##### Experiment_Setup -title=PDF analysis -user=me -facility=In house -temperature=300.000 containermut=0.000500000 filtermut=0.0200000 -## X-Ray radiationtype=3 - (0: Ag K_alpha, 1:Cu K_alpha, 2:Mo K_alpha, 3:Customize) -lambda=0.142773 energy=86.8406 polartype=0 polardegree=1.00000 -## MonoChromator crystaltype=0 (0:Perfect, 1:Mosaic, 2:None) -position=0 (0:Primary beam, 1:Diffracted beam) -dspacetype=0 (0:Si{111}, 1:Ge{111}, 2:Customize) dspacing=3.13200 - -##### Sample_Setup information num_atoms=1 -#L symbol valence fractions z user_f1 user_f2 user_macoef - Si 0.00 1.000000 14 0.000000 0.000000 0.001000 -geometry=2 mut=0.50000000 numberdensity=0.00600000 -thickness=2.00000 packingFraction=0.500000 theory_mut=0.00579218 - -##### GetIQ_Setup -xformat=1 -smoothcorr_isa=0 selfnormalize_isa=0 -#L par_name sample sample_bkg container container_bkg -smooth_degree 2 2 2 2 -smooth_width 6 6 6 6 -selfnormalize 0 0 0 0 -filtercorr_isa=0 samfiltercorr_isa=0 sambkgfiltercorr_isa=0 -confiltercorr_isa=0 conbkgfiltercorr_isa=0 -scatveffcorr_isa=1 samconveffcorr_isa=1 sambkgveffcorr_isa=0 -conbkgveffcorr_isa=0 -nonegative_isa=1 negativevalue=-1.00000 - -##### Calibration_Data -## Detection efficiency energy dependence detedepxaxis=0 -detedepfunctype=0 detedep_elastic=1.00000 detedep_fluores=1.80000 -detedep_quadra=0.000000 detedep_spline=0.000000 detedep_file= -## Detector transmission energy dependence dettcoefxaxis=0 -dettcoeffunctype=0 dettcoef_elastic=0.950000 dettcoef_fluores=0.600000 -dettcoef_quadra=0.000000 dettcoef_spline=0.000000 dettcoef_file= - -##### IQ_Simulation -### Elastic used_isa=1 mymethod=1 -do_samabsorp=1 do_multscat=1 do_conabsorp=0 do_airabsorp=0 -do_polarization=1 do_oblincident=0 do_energydep=0 -do_breitdirac=0 breitdiracexpo=2.00000 -do_rulandwin=0 rulandwinwidth=0.00100000 -do_useredit=0 add_user=0.000000 mul_user=1.00000 -### Compton used_isa=1 mymethod=1 -do_samabsorp=1 do_multscat=1 do_conabsorp=0 do_airabsorp=0 -do_polarization=1 do_oblincident=0 do_energydep=0 -do_breitdirac=0 breitdiracexpo=2.00000 -do_rulandwin=0 rulandwinwidth=0.00100000 -do_useredit=0 add_user=0.000000 mul_user=1.00000 -### Fluores used_isa=1 mymethod=1 -do_samabsorp=1 do_multscat=1 do_conabsorp=0 do_airabsorp=0 -do_polarization=1 do_oblincident=0 do_energydep=0 -do_breitdirac=0 breitdiracexpo=2.00000 -do_rulandwin=0 rulandwinwidth=0.00100000 -do_useredit=0 add_user=0.000000 mul_user=1.00000 - -##### Correction_Setup corrmethod=0 -oblincident_isa=1 dettranscoef=0.980000 samfluore_isa=1 -samfluoretype=0 samfluorescale=15.000000 -multiscat_isa=1 xraypolar_isa=1 samabsorp_isa=1 -highqscale_isa=1 highqratio=0.600000 scaleconst=0.039181914 -scaleconst_theory=0.039181914 -comptonscat_isa=1 rulandwin_isa=0 rulandintewidth=0.0100000 -comptonmethod=0 breitdirac_isa=1 breitdiracexponent=3 -detefficiency_isa=1 detefficiencytype=2 (0-1: linear, 2-3: quadratic) -detefficiency_a=-0.054826792 detefficiency_b=0.028062565 -lauediffuse_isa=1 -weight_isa=1 weighttype=0 (0: ^2, 1: , 2: Data Smoothed) -weightsmoothrmin=3.00000 weightsmoothwidth=100 weightsmoothcycles=600 -editsq_isa=0 editsqtype=0 add_sq=0.000000 mul_sq=1.00000 -editsqsmoothrmin=3.00000 editsqsmoothwidth=100 editsqsmoothcycles=600 -smoothdata_isa=0 smoothfunctype=0 smoothqmin=12.0000 smoothboxwidth=9 -interpolateqmin_isa=0 qmininterpolationtype=0 -dampfq_isa=0 dampfqtype=0 dampfqwidth=23.0000 - -##### SqGr_Optimization Setup -ftmethod=0 -## S(q) qmin=0.010000 qmax=27.000000 qgrid=0.000000 -## G(r) rmin=0.010000 rmax=60.000000 rgrid=0.010000 -## SqOptimization sqoptfunction=1 -optqmin=15.0000 optqmax=40.0000 optqgrid=0.000000 -optrmin=0.000000 optrmax=2.20000 optrgrid=0.0200000 -maxiter=20 relstep=0.000000 weighttype=0 weightfunc=0 -fitbkgmult_isa=0 fitsampmut_isa=1 fitpolariz_isa=1 -fitoblique_isa=0 fitfluores_isa=0 -fitrulandw_isa=0 fitenergya_isa=1 fitenergyb_isa=1 -fitsimurulandw_isa=1 fitDetEdepfluores_isa=0 fitDetEdepquadra_isa=0 -fitDetEdepspline_isa=0 fitDetTCoefElastic_isa=0 fitDetTCoefFluores_isa=0 -fitDetTcoefquadra_isa=0 fitDetTcoefspline_isa=0 - -##### Save&Plot Settings -datatype=GrData iqcorrtype=Int iqsimutype=SimuIq -sqcorrtype=Oblin sqtofqtype=DampFq -gropttype=OptFq miscdatatype=AtomASF ##### start data #F si325_mesh_300k_nor_4-8.gr #D Mon Apr 21 21:17:23 2008