diff --git a/.github/workflows/matrix-and-codecov-on-merge-to-main.yml b/.github/workflows/matrix-and-codecov-on-merge-to-main.yml index d9d7476..fc33dd8 100644 --- a/.github/workflows/matrix-and-codecov-on-merge-to-main.yml +++ b/.github/workflows/matrix-and-codecov-on-merge-to-main.yml @@ -17,5 +17,8 @@ jobs: project: diffpy.cmipdf c_extension: false headless: false + run: | + set -Eeuo pipefail + python -m pip install --no-deps diffpy.srfit==3.4.0rc0 secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/tests-on-pr.yml b/.github/workflows/tests-on-pr.yml index 3ecbc4c..e40d41e 100644 --- a/.github/workflows/tests-on-pr.yml +++ b/.github/workflows/tests-on-pr.yml @@ -11,5 +11,8 @@ jobs: project: diffpy.cmipdf c_extension: false headless: false + run: | + set -Eeuo pipefail + python -m pip install --no-deps diffpy.srfit==3.4.0rc0 secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/docs/source/api/diffpy.cmipdf.example_package.rst b/docs/source/api/diffpy.cmipdf.example_package.rst deleted file mode 100644 index ccf746c..0000000 --- a/docs/source/api/diffpy.cmipdf.example_package.rst +++ /dev/null @@ -1,31 +0,0 @@ -.. _example_package documentation: - -|title| -======= - -.. |title| replace:: diffpy.cmipdf.example_package package - -.. automodule:: diffpy.cmipdf.example_package - :members: - :undoc-members: - :show-inheritance: - -|foo| ------ - -.. |foo| replace:: diffpy.cmipdf.example_package.foo module - -.. automodule:: diffpy.cmipdf.example_package.foo - :members: - :undoc-members: - :show-inheritance: - -|bar| ------ - -.. |bar| replace:: diffpy.cmipdf.example_package.bar module - -.. automodule:: diffpy.cmipdf.example_package.foo - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/api/diffpy.cmipdf.rst b/docs/source/api/diffpy.cmipdf.rst index 8ce62e5..990948d 100644 --- a/docs/source/api/diffpy.cmipdf.rst +++ b/docs/source/api/diffpy.cmipdf.rst @@ -1,30 +1,60 @@ :tocdepth: -1 -|title| -======= - -.. |title| replace:: diffpy.cmipdf package +diffpy.cmipdf package +===================== .. automodule:: diffpy.cmipdf :members: :undoc-members: :show-inheritance: -Subpackages ------------ - -.. toctree:: - diffpy.cmipdf.example_package - Submodules ---------- -|module| --------- +diffpy.cmipdf.pdfgenerator module +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: diffpy.cmipdf.pdfgenerator + :members: + :undoc-members: + :show-inheritance: + +diffpy.cmipdf.debyepdfgenerator module +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: diffpy.cmipdf.debyepdfgenerator + :members: + :undoc-members: + :show-inheritance: + +diffpy.cmipdf.basepdfgenerator module +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: diffpy.cmipdf.basepdfgenerator + :members: + :undoc-members: + :show-inheritance: + +diffpy.cmipdf.pdfcontribution module +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: diffpy.cmipdf.pdfcontribution + :members: + :undoc-members: + :show-inheritance: + +diffpy.cmipdf.pdfparser module +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: diffpy.cmipdf.pdfparser + :members: + :undoc-members: + :show-inheritance: -.. |module| replace:: diffpy.cmipdf.example_submodule module +diffpy.cmipdf.characteristicfunctions module +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. automodule:: diffpy.cmipdf.example_submodule +.. automodule:: diffpy.cmipdf.characteristicfunctions :members: :undoc-members: :show-inheritance: diff --git a/docs/source/conf.py b/docs/source/conf.py index 5526599..d779970 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -50,7 +50,7 @@ "sphinx.ext.intersphinx", "sphinx_rtd_theme", "sphinx_copybutton", - "m2r", + "m2r2", ] # Add any paths that contain templates here, relative to this directory. diff --git a/news/sync-srfit-3.4.0.rst b/news/sync-srfit-3.4.0.rst new file mode 100644 index 0000000..c0f8aa6 --- /dev/null +++ b/news/sync-srfit-3.4.0.rst @@ -0,0 +1,23 @@ +**Added:** + +* No news needed: syncing with srfit. + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/news/test-standards.rst b/news/test-standards.rst new file mode 100644 index 0000000..05c9a0a --- /dev/null +++ b/news/test-standards.rst @@ -0,0 +1,23 @@ +**Added:** + +* No news added: Restyle tests to the group standards; no user-facing change. + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/pyproject.toml b/pyproject.toml index 3d492b5..a1024d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,11 +49,17 @@ exclude = [] # exclude packages matching these glob patterns (empty by default) namespaces = false # to disable scanning PEP 420 namespaces (true by default) [project.scripts] -diffpy-cmipdf = "diffpy.cmipdf.app:main" +diffpy-cmipdf = "diffpy.cmipdf.cmipdf_app:main" [tool.setuptools.dynamic] dependencies = {file = ["requirements/pip.txt"]} +[tool.pytest.ini_options] +# Fail when cmipdf source calls one of its own deprecated names. +filterwarnings = [ + "error:'diffpy\\.cmipdf\\.:DeprecationWarning:diffpy\\.cmipdf", +] + [tool.codespell] exclude-file = ".codespell/ignore_lines.txt" ignore-words = ".codespell/ignore_words.txt" diff --git a/requirements/docs.txt b/requirements/docs.txt index 5f34c6e..1de813f 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -2,4 +2,4 @@ sphinx sphinx_rtd_theme sphinx-copybutton doctr -m2r +m2r2 diff --git a/src/diffpy/__init__.py b/src/diffpy/__init__.py index 43d76dc..1061e09 100644 --- a/src/diffpy/__init__.py +++ b/src/diffpy/__init__.py @@ -12,3 +12,7 @@ # See LICENSE.rst for license information. # ############################################################################## + +from pkgutil import extend_path + +__path__ = extend_path(__path__, __name__) diff --git a/src/diffpy/cmipdf/basepdfgenerator.py b/src/diffpy/cmipdf/basepdfgenerator.py index 14ea083..b94827a 100644 --- a/src/diffpy/cmipdf/basepdfgenerator.py +++ b/src/diffpy/cmipdf/basepdfgenerator.py @@ -117,7 +117,7 @@ def _set_calculator(self, calc): """ self._calc = calc for pname in self.__class__._parnames: - self.addParameter(ParameterAdapter(pname, self._calc, attr=pname)) + self.add_parameter(ParameterAdapter(pname, self._calc, attr=pname)) self._process_metadata() return @@ -195,7 +195,7 @@ def set_scattering_type(self, stype="X"): ValueError If the scattering type is unknown. """ - self._calc.setScatteringFactorTableByType(stype) + self._calc.scatteringfactortable = stype # update the meta dictionary only if there was no exception self.meta["stype"] = self.get_scattering_type() return diff --git a/src/diffpy/cmipdf/characteristicfunctions.py b/src/diffpy/cmipdf/characteristicfunctions.py index e5b73b5..bebf880 100644 --- a/src/diffpy/cmipdf/characteristicfunctions.py +++ b/src/diffpy/cmipdf/characteristicfunctions.py @@ -225,18 +225,20 @@ def lognormal_spherical_distribution(radial_dist, p_diameter, p_sigma): (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: + diameter:: - F(r, mu, s) = 0.5*Erfc((-mu-3*s^2+Log(r))/(sqrt(2)*s)) - + 0.25*r^3*Erfc((-mu+Log(r))/(sqrt(2)*s))*exp(-3*mu-4.5*s^2) - - 0.75*r*Erfc((-mu-2*s^2+Log(r))/(sqrt(2)*s))*exp(-mu-2.5*s^2) + F(r, mu, s) = ( + 0.5*Erfc((-mu-3*s^2+Log(r))/(sqrt(2)*s)) + + 0.25*r^3*Erfc((-mu+Log(r))/(sqrt(2)*s))*exp(-3*mu-4.5*s^2) + - 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, p_diameter. The variance of the distribution gives p_sigma^2. - mu and s can be expressed in terms of these as: + mu and s can be expressed in terms of these as:: - s^2 = log((p_sigma/p_diameter)^2 + 1) - mu = log(p_diameter) - s^2/2 + s^2 = log((p_sigma/p_diameter)^2 + 1) + mu = log(p_diameter) - s^2/2 Source unknown """ @@ -402,14 +404,14 @@ def __init__(self, name, model): # Wrap normal parameters for parname in model.params: par = SASParameter(parname, model) - self.addParameter(par) + self.add_parameter(par) # Wrap dispersion parameters for parname in model.dispersion: name = parname + "_width" parname += ".width" par = SASParameter(name, model, parname) - self.addParameter(par) + self.add_parameter(par) return diff --git a/src/diffpy/cmipdf/pdfcontribution.py b/src/diffpy/cmipdf/pdfcontribution.py index 3163107..0e69bed 100644 --- a/src/diffpy/cmipdf/pdfcontribution.py +++ b/src/diffpy/cmipdf/pdfcontribution.py @@ -100,10 +100,10 @@ def __init__(self, name): # Need a parameter for the overall scale, in the case that this is a # multi-phase fit. - self.newParameter("scale", 1.0) + self.new_parameter("scale", 1.0) # Profile-related parameters that will be shared between the generators - self.newParameter("qdamp", 0) - self.newParameter("qbroad", 0) + self.new_parameter("qdamp", 0) + self.new_parameter("qbroad", 0) return # Data methods diff --git a/src/diffpy/cmipdf/pdfparser.py b/src/diffpy/cmipdf/pdfparser.py index e17c112..df54682 100644 --- a/src/diffpy/cmipdf/pdfparser.py +++ b/src/diffpy/cmipdf/pdfparser.py @@ -52,6 +52,7 @@ class PDFParser(ProfileParser): _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. diff --git a/tests/conftest.py b/tests/conftest.py index 9245c31..e4544a1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,12 +1,10 @@ import importlib.resources import json -import logging -from functools import lru_cache from pathlib import Path import pytest -logger = logging.getLogger(__name__) +from diffpy.structure import PDFFitStructure @pytest.fixture @@ -23,46 +21,6 @@ def user_filesystem(tmp_path): yield tmp_path -# diffpy.structure -@lru_cache() -def has_diffpy_structure(): - try: - import diffpy.structure as m - - del m - return True - except ImportError: - logger.warning( - "Cannot import diffpy.structure, Structure tests skipped." - ) - return False - - -# diffpy.srreal - - -@lru_cache() -def has_diffpy_srreal(): - try: - import diffpy.srreal.pdfcalculator as m - - del m - return True - except ImportError: - logger.warning("Cannot import diffpy.srreal, PDF tests skipped.") - return False - - -@pytest.fixture(scope="session") -def diffpy_structure_available(): - return has_diffpy_structure() - - -@pytest.fixture(scope="session") -def diffpy_srreal_available(): - return has_diffpy_srreal() - - @pytest.fixture(scope="session") def datafile(): """Fixture to load a test data file from the testdata package @@ -81,3 +39,14 @@ def _as_list(values): return None if values is None else values.tolist() return _as_list + + +@pytest.fixture +def ni_pdffit_structure(datafile): + """Fixture for the Ni structure from ni.cif with Biso = 1 on every + atom.""" + structure = PDFFitStructure() + structure.read(str(datafile("ni.cif"))) + for atom in structure: + atom.Bisoequiv = 1 + return structure diff --git a/tests/test_generators.py b/tests/test_generators.py index b440992..5cbff63 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -1,67 +1,121 @@ +"""Tests for the pdfgenerator module.""" + 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()) +# set_scattering_type stores the radiation type used for the calculation. +@pytest.mark.parametrize( + "input_scattering_type, expected_scattering_type", + [ + # C1: The scattering type is set to neutron. + # Expected: get_scattering_type returns "N". + ("N", "N"), + # C2: The scattering type is set to x-ray. + # Expected: get_scattering_type returns "X". + ("X", "X"), + ], +) +def test_set_scattering_type(input_scattering_type, expected_scattering_type): + generator = PDFGenerator() + generator.set_scattering_type(input_scattering_type) + actual_scattering_type = generator.get_scattering_type() + assert actual_scattering_type == expected_scattering_type - 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() +def test_set_qmax(): + # C1: qmax is set to 27 on a new generator. + # Expected: get_qmax returns 27. + generator = PDFGenerator() + generator.set_qmax(27.0) + actual_qmax = generator.get_qmax() + expected_qmax = 27.0 + assert actual_qmax == pytest.approx(expected_qmax) - 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. +def test_set_qmin(): + generator = PDFGenerator() + # C1: qmin is read from a new generator. + # Expected: qmin is 0 on both the generator and its calculator. + actual_qmins = (generator.get_qmin(), generator._calc.qmin) + expected_qmins = (0, 0) + assert actual_qmins == expected_qmins + # C2: qmin is set to 0.93. + # Expected: qmin is 0.93 on both the generator and its calculator. + generator.set_qmin(0.93) + actual_qmins = (generator.get_qmin(), generator._calc.qmin) + expected_qmins = (0.93, 0.93) + assert actual_qmins == expected_qmins - 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_parameter_names(ni_pdffit_structure): + # C1: A structure is set on the generator. + # Expected: The generator holds one Parameter for each calculator + # attribute it wraps. + generator = PDFGenerator() + generator.set_structure(ni_pdffit_structure) + actual_parameter_names = [ + parameter.name + for parameter in generator.iterate_over_parameters(recurse=False) + ] + expected_parameter_names = ["delta1", "delta2", "qbroad", "scale", "qdamp"] + assert actual_parameter_names == expected_parameter_names -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 +# Each generator Parameter wraps the calculator attribute of the same name. +@pytest.mark.parametrize( + "input_parameter_name", + [ + # C1: A wrapped calculator attribute is read and then set to 1, + # 1. delta1, 2. delta2, 3. qbroad, 4. scale, 5. qdamp. + # Expected: The Parameter starts at the calculator's value, and + # setting it to 1 sets the calculator attribute to 1. + "delta1", + "delta2", + "qbroad", + "scale", + "qdamp", + ], +) +def test_parameter_wraps_calculator(ni_pdffit_structure, input_parameter_name): + generator = PDFGenerator() + generator.set_structure(ni_pdffit_structure) + parameter = generator.get(input_parameter_name) + actual_initial_value = parameter.get_value() + expected_initial_value = generator._calc._getDoubleAttr( + input_parameter_name + ) + assert actual_initial_value == expected_initial_value + parameter.set_value(1.0) + actual_values = ( + parameter.get_value(), + generator._calc._getDoubleAttr(input_parameter_name), + ) + expected_values = (1.0, 1.0) + assert actual_values == expected_values + + +def test_generator_matches_calculator(ni_pdffit_structure): + # C1: The generator computes the neutron PDF of Ni with qmax = 27 on + # r = 0, 0.1, ..., 9.9. + # Expected: The PDF equals the one from a PDFCalculator with the same + # settings. + qmax = 27.0 + r = np.arange(0, 10, 0.1) + generator = PDFGenerator() + generator.set_scattering_type("N") + generator.set_qmax(qmax) + generator.set_structure(ni_pdffit_structure) + actual_pdf = generator(r) + calculator = PDFCalculator() + calculator.rstep = r[1] - r[0] + calculator.rmin = r[0] + calculator.rmax = r[-1] + 0.5 * calculator.rstep + calculator.qmax = qmax + calculator.scatteringfactortable = "N" + calculator.eval(ni_pdffit_structure) + expected_pdf = calculator.pdf + assert np.allclose(actual_pdf, expected_pdf) diff --git a/tests/test_pdfcontribution.py b/tests/test_pdfcontribution.py index 4931d08..466a0d1 100644 --- a/tests/test_pdfcontribution.py +++ b/tests/test_pdfcontribution.py @@ -1,83 +1,128 @@ +"""Tests for the pdfcontribution module.""" + import io import pickle -from itertools import chain +import re import numpy as np import pytest from diffpy.cmipdf import PDFContribution from diffpy.srfit.exceptions import SrFitError -from diffpy.structure import Structure, loadStructure +from diffpy.structure import Structure, load_structure 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 + contribution = PDFContribution("pdf") + # C1: qmax is set to 21 before a structure is added. + # Expected: The generator created for the structure gets qmax = 21. + contribution.set_qmax(21) + contribution.add_structure(Structure(), name="empty") + actual_generator_qmax = contribution.empty.get_qmax() + expected_generator_qmax = 21 + assert actual_generator_qmax == expected_generator_qmax + # C2: qmax is set to 22 after the structure is added. + # Expected: Both the contribution and its generator have qmax = 22. + contribution.set_qmax(22) + actual_qmaxes = (contribution.get_qmax(), contribution.empty.get_qmax()) + expected_qmaxes = (22, 22) + assert actual_qmaxes == expected_qmaxes + + +# get_qmax looks for qmax in the contribution, then in its generators, then +# in its profile. +def test_get_qmax_not_set(): + # C1: qmax is not set anywhere. + # Expected: Returns None. + contribution = PDFContribution("pdf") + actual_qmax = contribution.get_qmax() + expected_qmax = None + assert actual_qmax == expected_qmax + + +def test_get_qmax_from_contribution(): + # C1: qmax is set to 17 on the contribution. + # Expected: Returns 17. + contribution = PDFContribution("pdf") + contribution.set_qmax(17) + actual_qmax = contribution.get_qmax() + expected_qmax = 17 + assert actual_qmax == expected_qmax + + +def test_get_qmax_from_generator(): + # C1: qmax is set to 18 on the contribution's generator only. + # Expected: Returns 18. + contribution = PDFContribution("pdf") + contribution.add_structure(Structure(), name="empty") + contribution.empty.set_qmax(18) + actual_qmax = contribution.get_qmax() + expected_qmax = 18 + assert actual_qmax == expected_qmax + + +def test_get_qmax_from_profile(): + # C1: qmax is set to 19 in the profile metadata only. + # Expected: Returns 19. + contribution = PDFContribution("pdf") + contribution.profile.meta["qmax"] = 19 + actual_qmax = contribution.get_qmax() + expected_qmax = 19 + assert actual_qmax == expected_qmax 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 + # C1: A contribution with data on r = 0 to 10 is evaluated and saved. + # Expected: The file has a header line and one line for each of the + # 1000 points. + contribution = PDFContribution("pdf") + contribution.load_data(datafile("si-q27r60-xray.gr")) + contribution.set_calculation_range(0, 10) + contribution.add_structure(Structure(), name="empty") + contribution.evaluate() + file_object = io.BytesIO() + contribution.savetxt(file_object) + actual_text = file_object.getvalue().decode() + actual_line_count = len(actual_text.strip().split("\n")) + expected_line_count = 1001 + assert actual_line_count == expected_line_count + + +def test_savetxt_bad(datafile): + # C1: A contribution is saved before it is evaluated. + # Expected: Raises an SrFitError, since there is no calculated profile. + contribution = PDFContribution("pdf") + contribution.load_data(datafile("si-q27r60-xray.gr")) + contribution.set_calculation_range(0, 10) + contribution.add_structure(Structure(), name="empty") + expected_msg = "ycalc is None" + with pytest.raises(SrFitError, match=re.escape(expected_msg)): + contribution.savetxt(io.BytesIO()) + + +def _make_ni_contribution(datafile, uiso): + contribution = PDFContribution("pdf") + contribution.load_data(datafile("ni-q27r100-neutron.gr")) + ni = load_structure(str(datafile("ni.cif"))) + ni.Uisoequiv = uiso + contribution.add_structure(ni, name="ni") + contribution.set_calculation_range(0, 10) + return contribution 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 + contribution = _make_ni_contribution(datafile, uiso=0.003) + unpickled_contribution = pickle.loads(pickle.dumps(contribution)) + # C1: A Ni contribution is pickled and unpickled. + # Expected: The copy has the same residual as the original. + actual_residual = unpickled_contribution.residual() + expected_residual = contribution.residual() + assert np.array_equal(actual_residual, expected_residual) + # C2: Uiso of every atom in the copy is changed from 0.003 to 0.004. + # Expected: The copy's residual equals that of a contribution built + # with Uiso = 0.004. + for parameter in unpickled_contribution.iterate_over_parameters("Uiso"): + parameter.value = 0.004 + actual_residual = unpickled_contribution.residual() + expected_residual = _make_ni_contribution(datafile, uiso=0.004).residual() + assert np.allclose(actual_residual, expected_residual)