|
| 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 | +"""Tests for pdf package.""" |
| 17 | + |
| 18 | +import io |
| 19 | +import pickle |
| 20 | +import unittest |
| 21 | +from itertools import chain |
| 22 | + |
| 23 | +import numpy |
| 24 | +import pytest |
| 25 | + |
| 26 | +from diffpy.cmipdf import PDFContribution, PDFGenerator, PDFParser |
| 27 | +from diffpy.srfit.exceptions import SrFitError |
| 28 | + |
| 29 | +# ---------------------------------------------------------------------------- |
| 30 | + |
| 31 | + |
| 32 | +def testParser1(datafile): |
| 33 | + data = datafile("ni-q27r100-neutron.gr") |
| 34 | + parser = PDFParser() |
| 35 | + parser.parseFile(data) |
| 36 | + |
| 37 | + meta = parser._meta |
| 38 | + |
| 39 | + assert data == meta["filename"] |
| 40 | + assert 1 == meta["nbanks"] |
| 41 | + assert "N" == meta["stype"] |
| 42 | + assert 27 == meta["qmax"] |
| 43 | + assert 300 == meta.get("temperature") |
| 44 | + assert meta.get("qdamp") is None |
| 45 | + assert meta.get("qbroad") is None |
| 46 | + assert meta.get("spdiameter") is None |
| 47 | + assert meta.get("scale") is None |
| 48 | + assert meta.get("doping") is None |
| 49 | + |
| 50 | + x, y, dx, dy = parser.getData() |
| 51 | + assert dx is None |
| 52 | + assert dy is None |
| 53 | + |
| 54 | + testx = numpy.linspace(0.01, 100, 10000) |
| 55 | + diff = testx - x |
| 56 | + res = numpy.dot(diff, diff) |
| 57 | + assert 0 == pytest.approx(res) |
| 58 | + |
| 59 | + testy = numpy.array( |
| 60 | + [ |
| 61 | + 1.144, |
| 62 | + 2.258, |
| 63 | + 3.312, |
| 64 | + 4.279, |
| 65 | + 5.135, |
| 66 | + 5.862, |
| 67 | + 6.445, |
| 68 | + 6.875, |
| 69 | + 7.150, |
| 70 | + 7.272, |
| 71 | + ] |
| 72 | + ) |
| 73 | + diff = testy - y[:10] |
| 74 | + res = numpy.dot(diff, diff) |
| 75 | + assert 0 == pytest.approx(res) |
| 76 | + |
| 77 | + return |
| 78 | + |
| 79 | + |
| 80 | +def testParser2(datafile): |
| 81 | + data = datafile("si-q27r60-xray.gr") |
| 82 | + parser = PDFParser() |
| 83 | + parser.parseFile(data) |
| 84 | + |
| 85 | + meta = parser._meta |
| 86 | + |
| 87 | + assert data == meta["filename"] |
| 88 | + assert 1 == meta["nbanks"] |
| 89 | + assert "X" == meta["stype"] |
| 90 | + assert 27 == meta["qmax"] |
| 91 | + assert 300 == meta.get("temperature") |
| 92 | + assert meta.get("qdamp") is None |
| 93 | + assert meta.get("qbroad") is None |
| 94 | + assert meta.get("spdiameter") is None |
| 95 | + assert meta.get("scale") is None |
| 96 | + assert meta.get("doping") is None |
| 97 | + |
| 98 | + x, y, dx, dy = parser.getData() |
| 99 | + testx = numpy.linspace(0.01, 60, 5999, endpoint=False) |
| 100 | + diff = testx - x |
| 101 | + res = numpy.dot(diff, diff) |
| 102 | + assert 0 == pytest.approx(res) |
| 103 | + |
| 104 | + testy = numpy.array( |
| 105 | + [ |
| 106 | + 0.1105784, |
| 107 | + 0.2199684, |
| 108 | + 0.3270088, |
| 109 | + 0.4305913, |
| 110 | + 0.5296853, |
| 111 | + 0.6233606, |
| 112 | + 0.7108060, |
| 113 | + 0.7913456, |
| 114 | + 0.8644501, |
| 115 | + 0.9297440, |
| 116 | + ] |
| 117 | + ) |
| 118 | + diff = testy - y[:10] |
| 119 | + res = numpy.dot(diff, diff) |
| 120 | + assert 0 == pytest.approx(res) |
| 121 | + |
| 122 | + testdy = numpy.array( |
| 123 | + [ |
| 124 | + 0.001802192, |
| 125 | + 0.003521449, |
| 126 | + 0.005079115, |
| 127 | + 0.006404892, |
| 128 | + 0.007440527, |
| 129 | + 0.008142955, |
| 130 | + 0.008486813, |
| 131 | + 0.008466340, |
| 132 | + 0.008096858, |
| 133 | + 0.007416456, |
| 134 | + ] |
| 135 | + ) |
| 136 | + diff = testdy - dy[:10] |
| 137 | + res = numpy.dot(diff, diff) |
| 138 | + assert 0 == pytest.approx(res) |
| 139 | + |
| 140 | + assert dx is None |
| 141 | + return |
| 142 | + |
| 143 | + |
| 144 | +def testGenerator( |
| 145 | + diffpy_srreal_available, diffpy_structure_available, datafile |
| 146 | +): |
| 147 | + if not diffpy_structure_available: |
| 148 | + pytest.skip("diffpy.structure package not available") |
| 149 | + if not diffpy_srreal_available: |
| 150 | + pytest.skip("diffpy.srreal package not available") |
| 151 | + |
| 152 | + from diffpy.srreal.pdfcalculator import PDFCalculator |
| 153 | + from diffpy.structure import PDFFitStructure |
| 154 | + |
| 155 | + qmax = 27.0 |
| 156 | + gen = PDFGenerator() |
| 157 | + gen.setScatteringType("N") |
| 158 | + assert "N" == gen.getScatteringType() |
| 159 | + gen.setQmax(qmax) |
| 160 | + assert qmax == pytest.approx(gen.getQmax()) |
| 161 | + |
| 162 | + stru = PDFFitStructure() |
| 163 | + ciffile = datafile("ni.cif") |
| 164 | + cif_path = str(ciffile) |
| 165 | + stru.read(cif_path) |
| 166 | + for i in range(4): |
| 167 | + stru[i].Bisoequiv = 1 |
| 168 | + gen.setStructure(stru) |
| 169 | + |
| 170 | + calc = gen._calc |
| 171 | + # Test parameters |
| 172 | + for par in gen.iterPars(recurse=False): |
| 173 | + pname = par.name |
| 174 | + defval = calc._getDoubleAttr(pname) |
| 175 | + assert defval == par.getValue() |
| 176 | + # Test setting values |
| 177 | + par.setValue(1.0) |
| 178 | + assert 1.0 == par.getValue() |
| 179 | + par.setValue(defval) |
| 180 | + assert defval == par.getValue() |
| 181 | + |
| 182 | + r = numpy.arange(0, 10, 0.1) |
| 183 | + y = gen(r) |
| 184 | + |
| 185 | + # Now create a reference PDF. Since the calculator is testing its |
| 186 | + # output, we just have to make sure we can calculate from the |
| 187 | + # PDFGenerator interface. |
| 188 | + |
| 189 | + calc = PDFCalculator() |
| 190 | + calc.rstep = r[1] - r[0] |
| 191 | + calc.rmin = r[0] |
| 192 | + calc.rmax = r[-1] + 0.5 * calc.rstep |
| 193 | + calc.qmax = qmax |
| 194 | + calc.setScatteringFactorTableByType("N") |
| 195 | + calc.eval(stru) |
| 196 | + yref = calc.pdf |
| 197 | + |
| 198 | + diff = y - yref |
| 199 | + res = numpy.dot(diff, diff) |
| 200 | + assert 0 == pytest.approx(res) |
| 201 | + return |
| 202 | + |
| 203 | + |
| 204 | +def test_setQmin(diffpy_structure_available, diffpy_srreal_available): |
| 205 | + """Verify qmin is propagated to the calculator object.""" |
| 206 | + if not diffpy_srreal_available: |
| 207 | + pytest.skip("diffpy.srreal package not available") |
| 208 | + |
| 209 | + gen = PDFGenerator() |
| 210 | + assert 0 == gen.getQmin() |
| 211 | + assert 0 == gen._calc.qmin |
| 212 | + gen.setQmin(0.93) |
| 213 | + assert 0.93 == gen.getQmin() |
| 214 | + assert 0.93 == gen._calc.qmin |
| 215 | + return |
| 216 | + |
| 217 | + |
| 218 | +def test_setQmax(diffpy_structure_available, diffpy_srreal_available): |
| 219 | + """Check PDFContribution.setQmax()""" |
| 220 | + if not diffpy_structure_available: |
| 221 | + pytest.skip("diffpy.structure package not available") |
| 222 | + from diffpy.structure import Structure |
| 223 | + |
| 224 | + if not diffpy_srreal_available: |
| 225 | + pytest.skip("diffpy.srreal package not available") |
| 226 | + |
| 227 | + pc = PDFContribution("pdf") |
| 228 | + pc.setQmax(21) |
| 229 | + pc.addStructure("empty", Structure()) |
| 230 | + assert 21 == pc.empty.getQmax() |
| 231 | + pc.setQmax(22) |
| 232 | + assert 22 == pc.getQmax() |
| 233 | + assert 22 == pc.empty.getQmax() |
| 234 | + return |
| 235 | + |
| 236 | + |
| 237 | +def test_getQmax(diffpy_structure_available, diffpy_srreal_available): |
| 238 | + """Check PDFContribution.getQmax()""" |
| 239 | + if not diffpy_structure_available: |
| 240 | + pytest.skip("diffpy.structure package not available") |
| 241 | + from diffpy.structure import Structure |
| 242 | + |
| 243 | + if not diffpy_srreal_available: |
| 244 | + pytest.skip("diffpy.srreal package not available") |
| 245 | + |
| 246 | + # cover all code branches in PDFContribution._getMetaValue |
| 247 | + # (1) contribution metadata |
| 248 | + pc1 = PDFContribution("pdf") |
| 249 | + assert pc1.getQmax() is None |
| 250 | + pc1.setQmax(17) |
| 251 | + assert 17 == pc1.getQmax() |
| 252 | + # (2) contribution metadata |
| 253 | + pc2 = PDFContribution("pdf") |
| 254 | + pc2.addStructure("empty", Structure()) |
| 255 | + pc2.empty.setQmax(18) |
| 256 | + assert 18 == pc2.getQmax() |
| 257 | + # (3) profile metadata |
| 258 | + pc3 = PDFContribution("pdf") |
| 259 | + pc3.profile.meta["qmax"] = 19 |
| 260 | + assert 19 == pc3.getQmax() |
| 261 | + return |
| 262 | + |
| 263 | + |
| 264 | +def test_savetxt( |
| 265 | + diffpy_structure_available, diffpy_srreal_available, datafile |
| 266 | +): |
| 267 | + "check PDFContribution.savetxt()" |
| 268 | + if not diffpy_structure_available: |
| 269 | + pytest.skip("diffpy.structure package not available") |
| 270 | + from diffpy.structure import Structure |
| 271 | + |
| 272 | + if not diffpy_srreal_available: |
| 273 | + pytest.skip("diffpy.srreal package not available") |
| 274 | + |
| 275 | + pc = PDFContribution("pdf") |
| 276 | + pc.loadData(datafile("si-q27r60-xray.gr")) |
| 277 | + pc.setCalculationRange(0, 10) |
| 278 | + pc.addStructure("empty", Structure()) |
| 279 | + fp = io.BytesIO() |
| 280 | + with pytest.raises(SrFitError): |
| 281 | + pc.savetxt(fp) |
| 282 | + pc.evaluate() |
| 283 | + pc.savetxt(fp) |
| 284 | + txt = fp.getvalue().decode() |
| 285 | + nlines = len(txt.strip().split("\n")) |
| 286 | + assert 1001 == nlines |
| 287 | + return |
| 288 | + |
| 289 | + |
| 290 | +def test_pickling( |
| 291 | + diffpy_structure_available, diffpy_srreal_available, datafile |
| 292 | +): |
| 293 | + "validate PDFContribution.residual() after pickling." |
| 294 | + if not diffpy_structure_available: |
| 295 | + pytest.skip("diffpy.structure package not available") |
| 296 | + from diffpy.structure import loadStructure |
| 297 | + |
| 298 | + if not diffpy_srreal_available: |
| 299 | + pytest.skip("diffpy.srreal package not available") |
| 300 | + |
| 301 | + pc = PDFContribution("pdf") |
| 302 | + pc.loadData(datafile("ni-q27r100-neutron.gr")) |
| 303 | + ciffile = datafile("ni.cif") |
| 304 | + cif_path = str(ciffile) |
| 305 | + ni = loadStructure(cif_path) |
| 306 | + ni.Uisoequiv = 0.003 |
| 307 | + pc.addStructure("ni", ni) |
| 308 | + pc.setCalculationRange(0, 10) |
| 309 | + pc2 = pickle.loads(pickle.dumps(pc)) |
| 310 | + res0 = pc.residual() |
| 311 | + assert numpy.array_equal(res0, pc2.residual()) |
| 312 | + for p in chain(pc.iterPars("Uiso"), pc2.iterPars("Uiso")): |
| 313 | + p.value = 0.004 |
| 314 | + res1 = pc.residual() |
| 315 | + assert not numpy.allclose(res0, res1) |
| 316 | + assert numpy.array_equal(res1, pc2.residual()) |
| 317 | + return |
| 318 | + |
| 319 | + |
| 320 | +if __name__ == "__main__": |
| 321 | + unittest.main() |
0 commit comments