def makeRecipe(ciffile, datname): """Create a fitting recipe for crystalline PDF data.""" # Work directly with a custom PDFContribution to load the data contribution = PDFContribution("nickel") contribution.loadData(datname) contribution.setCalculationRange(xmin=1, xmax=20, dx=0.1) # and the phase stru = Structure() stru.read(ciffile) contribution.addStructure("nickel", stru) ## Make the FitRecipe and add the FitContribution. recipe = FitRecipe() recipe.addContribution(contribution) ## Configure the fit variables phase = contribution.nickel.phase from diffpy.srfit.structure import constrainAsSpaceGroup sgpars = constrainAsSpaceGroup(phase, "Fm-3m") for par in sgpars.latpars: recipe.addVar(par) for par in sgpars.adppars: recipe.addVar(par, 0.005) recipe.addVar(contribution.scale, 1) recipe.addVar(contribution.qdamp, 0.03, fixed=True) recipe.addVar(contribution.nickel.delta2, 5) # Give the recipe away so it can be used! return recipe
def makeRecipe(ciffile, datname): """Create a fitting recipe for crystalline PDF data.""" # Work directly with a custom PDFContribution to load the data contribution = PDFContribution("nickel") contribution.loadData(datname) contribution.setCalculationRange(xmin = 1, xmax = 20, dx = 0.1) # and the phase stru = Structure() stru.read(ciffile) contribution.addStructure("nickel", stru) ## Make the FitRecipe and add the FitContribution. recipe = FitRecipe() recipe.addContribution(contribution) ## Configure the fit variables phase = contribution.nickel.phase from diffpy.srfit.structure import constrainAsSpaceGroup sgpars = constrainAsSpaceGroup(phase, "Fm-3m") for par in sgpars.latpars: recipe.addVar(par) for par in sgpars.adppars: recipe.addVar(par, 0.005) recipe.addVar(contribution.scale, 1) recipe.addVar(contribution.qdamp, 0.03, fixed = True) recipe.addVar(contribution.nickel.delta2, 5) # Give the recipe away so it can be used! return recipe
# Add the structure from our cif file to the contribution MnOPDF.addStructure("MnO", mnostructure) # The FitRecipe does the work of calculating the PDF with the fit variable # that we give it. MnOFit = FitRecipe() # give the PDFContribution to the FitRecipe MnOFit.addContribution(MnOPDF) # Configure the fit variables and give them to the recipe. We can use the # srfit function constrainAsSpaceGroup to constrain the lattice and ADP # parameters according to the H-3m space group. from diffpy.srfit.structure import constrainAsSpaceGroup spaceGroupParams = constrainAsSpaceGroup(MnOPDF.MnO.phase, spaceGroup) print("Space group parameters are:", end=' ') print(', '.join([p.name for p in spaceGroupParams])) print() # We can now cycle through the parameters and activate them in the recipe as # variables for par in spaceGroupParams.latpars: MnOFit.addVar(par) # Set initial value for the ADP parameters, because CIF had no ADP data. for par in spaceGroupParams.adppars: MnOFit.addVar(par, value=0.003, fixed=True) # As usual, we add variables for the overall scale of the PDF and a delta2 # parameter for correlated motion of neighboring atoms. MnOFit.addVar(MnOPDF.scale, 1)
# Add the structure from our cif file to the contribution niStructure = loadStructure(structureFile) niPDF.addStructure("nickel", niStructure) # The FitRecipe does the work of calculating the PDF with the fit variable # that we give it. niFit = FitRecipe() # give the PDFContribution to the FitRecipe niFit.addContribution(niPDF) # Configure the fit variables and give them to the recipe. We can use the # srfit function constrainAsSpaceGroup to constrain the lattice and ADP # parameters according to the Fm-3m space group. from diffpy.srfit.structure import constrainAsSpaceGroup spaceGroupParams = constrainAsSpaceGroup(niPDF.nickel.phase, spaceGroup) print "Space group parameters are:", print ', '.join([p.name for p in spaceGroupParams]) print # We can now cycle through the parameters and activate them in the recipe as # variables for par in spaceGroupParams.latpars: niFit.addVar(par) # Set initial value for the ADP parameters, because CIF had no ADP data. for par in spaceGroupParams.adppars: niFit.addVar(par, value=0.005) # As usual, we add variables for the overall scale of the PDF and a delta2 # parameter for correlated motion of neighboring atoms. niFit.addVar(niPDF.scale, 1)
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 = Path("res") fitdir = Path("fit") figdir = Path("fig") folders = [resdir, fitdir, figdir] # Loop over all folders for folder in folders: # If the folder does not exist... if not folder.exists(): # ...then we create it. folder.mkdir() # Let the user know what fit we are running by printing to terminal. basename = FIT_ID print(f"\n{basename}\n") # Establish the full location of the data. data = DPATH / GR_NAME # Establish the location of the cif file with the structure of interest # and load it into a diffpy structure object. strudir = DPATH cif_file = strudir / CIF_NAME # Initialize the Fit Recipe by giving it this diffpy structure # as well as the path to the data file. p_cif = getParser('cif') structure = p_cif.parseFile(str(cif_file)) space_group = p_cif.spacegroup.short_name # Initialize the Fit Recipe by giving it this diffpy structure # as well as the path to the data file. recipe = makerecipe(cif_file, data) # Let's set the calculation range! recipe.crystal.profile.setCalculationRange(xmin=PDF_RMIN, xmax=PDF_RMAX, dx=PDF_RSTEP) # Add, initialize, and tag variables in the Fit Recipe object. # In this case we also add psize, which is the NP size. recipe.addVar(recipe.crystal.s1, SCALE_I, tag="scale") # Set an equation, based on your PDF generators. Here we add an extra layer # of complexity, incorporating "f" int our equation. This new term # incorporates damping to our PDF to model the effect of finite crystallite size. # In this case we use a function which models a spherical NP. from diffpy.srfit.pdf.characteristicfunctions import sphericalCF recipe.crystal.registerFunction(sphericalCF, name="f") recipe.crystal.setEquation("s1*G1*f") recipe.addVar(recipe.crystal.psize, PSIZE_I, tag="psize") # Initialize the instrument parameters, Q_damp and Q_broad, and # assign Q_max and Q_min. # Note, here we do not add the qdamp and qbroad parameters to the fit!!! # They are fixed here, because we refined them in the Ni standard fit! recipe.crystal.G1.qdamp.value = QDAMP_I recipe.crystal.G1.qbroad.value = QBROAD_I recipe.crystal.G1.setQmax(QMAX) recipe.crystal.G1.setQmin(QMIN) # Use the srfit function constrainAsSpaceGroup to constrain # the lattice and ADP parameters according to the Fm-3m space group. from diffpy.srfit.structure import constrainAsSpaceGroup spacegroupparams = constrainAsSpaceGroup(recipe.crystal.G1.phase, space_group) # Add and initialize delta, the lattice parameter, and a thermal parameter, # but not instrumental parameters to Fit Recipe. # The instrumental parameters will remain fixed at values obtained from # the Ni calibrant in our previous example. As we have not added them through # recipe.addVar, they cannot be refined. for par in spacegroupparams.latpars: recipe.addVar(par, value=CUBICLAT_I, name="fcc_Lat", tag="lat") for par in spacegroupparams.adppars: recipe.addVar(par, value=UISO_I, name="fcc_ADP", tag="adp") recipe.addVar(recipe.crystal.G1.delta2, name="Pt_Delta2", value=DELTA2_I, tag="d2") # Tell the Fit Recipe we want to write the maximum amount of # information to the terminal during fitting. recipe.fithooks[0].verbose = 0 refine_params = ["scale", "lat", "psize", "adp", "d2", "all"] recipe.fix("all") for params in refine_params: recipe.free(params) print(f"\n****\nFitting {recipe.getNames()} against " f"{GR_NAME} with {CIF_NAME}\n") least_squares(recipe.residual, recipe.values, x_scale="jac") # 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 / (basename + ".fit")) # We use the FitResults function to parse out the results from # the optimized Fit Recipe. res = FitResults(recipe) # We print these results to the terminal. res.printResults() # We grab the fit Rw rw = res.rw # 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 / (basename + ".res"), header=header) # We use the plotresults function we created earlier to make a plot of # the measured, calculated, and difference curves. We show this # as an interactive window and then write a pdf file to disk. # The file is named based on the basename we created earlier, and # written to the figdir directory. plotresults(recipe, figdir / basename) # Let make a dictionary to hold our results. This way make reloading the # fit parameters easier later refined_dict = dict() refined_dict['rw'] = rw.item() # We loop over the variable names, the variable values, and the variable uncertainties (esd) for name, val, unc in zip(res.varnames, res.varvals, res.varunc): # We store the refined value for this variable using the "value" key. # We use the ".item()" method because "res.varvals" exist as # numpy.float64 objects, and we want them as regular python floats. if name not in refined_dict: refined_dict[name] = dict() refined_dict[name]["value"] = val.item() refined_dict[name]["uncert"] = unc.item() with open(basename + ".yml", 'w') as outfile: yaml.safe_dump(refined_dict, outfile)
totpdf.addProfileGenerator(nucpdf) totpdf.setProfile(profile) totpdf.setEquation("nucscale*nucpdf") # The FitRecipe does the work of calculating the PDF with the fit variables # that we give it. mnofit = FitRecipe() # give the FitContribution to the FitRecipe mnofit.addContribution(totpdf) # Configure the fit variables and give them to the recipe. We can use the # srfit function constrainAsSpaceGroup to constrain the lattice and ADP # parameters according to the CIF-loaded space group. from diffpy.srfit.structure import constrainAsSpaceGroup sgpars = constrainAsSpaceGroup(nucpdf.phase, pcif.spacegroup.short_name) print("Space group parameters are:", end=' ') print(', '.join([p.name for p in sgpars])) print() # We can now cycle through the parameters and activate them in the recipe as # variables for par in sgpars.latpars: mnofit.addVar(par) # Set initial value for the ADP parameters, because CIF had no ADP data. for par in sgpars.adppars: mnofit.addVar(par, value=0.003) # As usual, we add variables for the overall scale of the PDF and a delta2 # parameter for correlated motion of neighboring atoms. mnofit.addVar(totpdf.nucscale, 1)
niStructure = loadStructure(structureFile) niPDF.addStructure("nickel", niStructure) # The FitRecipe does the work of calculating the PDF with the fit variable # that we give it. niFit = FitRecipe() # give the PDFContribution to the FitRecipe niFit.addContribution(niPDF) # Configure the fit variables and give them to the recipe. We can use the # srfit function constrainAsSpaceGroup to constrain the lattice and ADP # parameters according to the Fm-3m space group. from diffpy.srfit.structure import constrainAsSpaceGroup spaceGroupParams = constrainAsSpaceGroup(niPDF.nickel.phase, spaceGroup) print("Space group parameters are:", ', '.join(p.name for p in spaceGroupParams)) print() # We can now cycle through the parameters and activate them in the recipe as # variables for par in spaceGroupParams.latpars: niFit.addVar(par) # Set initial value for the ADP parameters, because CIF had no ADP data. for par in spaceGroupParams.adppars: niFit.addVar(par, value=0.005) # As usual, we add variables for the overall scale of the PDF and a delta2 # parameter for correlated motion of neighboring atoms. niFit.addVar(niPDF.scale, 1)
def make_recipe(self, structure, sg): """ Construct PDF with diffpy. """ # construct a PDFContribution object pdf = PDFContribution("Contribution") # read experimental data try: pdf.loadData(self.input_file) except: print_failure('Failed to parse ' + self.input_file + '. Exiting...') exit() print('Constructing PDF object for', structure.title) pdf.setCalculationRange(self.xmin, self.xmax, self.dx) pdf.addStructure("Contribution", structure) # create FitRecipe to calculate PDF with chosen fit variable fit = FitRecipe() fit.addContribution(pdf) # configure variables and add to recipe if sg != 'xxx' and sg is not None: print(sg) spacegroup_params = constrainAsSpaceGroup(pdf.Contribution.phase, sg) else: cart_lat = abc2cart([[ structure.lattice.a, structure.lattice.b, structure.lattice.c ], [ structure.lattice.alpha, structure.lattice.beta, structure.lattice.gamma ]]) positions_frac = structure.xyz atomic_numbers = [] for atom in structure.element: atomic_numbers.append(get_atomic_number(atom)) cell = (cart_lat, positions_frac, atomic_numbers) sg = int( spg.get_spacegroup(cell, symprec=1e-2).split(' ')[1].replace( '(', '').replace(')', '')) spacegroup_params = constrainAsSpaceGroup(pdf.Contribution.phase, sg) # print('Space group parameters:') # print(', '.join([param.name for param in spacegroup_params])) # iterate through spacegroup params and activate them for param in spacegroup_params.latpars: fit.addVar(param) for param in spacegroup_params.xyzpars: fit.addVar(param, fixed=True) # these next parameters are taken from Martin's PDFht.py, # though I have a feeling that was not their origin... # set initial ADP parameters for param in spacegroup_params.adppars: fit.addVar(param, value=0.03, fixed=True) # overall scale of PDF and delta2 parameter for correlated motion - from PDFht.py fit.addVar(pdf.scale, 1, fixed=True) fit.restrain(pdf.scale, lb=0, ub=0.1, scaled=True) fit.addVar(pdf.Contribution.delta2, 5, fixed=True) fit.restrain(pdf.Contribution.delta2, lb=1, ub=10, scaled=True) # fix Qdamp based on information about "our beamline": yep, no idea fit.addVar(pdf.qdamp, 0.03, fixed=True) fit.restrain(pdf.qdamp, lb=0, ub=0.1, scaled=True) return fit
def makeRecipe(ciffile, datname): """Create a fitting recipe for crystalline PDF data.""" ## The Profile # This will be used to store the observed and calculated PDF profile. profile = Profile() # Load data and add it to the Profile. Unlike in other examples, we use a # class (PDFParser) to help us load the data. This class will read the data # and relevant metadata from a two- to four-column data file generated # with PDFGetX2 or PDFGetN. The metadata will be passed to the PDFGenerator # when they are associated in the FitContribution, which saves some # configuration steps. parser = PDFParser() parser.parseFile(datname) profile.loadParsedData(parser) profile.setCalculationRange(xmax = 20) ## The ProfileGenerator # The PDFGenerator is for configuring and calculating a PDF profile. Here, # we want to refine a Structure object from diffpy.structure. We tell the # PDFGenerator that with the 'setStructure' method. All other configuration # options will be inferred from the metadata that is read by the PDFParser. # In particular, this will set the scattering type (x-ray or neutron), the # Qmax value, as well as initial values for the non-structural Parameters. generator = PDFGenerator("G") stru = Structure() stru.read(ciffile) generator.setStructure(stru) ## The FitContribution # Here we associate the Profile and ProfileGenerator, as has been done # before. contribution = FitContribution("nickel") contribution.addProfileGenerator(generator) contribution.setProfile(profile, xname = "r") ## Make the FitRecipe and add the FitContribution. recipe = FitRecipe() recipe.addContribution(contribution) ## Configure the fit variables # The PDFGenerator class holds the ParameterSet associated with the # Structure passed above in a data member named "phase". (We could have # given the ParameterSet a name other than "phase" when we added it to the # PDFGenerator.) The ParameterSet in this case is a StructureParameterSet, # the documentation for which is found in the # diffpy.srfit.structure.diffpystructure module. phase = generator.phase # We start by constraining the phase to the known space group. We could do # this by hand, but there is a method in diffpy.srfit.structure named # 'constrainAsSpaceGroup' for this purpose. The constraints will by default # be applied to the sites, the lattice and to the ADPs. See the method # documentation for more details. The 'constrainAsSpaceGroup' method may # create new Parameters, which it returns in a SpaceGroupParameters object. from diffpy.srfit.structure import constrainAsSpaceGroup sgpars = constrainAsSpaceGroup(phase, "Fm-3m") # The SpaceGroupParameters object returned by 'constrainAsSpaceGroup' holds # the free Parameters allowed by the space group constraints. Once a # structure is constrained, we need (should) only use the Parameters # provided in the SpaceGroupParameters, as the relevant structure # Parameters are constrained to these. # # We know that the space group does not allow for any free sites because # each atom is on a special position. There is one free (cubic) lattice # parameter and one free (isotropic) ADP. We can access these Parameters in # the xyzpars, latpars, and adppars members of the SpaceGroupParameters # object. for par in sgpars.latpars: recipe.addVar(par) for par in sgpars.adppars: recipe.addVar(par, 0.005) # We now select non-structural parameters to refine. # This controls the scaling of the PDF. recipe.addVar(generator.scale, 1) # This is a peak-damping resolution term. recipe.addVar(generator.qdamp, 0.01) # This is a vibrational correlation term that sharpens peaks at low-r. recipe.addVar(generator.delta2, 5) # Give the recipe away so it can be used! return recipe
# Cover your eyes, but a structure change will now trigger the same # reevaluations as if ordscale were modified. nucpdf.phase.addObserver(totpdf.ordscale.notify) # The FitRecipe does the work of calculating the PDF with the fit variable # that we give it. mnofit = FitRecipe() # give the PDFContribution to the FitRecipe mnofit.addContribution(totpdf) # Configure the fit variables and give them to the recipe. We can use the # srfit function constrainAsSpaceGroup to constrain the lattice and ADP # parameters according to the CIF-loaded space group. from diffpy.srfit.structure import constrainAsSpaceGroup sgpars = constrainAsSpaceGroup(nucpdf.phase, pcif.spacegroup.short_name) print "Space group parameters are:", print ', '.join([p.name for p in sgpars]) print # We can now cycle through the parameters and activate them in the recipe as # variables for par in sgpars.latpars: mnofit.addVar(par) # Set initial value for the ADP parameters, because CIF had no ADP data. for par in sgpars.adppars: mnofit.addVar(par, value=0.003, fixed=True) # As usual, we add variables for the overall scale of the PDF and a delta2 # parameter for correlated motion of neighboring atoms. mnofit.addVar(totpdf.nucscale, 1)
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 = Path("res") fitdir = Path("fit") figdir = Path("fig") folders = [resdir, fitdir, figdir] # Loop over all folders for folder in folders: # If the folder does not exist... if not folder.exists(): # ...then we create it. folder.mkdir() # Let the user know what fit we are running by printing to terminal. basename = FIT_ID print(f"\n{basename}\n") # Establish the full location of the two datasets. xray_data = DPATH / XRAY_GR_NAME nuetron_data = DPATH / NEUTRON_GR_NAME # Establish the location of the cif file with the structure of interest # and load it into a diffpy structure object. strudir = DPATH cif_file = strudir / CIF_NAME p_cif = getParser('cif') structure = p_cif.parseFile(str(cif_file)) space_group = p_cif.spacegroup.short_name # Initialize the Fit Recipe by giving it this diffpy structure # as well as the path to the data file. # Here we use a new function, which takes both datasets. recipe = makerecipe_coref(cif_file, xray_data, nuetron_data) # We first want to add two scale parameters to our fit recipe, # one for each dataset. recipe.addVar(recipe.xray.s1, XRAY_SCALE_I, tag="scale") recipe.addVar(recipe.neutron.s2, NEUTRON_SCALE_I, tag="scale") # Let's set the calculation range! # Here we use a loop to make it easier to edit both ranges. for cont in recipe._contributions.values(): cont.profile.setCalculationRange(xmin=PDF_RMIN, xmax=PDF_RMAX, dx=PDF_RSTEP) # 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 ths information, but in case it didn't, we set it # explicitly again here. # We do it for both neutron and PDF configurations recipe.xray.xray_G.setQmax(XRAY_QMAX) recipe.xray.xray_G.setQmin(XRAY_QMIN) recipe.neutron.neutron_G.setQmax(NEUTRON_QMAX) recipe.neutron.neutron_G.setQmin(NEUTRON_QMAX) # Initialize and add the instrument parameters, Q_damp and Q_broad, and # delta and instrumental parameters to Fit Recipe. # We give them unique names, and tag them with our choice of relevant strings. # Again, two datasets means we need to do this for each. recipe.addVar(recipe.xray.xray_G.delta2, name="Ni_Delta2", value=DELTA2_I, tag="d2") recipe.constrain(recipe.neutron.neutron_G.delta2, "Ni_Delta2") recipe.addVar(recipe.xray.xray_G.qdamp, name="xray_Calib_Qdamp", value=XRAY_QDAMP_I, tag="inst") recipe.addVar(recipe.xray.xray_G.qbroad, name="xray_Calib_Qbroad", value=XRAY_QBROAD_I, tag="inst") recipe.addVar(recipe.neutron.neutron_G.qdamp, name="neutron_Calib_Qdamp", value=NEUTRON_QDAMP_I, tag="inst") recipe.addVar(recipe.neutron.neutron_G.qbroad, name="neutron_Calib_Qbroad", value=NEUTRON_QBROAD_I, tag="inst") # 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 cycle through # the parameters and activate and tag them. # We must explicitly set the ADP parameters, because in this case, the # CIF had no ADP data. from diffpy.srfit.structure import constrainAsSpaceGroup # Create the symmetry distinct parameter sets, and constrain them # in the generator. neutron_spacegroupparams = constrainAsSpaceGroup(recipe.neutron.neutron_G.phase, space_group) xray_spacegroupparams = constrainAsSpaceGroup(recipe.xray.xray_G.phase, space_group) # Loop over all the symmetry distinct lattice parameters and add # them to the recipe. # We give them unique names, and tag them with our choice of a relevant string. for xray_par, neutron_par in zip(xray_spacegroupparams.latpars, neutron_spacegroupparams.latpars): recipe.addVar(xray_par, value=CUBICLAT_I, name="fcc_Lat", tag="lat") recipe.constrain(neutron_par, "fcc_Lat") # Loop over all the symmetry distinct ADPs and add # them to the recipe. # We give them unique names, and tag them with our choice of a relevant string. for xray_par, neutron_par in zip(xray_spacegroupparams.adppars, neutron_spacegroupparams.adppars): recipe.addVar(xray_par, value=UISO_I, name="fcc_ADP", tag="adp") recipe.constrain(neutron_par, "fcc_ADP") # Tell the Fit Recipe we want to write the maximum amount of # information to the terminal during fitting. recipe.fithooks[0].verbose = 3 # During the optimization, we fix and free parameters sequentially # as you would in PDFgui. This leads to more stability in the refinement. # We first fix all variables. "all" is a tag which incorporates # every parameter. recipe.fix("all") # Here will will set the weight of each contribution. In this case, we give each equal weight conts = list(recipe._contributions.values()) for cont in conts: recipe.setWeight(cont, 1.0/len(conts)) # We then run a fit using the SciPy function "least_squares" which # 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 an optional argument # that provides for a bit more stability in the refinement. # "least_squares" is a bit more robust than "leastsq," # which is another optimization function provided by SciPy. # "least_squares" supports bounds on refined parameters, # while "leastsq" does not. refine_params = ["scale", "lat", "adp", "d2", "all"] for params in refine_params: recipe.free(params) print(f"\n****\nFitting {recipe.getNames()} against " f"{XRAY_GR_NAME} and {NEUTRON_GR_NAME} with {CIF_NAME}\n") least_squares(recipe.residual, recipe.values, x_scale="jac") # 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 / (basename + ".fit")) # We use the FitResults function to parse out the results from # the optimized Fit Recipe. res = FitResults(recipe) # We print these results to the terminal. res.printResults() # We grab the fit Rw rw = res.rw # 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 / (basename + ".res"), header=header) # We use the plotresults function we created earlier to make a plot of # the measured, calculated, and difference curves. We show this # as an interactive window and then write a pdf file to disk. # The file is named based on the basename we created earlier, and # written to the figdir directory. plotresults(recipe, figdir / basename) # Let make a dictionary to hold our results. This way make reloading the # fit parameters easier later refined_dict = dict() refined_dict['rw'] = rw.item() recipe.free("all") # We loop over the variable names, the variable values, and the variable uncertainties (esd) for name, val, unc in zip(res.varnames, res.varvals, res.varunc): # We store the refined value for this variable using the "value" key. # We use the ".item()" method because "res.varvals" exist as # numpy.float64 objects, and we want them as regular python floats. if name not in refined_dict: refined_dict[name] = dict() refined_dict[name]["value"] = val.item() refined_dict[name]["uncert"] = unc.item() # Finally, let's write our dictionary to a yaml file! with open(basename + ".yml", 'w') as outfile: yaml.safe_dump(refined_dict, outfile)
# Add the structure from our cif file to the contribution MnOPDF.addStructure("MnO", mnostructure) # The FitRecipe does the work of calculating the PDF with the fit variable # that we give it. MnOFit = FitRecipe() # give the PDFContribution to the FitRecipe MnOFit.addContribution(MnOPDF) # Configure the fit variables and give them to the recipe. We can use the # srfit function constrainAsSpaceGroup to constrain the lattice and ADP # parameters according to the H-3m space group. from diffpy.srfit.structure import constrainAsSpaceGroup spaceGroupParams = constrainAsSpaceGroup(MnOPDF.MnO.phase, spaceGroup) print("Space group parameters are:", end=' ') print(', '.join([p.name for p in spaceGroupParams])) print() # We can now cycle through the parameters and activate them in the recipe as # variables for par in spaceGroupParams.latpars: MnOFit.addVar(par) # Set initial value for the ADP parameters, because CIF had no ADP data. for par in spaceGroupParams.adppars: MnOFit.addVar(par, value=0.003,fixed=True) # As usual, we add variables for the overall scale of the PDF and a delta2 # parameter for correlated motion of neighboring atoms. MnOFit.addVar(MnOPDF.scale, 1)
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 = "res" fitdir = "fit" figdir = "fig" folders = [resdir, fitdir, figdir] # Let's define our working directory. base_dir = Path() yaml_file = base_dir / (FIT_ID_BASE + "refined_params.yml") # This is a bit different than what we've done before. # We are going to look at a set of temperatures, so we want # to find all the relevant data files in the "DPATH" folder # we identified earlier which match a certain pattern. # To do this we will use list comprehension # For every file we find, we will link it up with the data path data_files = list(DPATH.glob(f"*{GR_NAME_BASE}*.gr")) # We now want to grab the temperature at which each file was measured. # We again use list comprehension, and we re-use the variable "temp" # This specific procedure depends on how the file is named. # In our case, we carefully named each file as: # "composition_TTTK.gr" where TTT is the temperature in Kelvin. # First we strip off the directory information for every file in "data_files" # This gives us a list of just full file names, without directories. temps = [f.stem for f in data_files] # Next we split every base filename into a list, delimited by "_" # We keep the second entry from this list, because we know it has the # temperature in the form TTTK. temps = [t.split('_')[1] for t in temps] # We want the temperature as an integer, so we need to drop the "K" # from each string and cast the values as integers using "int()" # Strings can be slides like arrays, so "[:-1]" means "take all the # values except the last one." temps = [int(t[:-1]) for t in temps] # This will sort the data files and temperatures in descending order # based on the temperature values. temps, data_files = zip(*sorted(zip(temps, data_files), reverse=True)) # We want to test two structure models, so we should find both the cif files. # Similar to how we found the data files, we use list comprehension # For every file we find, we will link it up with the data path cif_files = list(DPATH.glob(f"*{CIF_NAME_BASE}*.cif")) # We initialize and empty dictionary, where we will save all # the details of the refined parameters. if yaml_file.exists(): print(f"\n{yaml_file.name} exists, loading!\n") with open(yaml_file, 'r') as infile: refined_dict = yaml.safe_load(infile) else: print(f"\n{yaml_file.name} does not exist, creating!\n") refined_dict = dict() # We want to do a separate temperature series on each of the structures, # so we will use a loop on all the cif files we found. for cif in cif_files: # Let's get the space group, so we can refer to it later. p_cif = getParser('cif') p_cif.parseFile(str(cif)) space_group = p_cif.spacegroup.short_name # Backslashes are bad form, so let's remove them... structure_string = space_group.replace("/", "_on_") # Lets check if we already ran this fit...so we dont duplicate work if structure_string not in refined_dict: print(f"\n{structure_string} IS NOT in dictionary!\n") # Nest a dictionary inside "refined_dict" with a key defined by "structure_string" refined_dict[structure_string] = dict() # This is just for ease of coding/readability sg_dict = refined_dict[structure_string] done = False elif structure_string in refined_dict: print(f"\n{structure_string} IS IN dictionary!\n") sg_dict = refined_dict[structure_string] done = True # Where will we work? here! work_dir = base_dir / structure_string # Make our folders! for folder in folders: new_folder = work_dir / folder if not new_folder.exists(): new_folder.mkdir(parents=True) # Make a recipe based on this cif file, and the first data file in # "data_files" # We can pass in any data file at this point, this is only to initialize # the recipe, and we will replace this data before refining. recipe = makerecipe(cif, data_files[0]) # Let's set the calculation range! recipe.crystal.profile.setCalculationRange(xmin=PDF_RMIN, xmax=PDF_RMAX, dx=PDF_RSTEP) # Initialize the instrument parameters, Q_damp and Q_broad, and # assign Q_max and Q_min. recipe.crystal.G1.qdamp.value = QDAMP_I recipe.crystal.G1.qbroad.value = QBROAD_I recipe.crystal.G1.setQmax(QMAX) recipe.crystal.G1.setQmin(QMIN) # Add, initialize, and tag the scale variable. recipe.addVar(recipe.crystal.s1, SCALE_I, tag="scale") # Use the srfit function constrainAsSpaceGroup to constrain # the lattice and ADP parameters according to the space group setting, # in this case, contained in the function argument "sg" # from diffpy.srfit.structure import constrainAsSpaceGroup spacegroupparams = constrainAsSpaceGroup(recipe.crystal.G1.phase, space_group) for par in spacegroupparams.latpars: recipe.addVar(par, fixed=False, tag="lat") for par in spacegroupparams.adppars: recipe.addVar(par, fixed=False, tag="adp") # Note: here we also can refine atomic coordinates. # In our previous examples, all the atoms were on symmetry # operators, so their positions could not be refined. for par in spacegroupparams.xyzpars: recipe.addVar(par, fixed=False, tag="xyz") # Add delta, but not instrumental parameters to Fit Recipe. recipe.addVar(recipe.crystal.G1.delta2, name="Delta2", value=DELTA1_I, tag="d2") # Tell the Fit Recipe we want to write the maximum amount of # information to the terminal during fitting. recipe.fithooks[0].verbose = 0 # As we are doing a temperature series through a phase transition, we want to fit many # different data sets, each at a different temperature. # for this we will loop over both "temps" and "data_files" for file, temp in zip(data_files, temps): print(f"\nProcessing {file.name}!\n") if temp not in sg_dict: print( f"\nT = {temp} K NOT IN {structure_string} dictionary, creating!\n" ) # Nest a dictionary inside the nested dictionary "refined_dict[stru_type]" # with a key defined by "temp" sg_dict[temp] = dict() temp_dict = sg_dict[temp] done = False elif temp in sg_dict: print(f"\nT = {temp} K IS IN {structure_string} dictionary!\n") temp_dict = sg_dict[temp] done = True # We create a unique string to identify our fit, # using the structure type and the temperature. # This will be used when we write files later. basename = f"{FIT_ID_BASE}{structure_string}_{str(temp)}_K" # Print the basename to the terminal. print(f"\nWorking on {basename}!\n") # We now want to load in the proper dataset for the given temperature. # We do this by creating a new profile object and loading it into the fit recipe. profile = Profile() parser = PDFParser() parser.parseFile(file) profile.loadParsedData(parser) recipe.crystal.setProfile(profile) if not done: print( f"{basename} is NOT DONE with structure {structure_string} at T = {temp} K\n" ) # We are now ready to start the refinement. # During the optimization, fix and free parameters as you would # PDFgui. This leads to more stability in the refinement recipe.fix("all") refine_params = ["scale", "lat", "adp", "d2", "all"] for params in refine_params: recipe.free(params) print(f"\n****\nFitting {recipe.getNames()} against " f"{file.name} with {cif.name}\n") least_squares(recipe.residual, recipe.values, x_scale="jac") elif done: print( f"{basename} IS done with structure {structure_string} at T = {temp} K\n" ) recipe.free("all") print("\nLoading parameters...\n") for var in recipe.getNames(): if var not in temp_dict: print( f"{var} is not in the dictionary!! Let's try to fix it..." ) recipe.fix("all") recipe.free(var) print(f"\nFitting {recipe.getNames()}\n") least_squares(recipe.residual, recipe.values, x_scale="jac") recipe.free("all") elif var in temp_dict: var_dict = temp_dict[var] val = var_dict["value"] recipe.get(var).setValue(val) if not SKIP_DONE: print("\nPolishing...\n") recipe.free("all") print(f"\nFitting {recipe.getNames()}\n") least_squares(recipe.residual, recipe.values, x_scale="jac") print("\nPolishing done\n") if (done and not SKIP_DONE) or not done or REPLOT_DONE: print(f"\nStarting to write results for {basename}" f" with structure {structure_string} at T = {temp} K\n") # Print the fit results to the terminal. res = FitResults(recipe) print("\n******************\n") res.printResults() print("\n******************\n") rw = res.rw # Write the fitted data to a file. profile = recipe.crystal.profile profile.savetxt(work_dir / fitdir / (basename + ".fit")) # Now, as we will use the optimized fit recipe in the next loop, # we want to keep all the refined parameters for this part of the loop # we do this by recording everything in the nested dictionaries we made earlier. # We loop over the variable names, the variable values, and the variable uncertainties (esd) for name, val, unc in zip(res.varnames, res.varvals, res.varunc): # We create a new nested dictionary based on each variable name if name not in temp_dict: temp_dict[name] = dict() var_dict = temp_dict[name] # We store the refined value for this variable using the "value" key. # We use the ".item()" method because "res.varvals" exist as # numpy.float64 objects, and we want them as regular python floats. var_dict["value"] = val.item() var_dict["uncert"] = unc.item() # We also store the fit rw, for posterity. temp_dict['rw'] = rw.item() # Write the fit results to a file. header = "crystal_HF.\n" res.saveResults(work_dir / resdir / (basename + ".res"), header=header) # Write a plot of the fit to a (pdf) file. plotresults(recipe, work_dir / figdir / basename) # plt.ion() # We now write this dictionary to a file for later use. with open(yaml_file, 'w') as outfile: yaml.safe_dump(refined_dict, outfile) # We now write this dictionary to a file for later use. with open(yaml_file, 'w') as outfile: yaml.safe_dump(refined_dict, outfile)