diff --git a/docs/examples/sequential_fit_example.py b/docs/examples/sequential_fit_example.py index 6df67f2..435082e 100644 --- a/docs/examples/sequential_fit_example.py +++ b/docs/examples/sequential_fit_example.py @@ -16,10 +16,10 @@ def main(): output_result_dir=str(output_folder), filename_order_pattern=r"(\d+)K\.gr", refinable_variable_names=[ - "a_1", + "a_phase_1", "s0", - "Uiso_0_1", - "delta2_1", + "Uiso_phase_1_atom_1", + "delta2_phase_1", "qdamp", "qbroad", ], @@ -27,9 +27,9 @@ def main(): "s0": 0.4, "qdamp": 0.04, "qbroad": 0.02, - "a_1": 3.52, - "Uiso_0_1": 0.005, - "delta2_1": 2, + "a_phase_1": 3.52, + "Uiso_phase_1_atom_1": 0.005, + "delta2_phase_1": 2, }, xmin=1.5, xmax=25.0, diff --git a/news/fix-miscellaneous.rst b/news/fix-miscellaneous.rst new file mode 100644 index 0000000..83ca647 --- /dev/null +++ b/news/fix-miscellaneous.rst @@ -0,0 +1,23 @@ +**Added:** + +* No news added: fix miscellaneous errors. + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/pdfbl/sequential/pdfadapter.py b/src/pdfbl/sequential/pdfadapter.py index f64ead3..633d8a0 100644 --- a/src/pdfbl/sequential/pdfadapter.py +++ b/src/pdfbl/sequential/pdfadapter.py @@ -252,6 +252,14 @@ def initialize_recipe( - constrain variables of the scatters - change symmetry constraints """ + + def modify_xyz_adp_name(parname, nth_phase): + parname, nth_atom = parname.split("_") + return f"{parname}_phase_{nth_phase+1}_atom_{int(nth_atom)+1}" + + def modify_lat_delta_name(parname, nth_phase): + return f"{parname}_phase_{nth_phase+1}" + recipe = FitRecipe() recipe.addContribution(self.contribution) qdamp = recipe.newVar("qdamp", fixed=False, value=0.04) @@ -264,7 +272,9 @@ def initialize_recipe( "delta2", ]: par = getattr(pdfgenerator, pname) - recipe.addVar(par, name=pname + f"_{i+1}", fixed=False) + recipe.addVar( + par, name=modify_lat_delta_name(pname, i), fixed=False + ) if len(self.pdfgenerators) > 1: recipe.addVar( getattr(self.contribution, f"s{i+1}"), @@ -277,11 +287,17 @@ def initialize_recipe( stru_parset = pdfgenerator.phase spacegroupparams = constrainAsSpaceGroup(stru_parset, spacegroup) for par in spacegroupparams.xyzpars: - recipe.addVar(par, name=par.name + f"_{i+1}", fixed=False) + recipe.addVar( + par, name=modify_xyz_adp_name(par.name, i), fixed=False + ) for par in spacegroupparams.latpars: - recipe.addVar(par, name=par.name + f"_{i+1}", fixed=False) + recipe.addVar( + par, name=modify_lat_delta_name(par.name, i), fixed=False + ) for par in spacegroupparams.adppars: - recipe.addVar(par, name=par.name + f"_{i+1}", fixed=False) + recipe.addVar( + par, name=modify_xyz_adp_name(par.name, i), fixed=False + ) recipe.addVar(self.contribution.s0, name="s0", fixed=False) recipe.fix("all") recipe.fithooks[0].verbose = 0 @@ -313,12 +329,18 @@ def residual(self, p=[]): The residual array. """ residual = self.recipe.residual(p) - if self.intermediate_results is not None: - fitresults = FitResults(self.recipe) - for (key, step), values in self.intermediate_results.items(): - if (self.iter_count % step) == 0: - value = getattr(fitresults, key) - values.put(value) + fitresults_dict = None + for (key, step), values in self.intermediate_results.items(): + if (self.iter_count % step) == 0: + if fitresults_dict is None: + fitresults_dict = self.save_results(mode="dict") + value = fitresults_dict.get(key, None) + if value is None: + raise KeyError( + f"{key} is not found in the fit results. " + f"Available keys are: {list(fitresults_dict.keys())}" + ) + values.put(value) self.iter_count += 1 return residual diff --git a/src/pdfbl/sequential/sequential_cmi_runner.py b/src/pdfbl/sequential/sequential_cmi_runner.py index ad2742e..1a9db2a 100644 --- a/src/pdfbl/sequential/sequential_cmi_runner.py +++ b/src/pdfbl/sequential/sequential_cmi_runner.py @@ -9,7 +9,6 @@ from typing import Literal from bg_mpl_stylesheets.styles import all_styles -from diffpy.srfit.fitbase import FitResults from matplotlib import pyplot as plt from prompt_toolkit import PromptSession from prompt_toolkit.patch_stdout import patch_stdout @@ -476,8 +475,8 @@ def _run_one_cycle(self, stop_event=SimpleNamespace(is_set=lambda: False)): new_value ) for entry_name in self.visualization_data.get("results", {}): - fit_results = FitResults(self.adapter.recipe) - entry_value = getattr(fit_results, entry_name) + fitresults_dict = self.adapter.save_results(mode="dict") + entry_value = fitresults_dict.get(entry_name, None) self.visualization_data["results"][entry_name]["ydata"].put( entry_value ) diff --git a/tests/diffpycmi_scripts.py b/tests/diffpycmi_scripts.py deleted file mode 100644 index 7df2759..0000000 --- a/tests/diffpycmi_scripts.py +++ /dev/null @@ -1,425 +0,0 @@ -# flake8: noqa -# Copied from https://github.com/Billingegroup/pdfttp_data -"""Please see notes in Chapter 3 of the 'PDF to the People' book for -additional explanation of the code. - -This Diffpy-CMI script will carry out a structural refinement of a -measured PDF from nickel. It is the same refinement as is done using -PDFgui in this chapter of the book, only this time using Diffpy-CMI -""" - -# 1: Import relevant system packages that we will need... -from pathlib import Path - -import matplotlib as mpl -import matplotlib.pyplot as plt -import numpy as np - -# ... and the relevant CMI packages -from diffpy.srfit.fitbase import ( - FitContribution, - FitRecipe, - FitResults, - Profile, -) -from diffpy.srfit.pdf import PDFGenerator, PDFParser -from diffpy.srfit.structure import constrainAsSpaceGroup -from diffpy.structure.parsers import getParser -from scipy.optimize import least_squares - -############### Config ############################## -# 2: Give a file path to where your PDF (.gr) and structure (.cif) files are located. -# In this case it is two directories up, in a folder called 'data'. -# First we store the absolute directory of this script, then two directories above this, -# with the directory 'data' appended -PWD = Path().parent.absolute() -DPATH = PWD / "example" / "data" - -# 3: Give an identifying name for the refinement, similar -# to what you would name a fit tree in PDFGui. -FIT_ID = "Fit_Ni_Bulk" - -# 4: Specify the names of the input PDF and CIF file. -GR_NAME = "Ni_PDF_20250923-004037_8c9cd1_90K.gr" -CIF_NAME = "Ni.cif" - -######## Experimental PDF Config ###################### -# 5: Specify the min, max, and step r-values of the PDF (that we want to fit over) -# also, specify the Qmax and Qmin values used to reduce the PDF. -PDF_RMIN = 1.5 -PDF_RMAX = 50 -PDF_RSTEP = 0.01 -QMAX = 25 -QMIN = 0.1 - -######## PDF Initialize refinable variables ############# -# 6: We explicitly specify initial values lattice parameter, scale, and -# isotropic thermal parameters, as well as a correlated -# motion parameter, in this case delta_2. -CUBICLAT_I = 3.52 -SCALE_I = 0.4 -UISO_I = 0.005 -DELTA2_I = 2 - -# 7: We will give initial values for the instrumental parameters, but because -# this is a calibrant, we will also refine these variables. -QDAMP_I = 0.04 -QBROAD_I = 0.02 - -# If we want to run using multiprocessors, we can switch this to 'True'. -# This requires that the 'psutil' python package installed. -RUN_PARALLEL = True - - -######## Functions that will carry out the refinement ################## -# 8: We define a function 'make_recipe' to make the recipe that the fit will follow. -# This Fit Recipe object contains the PDF data, information on all the structure(s) -# we will use to fit, and all relevant details necessary to run the fit. -def make_recipe(cif_path, dat_path): - """Creates and returns a Fit Recipe object. - - Parameters - ---------- - cif_path : string, The full path to the structure CIF file to load. - dat_path : string, The full path to the PDF data to be fit. - - Returns - ---------- - recipe : The initialized Fit Recipe object using the dat_path and structure path - provided. - """ - # 9: Create a CIF file parsing object, and use it to parse out - # relevant info and load the structure in the CIF file. This - # includes the space group of the structure. We need this so we - # can constrain the structure parameters later on. - p_cif = getParser("cif") - stru1 = p_cif.parseFile(cif_path) - sg = p_cif.spacegroup.short_name - - # 10: Create a Profile object for the experimental dataset. - # This handles all details about the dataset. - # We also tell this profile the range and mesh of points in r-space. - # The 'PDFParser' function should parse out the appropriate Q_min and - # Q_max from the *.gr file, if the information is present. - profile = Profile() - parser = PDFParser() - parser.parseFile(dat_path) - profile.loadParsedData(parser) - profile.setCalculationRange(xmin=PDF_RMIN, xmax=PDF_RMAX, dx=PDF_RSTEP) - - # 11: Create a PDF Generator object for a periodic structure model. - # Here we name it arbitrarily 'G1' and we give it the structure object. - # This Generator will later compute the model PDF for the structure - # object we provide it here. - generator_crystal1 = PDFGenerator("G1") - generator_crystal1.setStructure(stru1, periodic=True) - - # 12: Create a Fit Contribution object, and arbitrarily name it 'crystal'. - # We then give the PDF Generator object we created just above - # to this Fit Contribution object. The Fit Contribution holds - # the equation used to fit the PDF. - contribution = FitContribution("crystal") - contribution.addProfileGenerator(generator_crystal1) - - # If you have a multi-core computer (you probably do), - # run your refinement in parallel! - # Here we just make sure not to overload your CPUs. - if RUN_PARALLEL: - try: - import multiprocessing - from multiprocessing import Pool - - import psutil - - syst_cores = multiprocessing.cpu_count() - cpu_percent = psutil.cpu_percent() - avail_cores = np.floor((100 - cpu_percent) / (100.0 / syst_cores)) - ncpu = int(np.max([1, avail_cores])) - pool = Pool(processes=ncpu) - generator_crystal1.parallel(ncpu=ncpu, mapfunc=pool.map) - except ImportError: - print( - "\nYou don't appear to have the necessary packages for parallelization" - ) - - # 13: Set the experimental profile, within the Fit Contribution object, - # to the Profile object we created earlier. - contribution.setProfile(profile, xname="r") - - # 14: Set an equation, within the Fit Contribution, based on your PDF - # Generators. Here we simply have one Generator, 'G1', and a scale variable, - # 's1'. Using this structure is a very flexible way of adding additional - # Generators (ie. multiple structural phases), experimental Profiles, - # PDF characteristic functions (ie. shape envelopes), and more. - contribution.setEquation("s1*G1") - - # 15: Create the Fit Recipe object that holds all the details of the fit, - # defined in previous lines above. We give the Fit Recipe the Fit - # Contribution we created earlier. - recipe = FitRecipe() - recipe.addContribution(contribution) - - # 16: Initialize the instrument parameters, Q_damp and Q_broad, and - # assign Q_max and Q_min, all part of the PDF Generator object. - # It's possible that the 'PDFParse' function we used above - # already parsed out this information, but in case it didn't, we set it - # explicitly again here. - # All parameter objects can have their value assigned using the - # below '.value = ' syntax. - recipe.crystal.G1.qdamp.value = QDAMP_I - recipe.crystal.G1.qbroad.value = QBROAD_I - recipe.crystal.G1.setQmax(QMAX) - recipe.crystal.G1.setQmin(QMIN) - - # 17: Add a variable to the Fit Recipe object, initialize the variables - # with some value, and tag it with an arbitrary string. Here we add the scale - # parameter from the Fit Contribution. The '.addVar' method can be - # used to add variables to the Fit Recipe. - recipe.addVar(contribution.s1, SCALE_I, tag="scale") - - # 18: Configure some additional fit variables pertaining to symmetry. - # We can use the srfit function 'constrainAsSpaceGroup' to constrain - # the lattice and ADP parameters according to the Fm-3m space group. - # First we establish the relevant parameters, then we loop through - # the parameters and activate and tag them. We must explicitly set the - # ADP parameters using 'value=' because CIF had no ADP data. - spacegroupparams = constrainAsSpaceGroup(generator_crystal1.phase, sg) - for par in spacegroupparams.latpars: - recipe.addVar( - par, value=CUBICLAT_I, fixed=False, name="fcc_Lat", tag="lat" - ) - for par in spacegroupparams.adppars: - recipe.addVar( - par, value=UISO_I, fixed=False, name="fcc_ADP", tag="adp" - ) - - # 19: Add delta and instrumental parameters to Fit Recipe. - # These parameters are contained as part of the PDF Generator object - # and initialized with values as defined in the opening of the script. - # We give them unique names, and tag them with relevant strings. - recipe.addVar( - generator_crystal1.delta2, name="Ni_Delta2", value=DELTA2_I, tag="d2" - ) - - recipe.addVar( - generator_crystal1.qdamp, - fixed=False, - name="Calib_Qdamp", - value=QDAMP_I, - tag="inst", - ) - - recipe.addVar( - generator_crystal1.qbroad, - fixed=False, - name="Calib_Qbroad", - value=QBROAD_I, - tag="inst", - ) - - # 20: Return the Fit Recipe object to be optimized. - return recipe - - # End of function - - -# 21: We create a useful function 'plot_results' for writing a plot of the fit to disk. -# We won't go into detail here as much of this is non-CMI specific -def plot_results(recipe, fig_name): - """Creates plots of the fitted PDF and residual, and writes them to - disk as *.pdf files. - - Parameters - ---------- - recipe : The optimized Fit Recipe object containing the PDF data - we wish to plot. - fig_name : Path object, the full path to the figure file to create.. - - Returns - ---------- - None - """ - if not isinstance(fig_name, Path): - fig_name = Path(fig_name) - - plt.clf() - plt.close("all") - - # Get an array of the r-values we fitted over. - r = recipe.crystal.profile.x - - # Get an array of the observed PDF. - g = recipe.crystal.profile.y - - # Get an array of the calculated PDF. - gcalc = recipe.crystal.profile.ycalc - - # Make an array of identical shape as g which is offset from g. - diffzero = -0.65 * max(g) * np.ones_like(g) - - # Calculate the residual (difference) array and offset it vertically. - diff = g - gcalc + diffzero - - # Change some style details of the plot - mpl.rcParams.update(mpl.rcParamsDefault) - if (PWD.parent.parent.parent / "utils" / "billinge.mplstyle").exists(): - plt.style.use( - str(PWD.parent.parent.parent / "utils" / "billinge.mplstyle") - ) - - # Create a figure and an axis on which to plot - fig, ax1 = plt.subplots(1, 1) - - # Plot the difference offset line - ax1.plot(r, diffzero, lw=1.0, ls="--", c="black") - - # Plot the measured data - ax1.plot( - r, - g, - ls="None", - marker="o", - ms=5, - mew=0.2, - mfc="None", - label="G(r) Data", - ) - - # Plot the calculated data - ax1.plot(r, gcalc, lw=1.3, label="G(r) Fit") - - # Plot the difference - ax1.plot(r, diff, lw=1.2, label="G(r) diff") - - # Let's label the axes! - ax1.set_xlabel(r"r ($\mathrm{\AA}$)") - ax1.set_ylabel(r"G ($\mathrm{\AA}$$^{-2}$)") - - # Tune the tick markers. We are picky! - ax1.tick_params(axis="both", which="major", top=True, right=True) - - # Set the boundaries on the x-axis - ax1.set_xlim(r[0], r[-1]) - - # We definitely want a legend! - ax1.legend() - - # Let's use a tight layout. Shun wasted space! - plt.tight_layout() - - # This is going to make a figure pop up on screen for you to view. - # The script will pause until you close the figure! - plt.show() - - # Let's save the figure! - fig.savefig(fig_name.parent / f"{fig_name.name}.pdf", format="pdf") - - # End of function - - -# 22: By Convention, this main function is where we do most of our work, and it -# is the bit of code which will be run when we issue 'python file.py' from a terminal. -def main(): - """This will run by default when the file is executed using 'python - file.py' in the command line. - - Parameters - ---------- - None - - Returns - ---------- - None - """ - # Make some folders to store our output files. - resdir = PWD / "res" - fitdir = PWD / "fit" - figdir = PWD / "fig" - - folders = [resdir, fitdir, figdir] - - for folder in folders: - if not folder.exists(): - folder.mkdir() - - # Establish the location of the data and a name for our fit. - gr_path = DPATH / "sequential_fit" / GR_NAME - basename = FIT_ID - print(basename) - - # Establish the full path of the CIF file with the structure of interest. - stru_path = DPATH / CIF_NAME - - print(gr_path) - print(stru_path) - - # 23: Now we call our 'make_recipe' function created above, giving - # strings which points to the relevant CIF file and PDF data file. - recipe = make_recipe(str(stru_path), str(gr_path)) - - # Tell the Fit Recipe we want to write the maximum amount of - # information to the terminal during fitting. - # Passing '2' or '1' prints intermediate info, while '0' prints no info. - recipe.fithooks[0].verbose = 0 - - # 24: During the optimization, fix and free parameters sequentially - # as you would PDFgui. This leads to more stability in the refinement. - # This can be done with 'recipe.fix' and 'recipe.free' and we can use - # either a single parameter name or any of the tags we assigned when creating - # the fit recipe. We first fix all variables. The tag 'all' incorporates every parameter. - # We then create a list of 'tags' which we want free sequentially, we - # loop over them freeing each during a loop, and then fit using the - # SciPy function 'least_squares'. 'least_squares' takes as its arguments - # the function to be optimized, here 'recipe.residual', - # as well as initial values for the fitted parameters, provided by - # 'recipe.values'. The 'x_scale="jac"' argument is optional - # and provides for a bit more stability. - recipe.fix("all") - tags = ["lat", "scale", "adp", "d2", "all"] - - for tag in tags: - recipe.free(tag) - least_squares(recipe.residual, recipe.values, x_scale="jac") - for tag in tags: - recipe.free(tag) - least_squares(recipe.residual, recipe.values, x_scale="jac") - - for pname, parameter in recipe._parameters.items(): - print(f"{pname}: {parameter.value}") - - # 25: We use the 'savetxt' method of the profile to write a text file - # containing the measured and fitted PDF to disk. - # The file is named based on the basename we created earlier, and - # written to the 'fitdir' directory. - profile = recipe.crystal.profile - profile.savetxt(fitdir / f"{basename}.fit") - - # 26: We use the 'FitResults' method to parse out the results from - # the optimized Fit Recipe, and 'printResults' to print them - # to the terminal. - res = FitResults(recipe) - res.printResults() - - # 27: We use the 'saveResults' method of 'FitResults' to write a text file - # containing the fitted parameters and fit quality indices to disk. - # The file is named based on the basename we created earlier, and - # written to the 'resdir' directory. - header = "crystal_HF.\n" - res.saveResults(resdir / f"{basename}.res", header=header) - - # 28: We use the 'plot_results' method we created earlier to write a pdf file - # containing the measured and fitted PDF with residual to disk. - # The file is named based on the 'basename' we created earlier, and - # written to the 'figdir' directory. - plot_results(recipe, figdir / basename) - for varname, var in recipe._parameters.items(): - print(f"{varname}: {var.value}") - - # End of function - - -# This tells python to run the 'main' function we defined above. -if __name__ == "__main__": - main() - -# End of file diff --git a/tests/helper.py b/tests/helper.py new file mode 100644 index 0000000..6b0c61d --- /dev/null +++ b/tests/helper.py @@ -0,0 +1,101 @@ +import numpy as np +from diffpy.srfit.fitbase import FitContribution, FitRecipe, Profile +from diffpy.srfit.pdf import PDFGenerator, PDFParser +from diffpy.srfit.structure import constrainAsSpaceGroup +from diffpy.structure.parsers import getParser + + +def make_cmi_recipe(cif_path, dat_path, variable_values={}): + """Creates and returns a diffpy.cmi Fit Recipe object. + + Parameters + ---------- + cif_path : str + The full path to the structure CIF file to load. + dat_path : str + The full path to the PDF data to be fit. + variable_values : dict, + The dictionary of variable values to initialize the + FitRecipe. + + Returns + ---------- + recipe : FitRecipe + The created FitRecipe. + """ + PDF_RMIN = variable_values.get("xmin", 1.5) + PDF_RMAX = variable_values.get("xmax", 50) + PDF_RSTEP = variable_values.get("dx", 0.01) + QMAX = variable_values.get("qmax", 25) + QMIN = variable_values.get("qmin", 0.1) + SCALE_I = variable_values.get("s0", 0.4) + CUBICLAT_I = variable_values.get("a_phase_1", 3.52) + UISO_I = variable_values.get("Uiso_phase_1_atom_1", 0.005) + DELTA2_I = variable_values.get("delta2_phase_1", 2) + QDAMP_I = variable_values.get("qdamp", 0.04) + QBROAD_I = variable_values.get("qbroad", 0.02) + RUN_PARALLEL = True + + p_cif = getParser("cif") + stru1 = p_cif.parseFile(cif_path) + sg = p_cif.spacegroup.short_name + profile = Profile() + parser = PDFParser() + parser.parseFile(dat_path) + profile.loadParsedData(parser) + profile.setCalculationRange(xmin=PDF_RMIN, xmax=PDF_RMAX, dx=PDF_RSTEP) + generator_crystal1 = PDFGenerator("G1") + generator_crystal1.setStructure(stru1, periodic=True) + generator_crystal1.setQmax(QMAX) + generator_crystal1.setQmin(QMIN) + generator_crystal1.delta2.value = DELTA2_I + contribution = FitContribution("crystal") + contribution.addProfileGenerator(generator_crystal1) + if RUN_PARALLEL: + try: + import multiprocessing + from multiprocessing import Pool + + import psutil + + syst_cores = multiprocessing.cpu_count() + cpu_percent = psutil.cpu_percent() + avail_cores = np.floor((100 - cpu_percent) / (100.0 / syst_cores)) + ncpu = int(np.max([1, avail_cores])) + pool = Pool(processes=ncpu) + generator_crystal1.parallel(ncpu=ncpu, mapfunc=pool.map) + except ImportError: + print( + "\nYou don't appear to have the necessary packages for " + "parallelization" + ) + contribution.setProfile(profile, xname="r") + contribution.setEquation("s0*G1") + recipe = FitRecipe() + recipe.addContribution(contribution) + recipe.crystal.G1.qdamp.value = QDAMP_I + recipe.crystal.G1.qbroad.value = QBROAD_I + recipe.crystal.G1.setQmax(QMAX) + recipe.crystal.G1.setQmin(QMIN) + recipe.addVar(contribution.s0, SCALE_I, name="s0") + spacegroupparams = constrainAsSpaceGroup(generator_crystal1.phase, sg) + for par in spacegroupparams.latpars: + recipe.addVar(par, value=CUBICLAT_I, fixed=False, name="a_phase_1") + for par in spacegroupparams.adppars: + recipe.addVar( + par, value=UISO_I, fixed=False, name="Uiso_phase_1_atom_1" + ) + recipe.addVar(generator_crystal1.delta2, name="delta2_phase_1") + recipe.addVar( + generator_crystal1.qdamp, + fixed=False, + name="qdamp", + value=QDAMP_I, + ) + recipe.addVar( + generator_crystal1.qbroad, + fixed=False, + name="qbroad", + value=QBROAD_I, + ) + return recipe diff --git a/tests/test_pdfadapter.py b/tests/test_pdfadapter.py index 9d7a8ed..b6e2560 100644 --- a/tests/test_pdfadapter.py +++ b/tests/test_pdfadapter.py @@ -1,14 +1,11 @@ -import sys from pathlib import Path import numpy +from helper import make_cmi_recipe from scipy.optimize import least_squares from pdfbl.sequential.pdfadapter import PDFAdapter -sys.path.append(str(Path(__file__).parent / "diffpycmi_scripts.py")) -from diffpycmi_scripts import make_recipe # noqa: E402 - def test_pdfadapter(): # C1: Run the same fit with pdfadapter and diffpy_cmi @@ -16,12 +13,30 @@ def test_pdfadapter(): # diffpy_cmi fitting structure_path = Path(__file__).parent / "data" / "Ni.cif" profile_path = Path(__file__).parent / "data" / "Ni.gr" - diffpycmi_recipe = make_recipe(str(structure_path), str(profile_path)) + initial_pv_dict = { + "s0": 0.4, + "qdamp": 0.04, + "qbroad": 0.02, + "a_phase_1": 3.52, + "delta2_phase_1": 2, + "Uiso_phase_1_atom_1": 0.005, + } + variables_to_refine = [ + "a_phase_1", + "s0", + "Uiso_phase_1_atom_1", + "delta2_phase_1", + "qdamp", + "qbroad", + ] + diffpycmi_recipe = make_cmi_recipe( + str(structure_path), str(profile_path), initial_pv_dict + ) diffpycmi_recipe.fithooks[0].verbose = 0 diffpycmi_recipe.fix("all") - tags = ["lat", "scale", "adp", "d2", "all"] - for tag in tags: - diffpycmi_recipe.free(tag) + + for var_name in variables_to_refine: + diffpycmi_recipe.free(var_name) least_squares( diffpycmi_recipe.residual, diffpycmi_recipe.values, @@ -38,39 +53,14 @@ def test_pdfadapter(): adapter.initialize_structures([str(structure_path)]) adapter.initialize_contribution() adapter.initialize_recipe() - initial_pdfadapter_pv_dict = { - "s0": 0.4, - "qdamp": 0.04, - "qbroad": 0.02, - "a_1": 3.52, - "Uiso_0_1": 0.005, - "delta2_1": 2, - } - adapter.set_initial_variable_values(initial_pdfadapter_pv_dict) + adapter.set_initial_variable_values(initial_pv_dict) adapter.refine_variables( - [ - "a_1", - "s0", - "Uiso_0_1", - "delta2_1", - "qdamp", - "qbroad", - ] + variables_to_refine, ) - diffpyname_to_adaptername = { - "fcc_Lat": "a_1", - "s1": "s0", - "fcc_ADP": "Uiso_0_1", - "Ni_Delta2": "delta2_1", - "Calib_Qdamp": "qdamp", - "Calib_Qbroad": "qbroad", - } pdfadapter_pv_dict = {} for pname, parameter in adapter.recipe._parameters.items(): pdfadapter_pv_dict[pname] = parameter.value - for diffpy_pname, adapter_pname in diffpyname_to_adaptername.items(): - assert numpy.isclose( - diffpy_pv_dict[diffpy_pname], - pdfadapter_pv_dict[adapter_pname], - atol=1e-5, - ) + for var_name in variables_to_refine: + diffpy_value = diffpy_pv_dict[var_name] + pdfadapter_value = pdfadapter_pv_dict[var_name] + assert numpy.isclose(diffpy_value, pdfadapter_value, atol=1e-5) diff --git a/tests/test_sequential_cmi_runner.py b/tests/test_sequential_cmi_runner.py index 5f7e689..3b7cc58 100644 --- a/tests/test_sequential_cmi_runner.py +++ b/tests/test_sequential_cmi_runner.py @@ -61,10 +61,10 @@ def test_run_sequential_cmi_runner(user_filesystem): input_data_folder = Path(__file__).parent / "data" / "input_data_dir" structure_path = Path(__file__).parent / "data" / "Ni.cif" refinable_variable_names = [ - "a_1", + "a_phase_1", "s0", - "Uiso_0_1", - "delta2_1", + "Uiso_phase_1_atom_1", + "delta2_phase_1", "qdamp", "qbroad", ] @@ -72,9 +72,9 @@ def test_run_sequential_cmi_runner(user_filesystem): "s0": 0.4, "qdamp": 0.04, "qbroad": 0.02, - "a_1": 3.52, - "Uiso_0_1": 0.005, - "delta2_1": 2, + "a_phase_1": 3.52, + "Uiso_phase_1_atom_1": 0.005, + "delta2_phase_1": 2, } runner = SequentialCMIRunner() runner.load_inputs( @@ -106,10 +106,10 @@ def test_data_for_plot(user_filesystem): input_data_folder = Path(__file__).parent / "data" / "input_data_dir" structure_path = Path(__file__).parent / "data" / "Ni.cif" refinable_variable_names = [ - "a_1", + "a_phase_1", "s0", - "Uiso_0_1", - "delta2_1", + "Uiso_phase_1_atom_1", + "delta2_phase_1", "qdamp", "qbroad", ] @@ -117,10 +117,17 @@ def test_data_for_plot(user_filesystem): "s0": 0.4, "qdamp": 0.04, "qbroad": 0.02, - "a_1": 3.52, - "Uiso_0_1": 0.005, - "delta2_1": 2, + "a_phase_1": 3.52, + "Uiso_phase_1_atom_1": 0.005, + "delta2_phase_1": 2, } + result_entry_names = [ + "residual", + "contributions", + "restraints", + "chi2", + "reduced_chi2", + ] runner = SequentialCMIRunner() runner.load_inputs( input_data_dir=str(input_data_folder), @@ -134,18 +141,23 @@ def test_data_for_plot(user_filesystem): dx=0.01, qmax=25, qmin=0.1, - plot_variable_names=["a_1"], - plot_result_names=["residual"], - plot_intermediate_result_names=["residual"], + plot_variable_names=refinable_variable_names, + plot_result_names=result_entry_names, + plot_intermediate_result_names=result_entry_names, show_plot=False, ) # Expect corresponding key are created in data_for_plot after running. assert "variables" in runner.visualization_data assert "results" in runner.visualization_data assert "intermediate_results" in runner.visualization_data - assert "a_1" in runner.visualization_data["variables"] - assert "residual" in runner.visualization_data["results"] - assert "residual" in runner.visualization_data["intermediate_results"] + for var_name in refinable_variable_names: + assert var_name in runner.visualization_data["variables"] + for result_entry_name in result_entry_names: + assert result_entry_name in runner.visualization_data["results"] + assert ( + result_entry_name + in runner.visualization_data["intermediate_results"] + ) runner.run(mode="batch") # Expect 'buffer' in each plot data are populated after running. names = ["variables", "results", "intermediate_results"]