Exemple #1
0
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
Exemple #2
0
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)
Exemple #4
0
# 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)
Exemple #6
0
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)
Exemple #7
0
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)
Exemple #8
0
    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
Exemple #9
0
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)
Exemple #11
0
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)