def makeRecipe(molecule, datname): """Create a recipe that uses the DebyePDFGenerator.""" ## The Profile profile = Profile() # Load data and add it to the profile profile.loadtxt(datname) profile.setCalculationRange(xmin=1.2, xmax=8) ## The ProfileGenerator # Create a DebyePDFGenerator named "G". generator = DebyePDFGenerator("G") generator.setStructure(molecule) # These are metadata needed by the generator generator.setQmin(0.68) generator.setQmax(22) ## The FitContribution contribution = FitContribution("bucky") contribution.addProfileGenerator(generator) contribution.setProfile(profile, xname = "r") # Make a FitRecipe. recipe = FitRecipe() recipe.addContribution(contribution) # Specify which parameters we want to refine. c60 = generator.phase # We're not going to refine the ADPs. However, every atom must have a # small, but finite ADP for the PDF calculator to work properly. atoms = c60.getScatterers() for atom in atoms: atom.Uiso.value = 0.001 # Scale factor. We cannot optimize this efficiently, so we take the value # from a previous refinement. In general, care must be taken to properly # determine the scale of the profile, or to make sure that the residual is # not affected by the scale. generator.scale.value = 1.24457360e+4 # Allow every atom to move. We define the bounds to be a window of radius # 0.1 centered on the current value of the position. win = 0.1 for idx, atom in enumerate(atoms): xname, yname, zname = getXYZNames(idx) recipe.addVar(atom.x, name = xname).boundWindow(win) recipe.addVar(atom.y, name = yname).boundWindow(win) recipe.addVar(atom.z, name = zname).boundWindow(win) return recipe
def makeRecipe(): """Create a recipe that uses the GaussianGenerator. This will create a FitContribution that uses the GaussianGenerator, associate this with a Profile, and use this to define a FitRecipe. """ ## The Profile # Create a Profile to hold the experimental and calculated signal. profile = Profile() # Load data and add it to the profile. This uses the loadtxt function from # numpy. profile.loadtxt("data/gaussian.dat") ## The ProfileGenerator # Create a GaussianGenerator named "g". This will be the name we use to # refer to the generator from within the FitContribution equation. generator = GaussianGenerator("g") ## The FitContribution # Create a FitContribution that will associate the Profile with the # GaussianGenerator. The GaussianGenerator will be accessible as an # attribute of the FitContribution by its name ("g"). Note that this will # set the fitting equation to "g", which calls the GaussianGenerator. contribution = FitContribution("g1") contribution.addProfileGenerator(generator) contribution.setProfile(profile) ## The FitRecipe # Now we create the FitRecipe and add the FitContribution. recipe = FitRecipe() recipe.addContribution(contribution) # Specify which Parameters we want to vary in the fit. This will add # Variables to the FitRecipe that directly modify the Parameters of the # FitContribution. # # We create a variable for each Parameter of the GaussianGenerator. Note # that the Parameters belong to the GaussianGenerator, not the # FitContribution as in gaussianrecipe.py. We initialize parameters as in # gaussianrecipe.py so we can expect the same output. recipe.addVar(generator.A, 1) recipe.addVar(generator.x0, 5) recipe.addVar(generator.sigma, name = "sig") recipe.sig.value = 1 # Give the recipe away so it can be used! return recipe
def makeRecipe(x, y, dy, A, sig, x0): """Make a FitRecipe for fitting a Gaussian curve to data. """ profile = Profile() profile.setObservedProfile(x, y, dy) contribution = FitContribution("g1") contribution.setProfile(profile, xname="x") contribution.setEquation("A * exp(-0.5*(x-x0)**2/sigma**2)") recipe = FitRecipe() recipe.addContribution(contribution) recipe.addVar(contribution.A, A) recipe.addVar(contribution.x0, x0) recipe.addVar(contribution.sigma, sig) return recipe
def makeRecipe(datname): """Create a fitting recipe for ellipsoidal SAS data.""" ## The Profile # This will be used to store the observed and calculated I(Q) data. profile = Profile() # Load data and add it to the Profile. We use a SASParser to load the data # properly and pass the metadata along. parser = SASParser() parser.parseFile(datname) profile.loadParsedData(parser) ## The ProfileGenerator # The SASGenerator is for configuring and calculating a SAS profile. We use # a sans model to configure and serve as the calculation engine of the # generator. This allows us to use the full sans model creation # capabilities, and tie this into SrFit when we want to fit a model to # data. The documentation for the various sans models can be found at # http://danse.chem.utk.edu/sansview.html. from sans.models.EllipsoidModel import EllipsoidModel model = EllipsoidModel() generator = SASGenerator("generator", model) ## The FitContribution # Here we associate the Profile and ProfileGenerator, as has been done # before. contribution = FitContribution("ellipsoid") contribution.addProfileGenerator(generator) contribution.setProfile(profile, xname="q") # We want to fit the log of the signal to the log of the data so that the # higher-Q information remains significant. There are no I(Q) uncertainty # values with the data, so we do not need to worry about the effect this # will have on the estimated parameter uncertainties. contribution.setResidualEquation("log(eq) - log(y)") ## Make the FitRecipe and add the FitContribution. recipe = FitRecipe() recipe.addContribution(contribution) ## Configure the fit variables # The SASGenerator uses the parameters from the params and dispersion # attribues of the model. These vary from model to model, but are adopted # as SrFit Parameters within the generator. Whereas the dispersion # parameters are accessible as, e.g. "radius.width", within the # SASGenerator these are named like "radius_width". # # We want to fit the scale factor, radii and background factors. recipe.addVar(generator.scale, 1) recipe.addVar(generator.radius_a, 50) recipe.addVar(generator.radius_b, 500) recipe.addVar(generator.background, 0) # Give the recipe away so it can be used! return recipe
def __init__(self, name): """Create the PDFContribution. name -- The name of the contribution. """ FitContribution.__init__(self, name) self._meta = {} # Add the profile profile = Profile() self.setProfile(profile, xname = "r") # Need a parameter for the overall scale, in the case that this is a # multi-phase fit. self.newParameter("scale", 1.0) # Profile-related parameters that will be shared between the generators self.newParameter("qdamp", 0) self.newParameter("qbroad", 0) return
def __init__(self, name): """Create the PDFContribution. name -- The name of the contribution. """ FitContribution.__init__(self, name) self._meta = {} # Add the profile profile = Profile() self.setProfile(profile, xname="r") # Need a parameter for the overall scale, in the case that this is a # multi-phase fit. self.newParameter("scale", 1.0) # Profile-related parameters that will be shared between the generators self.newParameter("qdamp", 0) self.newParameter("qbroad", 0) return
def makeRecipe(ciffile, grdata): """Make a recipe to model a crystal-like nanoparticle PDF.""" # Set up a PDF fit as has been done in other examples. pdfprofile = Profile() pdfparser = PDFParser() pdfparser.parseFile(grdata) pdfprofile.loadParsedData(pdfparser) pdfprofile.setCalculationRange(xmin=0.1, xmax=20) pdfcontribution = FitContribution("pdf") pdfcontribution.setProfile(pdfprofile, xname="r") pdfgenerator = PDFGenerator("G") pdfgenerator.setQmax(30.0) stru = CreateCrystalFromCIF(file(ciffile)) pdfgenerator.setStructure(stru) pdfcontribution.addProfileGenerator(pdfgenerator) # Register the nanoparticle shape factor. from diffpy.srfit.pdf.characteristicfunctions import sphericalCF pdfcontribution.registerFunction(sphericalCF, name="f") # Now we set up the fitting equation. pdfcontribution.setEquation("f * G") # Now make the recipe. Make sure we fit the characteristic function shape # parameters, in this case 'psize', which is the diameter of the particle. recipe = FitRecipe() recipe.addContribution(pdfcontribution) phase = pdfgenerator.phase for par in phase.sgpars: recipe.addVar(par) recipe.addVar(pdfcontribution.psize, 20) recipe.addVar(pdfgenerator.scale, 1) recipe.addVar(pdfgenerator.delta2, 0) recipe.B11_0 = 0.1 return recipe
def makeRecipe(datname): """Create a fitting recipe for ellipsoidal SAS data.""" ## The Profile # This will be used to store the observed and calculated I(Q) data. profile = Profile() # Load data and add it to the Profile. We use a SASParser to load the data # properly and pass the metadata along. parser = SASParser() parser.parseFile(datname) profile.loadParsedData(parser) ## The ProfileGenerator # The SASGenerator is for configuring and calculating a SAS profile. We use # a sans model to configure and serve as the calculation engine of the # generator. This allows us to use the full sans model creation # capabilities, and tie this into SrFit when we want to fit a model to # data. The documentation for the various sans models can be found at # http://danse.chem.utk.edu/sansview.html. from sans.models.EllipsoidModel import EllipsoidModel model = EllipsoidModel() generator = SASGenerator("generator", model) ## The FitContribution # Here we associate the Profile and ProfileGenerator, as has been done # before. contribution = FitContribution("ellipsoid") contribution.addProfileGenerator(generator) contribution.setProfile(profile, xname = "q") # We want to fit the log of the signal to the log of the data so that the # higher-Q information remains significant. There are no I(Q) uncertainty # values with the data, so we do not need to worry about the effect this # will have on the estimated parameter uncertainties. contribution.setResidualEquation("log(eq) - log(y)") ## Make the FitRecipe and add the FitContribution. recipe = FitRecipe() recipe.addContribution(contribution) ## Configure the fit variables # The SASGenerator uses the parameters from the params and dispersion # attribues of the model. These vary from model to model, but are adopted # as SrFit Parameters within the generator. Whereas the dispersion # parameters are accessible as, e.g. "radius.width", within the # SASGenerator these are named like "radius_width". # # We want to fit the scale factor, radii and background factors. recipe.addVar(generator.scale, 1) recipe.addVar(generator.radius_a, 50) recipe.addVar(generator.radius_b, 500) recipe.addVar(generator.background, 0) # Give the recipe away so it can be used! return recipe
def main(): p = Profile() p.loadtxt("data/gaussian.dat") # FitContribution operations # "<<" - Inject a parameter value c = FitContribution("g1") c.setProfile(p) c.setEquation("A * exp(-0.5*(x-x0)**2/sigma**2)") c.A << 0.5 c.x0 << 5 c.sigma << 1 # FitRecipe operations # "|=" - Union of necessary components. # "+=" - Add Parameter or create a new one. Each tuple is a set of # arguments for either setVar or addVar. # "*=" - Constrain a parameter. Think of "*" as a push-pin holding one # parameter's value to that of another. # "%=" - Restrain a parameter or equation. Think of "%" as a rope # loosely tying parameters to a value. r = FitRecipe() r |= c r += (c.A, 0.5), (c.x0, 5), 'sig' r *= c.sigma, 'sig' r %= c.A, 0.5, 0.5 from gaussianrecipe import scipyOptimize scipyOptimize(r) res = FitResults(r) # Print the results. res.printResults() # Plot the results. from gaussianrecipe import plotResults plotResults(r) return
def test___hash__(self): """check WeakBoundMethod.__hash__() """ f1 = FitContribution('f1') w1 = weak_ref(f1._flush) h0 = hash(w1) del f1 self.assertTrue(None is w1._wref()) self.assertEqual(h0, hash(w1)) w1c1 = pickle.loads(pickle.dumps(w1)) w1c2 = pickle.loads(pickle.dumps(w1)) self.assertEqual(hash(w1c1), hash(w1c2)) return
def makeRecipe(ciffile, grdata): """Make a recipe to model a crystal-like nanoparticle PDF.""" # Set up a PDF fit as has been done in other examples. pdfprofile = Profile() pdfparser = PDFParser() pdfparser.parseFile(grdata) pdfprofile.loadParsedData(pdfparser) pdfprofile.setCalculationRange(xmin = 0.1, xmax = 20) pdfcontribution = FitContribution("pdf") pdfcontribution.setProfile(pdfprofile, xname = "r") pdfgenerator = PDFGenerator("G") pdfgenerator.setQmax(30.0) stru = CreateCrystalFromCIF(file(ciffile)) pdfgenerator.setStructure(stru) pdfcontribution.addProfileGenerator(pdfgenerator) # Register the nanoparticle shape factor. from diffpy.srfit.pdf.characteristicfunctions import sphericalCF pdfcontribution.registerFunction(sphericalCF, name = "f") # Now we set up the fitting equation. pdfcontribution.setEquation("f * G") # Now make the recipe. Make sure we fit the characteristic function shape # parameters, in this case 'psize', which is the diameter of the particle. recipe = FitRecipe() recipe.addContribution(pdfcontribution) phase = pdfgenerator.phase for par in phase.sgpars: recipe.addVar(par) recipe.addVar(pdfcontribution.psize, 20) recipe.addVar(pdfgenerator.scale, 1) recipe.addVar(pdfgenerator.delta2, 0) recipe.B11_0 = 0.1 return recipe
def _makeRecipe(self, x, y, dy): '''Make a FitRecipe for fitting a Gaussian curve to data. ''' profile = Profile() profile.setObservedProfile(x, y, dy) contribution = FitContribution("g1") contribution.setProfile(profile, xname="x") contribution.registerStringFunction('1/sqrt(2 * pi * sig**2)', name='gaussnorm') contribution.setEquation( "A * gaussnorm * exp(-0.5 * (x - x0)**2/sig**2)") recipe = FitRecipe() recipe.addContribution(contribution) recipe.addVar(contribution.A) recipe.addVar(contribution.x0) recipe.addVar(contribution.sig) recipe.clearFitHooks() self.recipe = recipe return
def makeRecipe(molecule, datname): """Create a recipe that uses the DebyePDFGenerator.""" ## The Profile profile = Profile() # Load data and add it to the profile profile.loadtxt(datname) profile.setCalculationRange(xmin=1.2, xmax=8) ## The ProfileGenerator # Create a DebyePDFGenerator named "G". generator = DebyePDFGenerator("G") generator.setStructure(molecule) # These are metadata needed by the generator generator.setQmin(0.68) generator.setQmax(22) ## The FitContribution contribution = FitContribution("bucky") contribution.addProfileGenerator(generator) contribution.setProfile(profile, xname="r") # Make a FitRecipe. recipe = FitRecipe() recipe.addContribution(contribution) # Specify which parameters we want to refine. c60 = generator.phase # We're not going to refine the ADPs. However, every atom must have a # small, but finite ADP for the PDF calculator to work properly. atoms = c60.getScatterers() for atom in atoms: atom.Uiso.value = 0.001 # Scale factor. We cannot optimize this efficiently, so we take the value # from a previous refinement. In general, care must be taken to properly # determine the scale of the profile, or to make sure that the residual is # not affected by the scale. generator.scale.value = 1.24457360e+4 # Allow every atom to move. We define the bounds to be a window of radius # 0.1 centered on the current value of the position. win = 0.1 for idx, atom in enumerate(atoms): xname, yname, zname = getXYZNames(idx) recipe.addVar(atom.x, name=xname).boundWindow(win) recipe.addVar(atom.y, name=yname).boundWindow(win) recipe.addVar(atom.z, name=zname).boundWindow(win) return recipe
def test___eq__(self): """check WeakBoundMethod.__eq__() """ f1 = FitContribution('f1') w1 = weak_ref(f1._flush) w2 = weak_ref(f1._flush) self.assertEqual(w1, w2) w1c = pickle.loads(pickle.dumps(w1)) # pickle-copied objects should have empty reference self.assertTrue(None is w1c._wref()) self.assertNotEqual(w1, w1c) del f1 self.assertTrue(None is w1._wref()) self.assertEqual(w1, w1c) w1cc = pickle.loads(pickle.dumps(w1c)) self.assertTrue(None is w1cc._wref()) self.assertEqual(w1c, w1cc) self.assertEqual(w1, w1cc) return
def makeRecipe(): """Create a recipe that uses the GaussianGenerator. This will create a FitContribution that uses the GaussianGenerator, associate this with a Profile, and use this to define a FitRecipe. """ ## The Profile # Create a Profile to hold the experimental and calculated signal. profile = Profile() # Load data and add it to the profile. This uses the loadtxt function from # numpy. profile.loadtxt("data/gaussian.dat") ## The ProfileGenerator # Create a GaussianGenerator named "g". This will be the name we use to # refer to the generator from within the FitContribution equation. generator = GaussianGenerator("g") ## The FitContribution # Create a FitContribution that will associate the Profile with the # GaussianGenerator. The GaussianGenerator will be accessible as an # attribute of the FitContribution by its name ("g"). Note that this will # set the fitting equation to "g", which calls the GaussianGenerator. contribution = FitContribution("g1") contribution.addProfileGenerator(generator) contribution.setProfile(profile) ## The FitRecipe # Now we create the FitRecipe and add the FitContribution. recipe = FitRecipe() recipe.addContribution(contribution) # Specify which Parameters we want to vary in the fit. This will add # Variables to the FitRecipe that directly modify the Parameters of the # FitContribution. # # We create a variable for each Parameter of the GaussianGenerator. Note # that the Parameters belong to the GaussianGenerator, not the # FitContribution as in gaussianrecipe.py. We initialize parameters as in # gaussianrecipe.py so we can expect the same output. recipe.addVar(generator.A, 1) recipe.addVar(generator.x0, 5) recipe.addVar(generator.sigma, name="sig") recipe.sig.value = 1 # Give the recipe away so it can be used! return recipe
def main(): p = Profile() p.loadtxt("data/gaussian.dat") # FitContribution operations # "|=" - Union of necessary components. # "<<" - Inject a parameter value c = FitContribution("g1") c |= p c |= "A * exp(-0.5*(x-x0)**2/sigma**2)" c.A << 0.5 c.x0 << 5 c.sigma << 1 # FitRecipe operations # "|=" - Union of necessary components. # "+=" - Add Parameter or create a new one. Each tuple is a set of # arguments for either setVar or addVar. # "*=" - Constrain a parameter. Think of "*" as a push-pin holding one # parameter's value to that of another. # "%=" - Restrain a parameter or equation. Think of "%" as a rope # loosely tying parameters to a value. r = FitRecipe() r |= c r += (c.A, 0.5), (c.x0, 5), 'sig' r *= c.sigma, 'sig' r %= c.A, 0.5, 0.5 from gaussianrecipe import scipyOptimize scipyOptimize(r) res = FitResults(r) # Print the results. res.printResults() # Plot the results. from gaussianrecipe import plotResults plotResults(r) return
def _makeRecipe(self, x, y, dy): '''Make a FitRecipe for fitting a Gaussian curve to data. ''' profile = Profile() profile.setObservedProfile(x, y, dy) contribution = FitContribution("g1") contribution.setProfile(profile, xname="x") contribution.registerStringFunction( '1/sqrt(2 * pi * sig**2)', name='gaussnorm') contribution.setEquation( "A * gaussnorm * exp(-0.5 * (x - x0)**2/sig**2)") recipe = FitRecipe() recipe.addContribution(contribution) recipe.addVar(contribution.A) recipe.addVar(contribution.x0) recipe.addVar(contribution.sig) recipe.clearFitHooks() self.recipe = recipe return
def makeRecipe(niciffile, siciffile, datname): """Create a fitting recipe for crystalline PDF data.""" ## The Profile profile = Profile() # Load data and add it to the profile parser = PDFParser() parser.parseFile(datname) profile.loadParsedData(parser) profile.setCalculationRange(xmax = 20) ## The ProfileGenerator # In order to fit two phases simultaneously, we must use two PDFGenerators. # PDFGenerator is designed to take care of as little information as it # must. (Don't do too much, and do it well.) A PDFGenerator can generate # the signal from only a single phase at a time. So, we will create one # PDFGenerator for each phase and compose them within the same # FitContribution. Note that both generators will be associated with the # same Profile within the FitContribution, so they will both be # automatically configured according to the metadata. # # The generator for the nickel phase. We call it "G_ni" and will use this # name later when we set the fitting equation in the FitContribution. generator_ni = PDFGenerator("G_ni") stru = CreateCrystalFromCIF(file(niciffile)) generator_ni.setStructure(stru) # The generator for the silicon phase. We call it "G_si". generator_si = PDFGenerator("G_si") stru = CreateCrystalFromCIF(file(siciffile)) generator_si.setStructure(stru) ## The FitContribution # Add both generators to the FitContribution. Add the Profile. This will # send the metadata to the generators. contribution = FitContribution("nisi") contribution.addProfileGenerator(generator_ni) contribution.addProfileGenerator(generator_si) contribution.setProfile(profile, xname = "r") # Write the fitting equation. We want to sum the PDFs from each phase and # multiply it by a scaling factor. We also want a certain phase scaling # relationship between the PDFs which we will enforce with constraints in # the FitRecipe. contribution.setEquation("scale * (G_ni + G_si)") # Make the FitRecipe and add the FitContribution. recipe = FitRecipe() recipe.addContribution(contribution) ## Configure the fit variables # Start by configuring the scale factor and resolution factors. # We want the sum of the phase scale factors to be 1. recipe.newVar("scale_ni", 0.1) recipe.constrain(generator_ni.scale, "scale_ni") recipe.constrain(generator_si.scale, "1 - scale_ni") # We also want the resolution factor to be the same on each. recipe.newVar("qdamp", 0.03) recipe.constrain(generator_ni.qdamp, "qdamp") recipe.constrain(generator_si.qdamp, "qdamp") # Vary the gloabal scale as well. recipe.addVar(contribution.scale, 1) # Now we can configure the structural parameters. Since we're using # ObjCrystCrystalParSets, the space group constraints are automatically # applied to each phase. We must selectively vary the free parameters. # # First the nickel parameters phase_ni = generator_ni.phase for par in phase_ni.sgpars: recipe.addVar(par, name = par.name + "_ni") recipe.addVar(generator_ni.delta2, name = "delta2_ni") # Next the silicon parameters phase_si = generator_si.phase for par in phase_si.sgpars: recipe.addVar(par, name = par.name + "_si") recipe.addVar(generator_si.delta2, name = "delta2_si") # We have prior information from the earlier examples so we'll use it here # in the form of restraints. # # The nickel lattice parameter was measured to be 3.527. The uncertainty # values are invalid for that measurement, since the data from which it is # derived has no uncertainty. Thus, we will tell the recipe to scale the # residual, which means that it will be weighted as much as the average # data point during the fit. recipe.restrain("a_ni", lb = 3.527, ub = 3.527, scaled = True) # Now we do the same with the delta2 and Biso parameters (remember that # Biso = 8*pi**2*Uiso) recipe.restrain("delta2_ni", lb = 2.22, ub = 2.22, scaled = True) recipe.restrain("Biso_0_ni", lb = 0.454, ub = 0.454, scaled = True) # # We can do the same with the silicon values. We haven't done a thorough # job of measuring the uncertainties in the results, so we'll scale these # as well. recipe.restrain("a_si", lb = 5.430, ub = 5.430, scaled = True) recipe.restrain("delta2_si", lb = 3.54, ub = 3.54, scaled = True) recipe.restrain("Biso_0_si", lb = 0.645, ub = 0.645, scaled = True) # Give the recipe away so it can be used! return recipe
def makeRecipe(ciffile, xdatname, ndatname): """Create a fitting recipe for crystalline PDF data.""" ## The Profiles # We need a profile for each data set. This means that we will need two # FitContributions as well. xprofile = Profile() nprofile = Profile() # Load data and add it to the proper Profile. parser = PDFParser() parser.parseFile(xdatname) xprofile.loadParsedData(parser) xprofile.setCalculationRange(xmax = 20) parser = PDFParser() parser.parseFile(ndatname) nprofile.loadParsedData(parser) nprofile.setCalculationRange(xmax = 20) ## The ProfileGenerators # We need one of these for the x-ray data. xgenerator = PDFGenerator("G") stru = loadCrystal(ciffile) xgenerator.setStructure(stru) # And we need one for the neutron data. We want to refine the same # structure object in each PDFGenerator. This would suggest that we add the # same Crystal to each. However, if we do that then we will have two # Parameters for each Crystal data member (two Parameters for the "a" # lattice parameter, etc.), held in different ObjCrystCrystalParSets, each # managed by its own PDFGenerator. Thus, changes made to the Crystal # through one PDFGenerator will not be known to the other PDFGenerator # since their ObjCrystCrystalParSets don't know about each other. The # solution is to share ObjCrystCrystalParSets rather than Crystals. This # way there is only one Parameter for each Crystal data member. (An # alternative to this is to constrain each structure Parameter to be varied # to the same variable. The present approach is easier and less error # prone.) # # Tell the neutron PDFGenerator to use the phase from the x-ray # PDFGenerator. ngenerator = PDFGenerator("G") ngenerator.setPhase(xgenerator.phase) ## The FitContributions # We associate the x-ray PDFGenerator and Profile in one FitContribution... xcontribution = FitContribution("xnickel") xcontribution.addProfileGenerator(xgenerator) xcontribution.setProfile(xprofile, xname = "r") # and the neutron objects in another. ncontribution = FitContribution("nnickel") ncontribution.addProfileGenerator(ngenerator) ncontribution.setProfile(nprofile, xname = "r") # This example is different than the previous ones in that we are composing # a residual function from other residuals (one for the x-ray contribution # and one for the neutron contribution). The relative magnitude of these # residuals effectively determines the influence of each contribution over # the fit. This is a problem in this case because the x-ray data has # uncertainty values associated with it (on the order of 1e-4), and the # chi^2 residual is proportional to 1 / uncertainty**2. The neutron has no # uncertainty, so it's chi^2 is proportional to 1. Thus, my optimizing # chi^2 we would give the neutron data practically no weight in the fit. To # get around this, we will optimize a different metric. # # The contribution's residual can be either chi^2, Rw^2, or custom crafted. # In this case, we should minimize Rw^2 of each contribution so that each # one can contribute roughly equally to the fit. xcontribution.setResidualEquation("resv") ncontribution.setResidualEquation("resv") # Make the FitRecipe and add the FitContributions. recipe = FitRecipe() recipe.addContribution(xcontribution) recipe.addContribution(ncontribution) # Now we vary and constrain Parameters as before. recipe.addVar(xgenerator.scale, 1, "xscale") recipe.addVar(ngenerator.scale, 1, "nscale") recipe.addVar(xgenerator.qdamp, 0.01, "xqdamp") recipe.addVar(ngenerator.qdamp, 0.01, "nqdamp") # delta2 is a non-structual material propery. Thus, we constrain together # delta2 Parameter from each PDFGenerator. delta2 = recipe.newVar("delta2", 2) recipe.constrain(xgenerator.delta2, delta2) recipe.constrain(ngenerator.delta2, delta2) # We only need to constrain phase properties once since there is a single # ObjCrystCrystalParSet for the Crystal. phase = xgenerator.phase for par in phase.sgpars: recipe.addVar(par) recipe.B11_0 = 0.1 # Give the recipe away so it can be used! return recipe
class TestWeakBoundMethod(unittest.TestCase): def setUp(self): self.f = FitContribution('f') self.f.setEquation('7') self.w = weak_ref(self.f._eq._flush, fallback=_fallback_example) return def tearDown(self): self.f = None self.assertTrue(None is self.w._wref()) obj, args, kw = self.w('any', 'argument', foo=37) self.assertTrue(obj is self.w) self.assertEqual(('any', 'argument'), args) self.assertEqual({'foo' : 37}, kw) return def test___init__(self): """check WeakBoundMethod.__init__() """ self.assertTrue(self.w.fallback is _fallback_example) wf = weak_ref(self.f._flush) self.assertTrue(None is wf.fallback) return def test___call__(self): """check WeakBoundMethod.__call__() """ f = self.f self.assertEqual(7, f.evaluate()) self.assertEqual(7, f._eq._value) # verify f has the same effect as f._eq._flush self.w(()) self.assertTrue(None is f._eq._value) # check WeakBoundMethod behavior with no fallback x = Parameter('x', value=3) wgetx = weak_ref(x.getValue) self.assertEqual(3, wgetx()) del x self.assertRaises(ReferenceError, wgetx) return def test___hash__(self): """check WeakBoundMethod.__hash__() """ f1 = FitContribution('f1') w1 = weak_ref(f1._flush) h0 = hash(w1) del f1 self.assertTrue(None is w1._wref()) self.assertEqual(h0, hash(w1)) w1c1 = pickle.loads(pickle.dumps(w1)) w1c2 = pickle.loads(pickle.dumps(w1)) self.assertEqual(hash(w1c1), hash(w1c2)) return def test___eq__(self): """check WeakBoundMethod.__eq__() """ f1 = FitContribution('f1') w1 = weak_ref(f1._flush) w2 = weak_ref(f1._flush) self.assertEqual(w1, w2) w1c = pickle.loads(pickle.dumps(w1)) # pickle-copied objects should have empty reference self.assertTrue(None is w1c._wref()) self.assertNotEqual(w1, w1c) del f1 self.assertTrue(None is w1._wref()) self.assertEqual(w1, w1c) w1cc = pickle.loads(pickle.dumps(w1c)) self.assertTrue(None is w1cc._wref()) self.assertEqual(w1c, w1cc) self.assertEqual(w1, w1cc) return def test_pickling(self): """Verify unpickling works when it involves __hash__ call. """ holder = set([self.w]) objs = [holder, self.f._eq, self.w] data = pickle.dumps(objs) objs2 = pickle.loads(data) h2, feq2, w2 = objs2 self.assertTrue(w2 in h2) self.assertTrue(feq2 is w2._wref()) return def test_observable_deregistration(self): """check if Observable drops dead Observer. """ f = self.f x = f.newParameter('x', 5) f.setEquation('3 * x') self.assertEqual(15, f.evaluate()) self.assertEqual(15, f._eq._value) # get one of the observer callables that are associated with f xof = next(iter(x._observers)) self.assertTrue(isinstance(xof, WeakBoundMethod)) # changing value of x should reset f._eq x.setValue(x.value + 1) self.assertTrue(None is f._eq._value) self.assertEqual(18, f.evaluate()) # deallocate f now self.f = f = None self.assertTrue(xof in x._observers) # since f does not exist anymore, the next notification call # should drop the associated observer. x.setValue(x.value + 1) self.assertEqual(0, len(x._observers)) return
plt.show() # <demo> --- stop --- # We are going to define a line fitting regression using SrFit. # At first we create a SrFit Profile object that holds the observed data. from diffpy.srfit.fitbase import Profile profile = Profile() profile.setObservedProfile(xobs, yobs) # The second step is to create a FitContribution object, which associates # observed profile with a mathematical model for the dependent variable. from diffpy.srfit.fitbase import FitContribution large_gaussian = FitContribution("g1") large_gaussian.setProfile(profile, xname="x") large_gaussian.registerStringFunction('1/sqrt(2 * pi * lgsig**2)', name='gaussnorm') large_gaussian.setEquation( "lgA * gaussnorm * exp(-0.5 * (x - lgx0)**2/lgsig**2)") small_gaussian = FitContribution("g2") small_gaussian.setProfile(profile, xname="x") small_gaussian.registerStringFunction('1/sqrt(2 * pi * sgsig**2)', name='gaussnorm') small_gaussian.setEquation( "sgA * gaussnorm * exp(-0.5 * (x - sgx0)**2/sgsig**2)") # SrFit objects can be examined by calling their show() function. SrFit # parses the model equation and finds two parameters A, B at independent
def makeRecipe(): """Make a FitRecipe for fitting three double-gaussian curves to data. The separation and amplitude ratio of the double peaks follows a specific relationship. The peaks are broadend according to their position and they sit on top of a background. We are seeking the absolute locations of the peaks as well as their amplitudes. The independent variable is t. The relationship between the double peaks is sin(t2) / l2 = sin(t1) / l1 amplitude(peak2) = r * amplitude(peak1) The values of l1, l2 and r come from experiment. For this example, we use l1 = 1.012, l2 = 1.0 and r = 0.23. """ ## The Profile # Create a Profile to hold the experimental and calculated signal. profile = Profile() x, y, dy = profile.loadtxt("data/threedoublepeaks.dat") # Create the contribution contribution = FitContribution("peaks") contribution.setProfile(profile, xname = "t") pi = numpy.pi exp = numpy.exp # This is a building-block of our profile function def gaussian(t, mu, sig): return 1/(2*pi*sig**2)**0.5 * exp(-0.5 * ((t-mu)/sig)**2) contribution.registerFunction(gaussian, name = "peakshape") def delta(t, mu): """Calculate a delta-function. We don't have perfect precision, so we must make this a very thin Gaussian. """ sig = t[1] - t[0] return gaussian(t, mu, sig) contribution.registerFunction(delta) # Here is another one bkgdstr = "b0 + b1*t + b2*t**2 + b3*t**3 + b4*t**4 + b5*t**5 + b6*t**6" contribution.registerStringFunction(bkgdstr, "bkgd") # Now define our fitting equation. We will hardcode the peak ratios. contribution.setEquation( "A1 * ( convolve( delta(t, mu11), peakshape(t, c, sig11) ) \ + 0.23*convolve( delta(t, mu12), peakshape(t, c, sig12) ) ) + \ A2 * ( convolve( delta(t, mu21), peakshape(t, c, sig21) ) \ + 0.23*convolve( delta(t, mu22), peakshape(t, c, sig22) ) ) + \ A3 * ( convolve( delta(t, mu31), peakshape(t, c, sig31) ) \ + 0.23*convolve( delta(t, mu32), peakshape(t, c, sig32) ) ) + \ bkgd") # c is the center of the gaussian. contribution.c.value = x[len(x)/2] ## The FitRecipe # The FitRecipe lets us define what we want to fit. It is where we can # create variables, constraints and restraints. recipe = FitRecipe() # Here we tell the FitRecipe to use our FitContribution. When the FitRecipe # calculates its residual function, it will call on the FitContribution to # do part of the work. recipe.addContribution(contribution) # Vary the amplitudes for each double peak recipe.addVar(contribution.A1, 100) recipe.addVar(contribution.A2, 100) recipe.addVar(contribution.A3, 100) # Vary the position of the first of the double peaks recipe.addVar(contribution.mu11, 13.0) recipe.addVar(contribution.mu21, 24.0) recipe.addVar(contribution.mu31, 33.0) # Constrain the position of the second double peak from numpy import sin, arcsin def peakloc(mu): """Calculate the location of the second peak given the first.""" l1 = 1.012 l2 = 1.0 return 180 / pi * arcsin( pi / 180 * l2 * sin(mu) / l1 ) recipe.registerFunction(peakloc) recipe.constrain(contribution.mu12, "peakloc(mu11)") recipe.constrain(contribution.mu22, "peakloc(mu21)") recipe.constrain(contribution.mu32, "peakloc(mu31)") # Vary the width of the peaks. We know the functional form of the peak # broadening. sig0 = recipe.newVar("sig0", 0.001) dsig = recipe.newVar("dsig", 4) def sig(sig0, dsig, mu): """Calculate the peak broadening with respect to position.""" return sig0 * (1 - dsig * mu**2); recipe.registerFunction(sig) recipe.fix("mu") # Now constrain the peak widths to this recipe.sig0.value = 0.001 recipe.dsig.value = 4.0 recipe.constrain(contribution.sig11, "sig(sig0, dsig, mu11)") recipe.constrain(contribution.sig12, "sig(sig0, dsig, mu12)", ns = {"mu12" : contribution.mu12} ) recipe.constrain(contribution.sig21, "sig(sig0, dsig, mu21)") recipe.constrain(contribution.sig22, "sig(sig0, dsig, mu22)", ns = {"mu22" : contribution.mu22} ) recipe.constrain(contribution.sig31, "sig(sig0, dsig, mu31)") recipe.constrain(contribution.sig32, "sig(sig0, dsig, mu32)", ns = {"mu32" : contribution.mu32} ) # Also the background recipe.addVar(contribution.b0, 0, tag = "bkgd") recipe.addVar(contribution.b1, 0, tag = "bkgd") recipe.addVar(contribution.b2, 0, tag = "bkgd") recipe.addVar(contribution.b3, 0, tag = "bkgd") recipe.addVar(contribution.b4, 0, tag = "bkgd") recipe.addVar(contribution.b5, 0, tag = "bkgd") recipe.addVar(contribution.b6, 0, tag = "bkgd") return recipe
def setUp(self): self.f = FitContribution('f') self.holder = set() self.w = weak_ref(self.f._flush, holder=self.holder) self.holder.add(self.w) return
plt.show() # <demo> --- stop --- # We are going to define a line fitting regression using SrFit. # At first we create a SrFit Profile object that holds the observed data. from diffpy.srfit.fitbase import Profile linedata = Profile() linedata.setObservedProfile(xobs, yobs, dyobs) # The second step is to create a FitContribution object, which associates # observed profile with a mathematical model for the dependent variable. from diffpy.srfit.fitbase import FitContribution linefit = FitContribution('linefit') linefit.setProfile(linedata) linefit.setEquation("A * x + B") # SrFit objects can be examined by calling their show() function. SrFit # parses the model equation and finds two parameters A, B at independent # variable x. The values of parameters A, B are at this stage undefined. linefit.show() # <demo> --- stop --- # We can set A and B to some specific values and calculate model # observations. The x and y attributes of the FitContribution are # the observed values, which may be re-sampled or truncated to a shorter # fitting range.
def makeRecipe(molecule, datname): """Create a recipe that uses the DebyePDFGenerator.""" ## The Profile profile = Profile() # Load data and add it to the profile profile.loadtxt(datname) profile.setCalculationRange(xmin=1.2, xmax=8) ## The ProfileGenerator # Create a DebyePDFGenerator named "G". generator = DebyePDFGenerator("G") generator.setStructure(molecule) # These are metadata needed by the generator generator.setQmin(0.68) generator.setQmax(22) ## The FitContribution contribution = FitContribution("bucky") contribution.addProfileGenerator(generator) contribution.setProfile(profile, xname = "r") # Make a FitRecipe. recipe = FitRecipe() recipe.addContribution(contribution) # Specify which parameters we want to refine. We'll be using the # MoleculeParSet within the generator, so let's get a handle to it. See the # diffpy.srfit.structure.objcryststructure module for more information # about the MoleculeParSet hierarchy. c60 = generator.phase # First, the isotropic thermal displacement factor. Biso = recipe.newVar("Biso") for atom in c60.getScatterers(): # We have defined a 'center' atom that is a dummy, which means that it # has no scattering power. It is only used as a reference point for # our bond length. We don't want to constrain it. if not atom.isDummy(): recipe.constrain(atom.Biso, Biso) # We need to let the molecule expand. If we were modeling it as a crystal, # we could let the unit cell expand. For instruction purposes, we use a # Molecule to model C60, and molecules have different modeling options than # crystals. To make the molecule expand from a central point, we will # constrain the distance from each atom to a dummy center atom that was # created with the molecule, and allow that distance to vary. (We could # also let the nearest-neighbor bond lengths vary, but that would be much # more difficult to set up.) center = c60.center # Create a new Parameter that represents the radius of the molecule. Note # that we don't give it an initial value. Since the variable is being # directly constrained to further below, its initial value will be inferred # from the constraint. radius = recipe.newVar("radius") for i, atom in enumerate(c60.getScatterers()): if atom.isDummy(): continue # This creates a Parameter that moves the second atom according to the # bond length. Note that each Parameter needs a unique name. par = c60.addBondLengthParameter("rad%i"%i, center, atom) recipe.constrain(par, radius) # Add the correlation term, scale. The scale is too short to effectively # determine qdamp. recipe.addVar(generator.delta2, 2) recipe.addVar(generator.scale, 1.3e4) # Give the recipe away so it can be used! return recipe
def makeRecipe(): """Make a FitRecipe for fitting a Gaussian curve to data. The instructions for what we want to refine, and how to refine it will be defined within a FitRecipe instance. The job of a FitRecipe is to collect and associate all the data, the fitting equations, fitting variables, constraints and restraints. The configured recipe provides a 'residual' function and the initial variable values that an optimizer can use to refine the variables to minimize the disagreement between the calculated profile and the data. Once we define the FitRecipe, we can send it an optimizer to be optimized. See the 'scipyOptimize' function. """ ## The Profile # Create a Profile to hold the experimental and calculated signal. profile = Profile() # Load data and add it to the profile. This uses the loadtxt function from # numpy. profile.loadtxt("data/gaussian.dat") ## The FitContribution # The FitContribution associates the Profile with a fitting equation. The # FitContribution also stores the parameters of the fitting equation. We # give our FitContribution then name "g1". We will be able to access the # FitContribution by that name within the FitRecipe. contribution = FitContribution("g1") # Tell the FitContribution about the Profile. The FitContribution will give # us access to the data held within the Profile. Here, we can tell it what # name we want to use for the independent variable. We tell it to use the # name "x". contribution.setProfile(profile, xname="x") # Now we need to create a fitting equation. We do that by writing out the # equation as a string. The FitContribution will turn this into a callable # function internally. In the process, it extracts all the parameters from # the equation (A, x, x0, sigma) and turns them into Parameter objects # internally. These objects can be accessed as attributes of the # contribution by name. Since we told the contribution that our # independent variable is named "x", this value will be substituted into # the fitting equation whenever it is called. contribution.setEquation("A * exp(-0.5*(x-x0)**2/sigma**2)") # To demonstrate how these parameters are used, we will give "A" an initial # value. Note that Parameters are not numbers, but are containers for # numbers. To get or modify the value of a parameter, use its 'value' # attribute. Parameters also have a 'name' attribute. contribution.A.value = 1.0 ## The FitRecipe # The FitRecipe lets us define what we want to fit. It is where we can # create variables, constraints and restraints. recipe = FitRecipe() # Here we tell the FitRecipe to use our FitContribution. When the FitRecipe # calculates its residual function, it will call on the FitContribution to # do part of the work. recipe.addContribution(contribution) # Specify which Parameters we want to vary in the fit. This will add # Variables to the FitRecipe that directly modify the Parameters of the # FitContribution. # # Here we create a Variable for the 'A' Parameter from our fit equation. # The resulting Variable will be named 'A' as well, but it will be accessed # via the FitRecipe. recipe.addVar(contribution.A) # Here we create the Variable for 'x0' and give it an initial value of 5. recipe.addVar(contribution.x0, 5) # Here we create a Variable named 'sig', which is tied to the 'sigma' # Parameter of our FitContribution. We give it an initial value through the # FitRecipe instance. recipe.addVar(contribution.sigma, name="sig") recipe.sig.value = 1 return recipe
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
def makeRecipe(niciffile, siciffile, datname): """Create a fitting recipe for crystalline PDF data.""" ## The Profile profile = Profile() # Load data and add it to the profile parser = PDFParser() parser.parseFile(datname) profile.loadParsedData(parser) profile.setCalculationRange(xmax=20) ## The ProfileGenerator # In order to fit two phases simultaneously, we must use two PDFGenerators. # PDFGenerator is designed to take care of as little information as it # must. (Don't do too much, and do it well.) A PDFGenerator can generate # the signal from only a single phase at a time. So, we will create one # PDFGenerator for each phase and compose them within the same # FitContribution. Note that both generators will be associated with the # same Profile within the FitContribution, so they will both be # automatically configured according to the metadata. # # The generator for the nickel phase. We call it "G_ni" and will use this # name later when we set the fitting equation in the FitContribution. generator_ni = PDFGenerator("G_ni") stru = CreateCrystalFromCIF(file(niciffile)) generator_ni.setStructure(stru) # The generator for the silicon phase. We call it "G_si". generator_si = PDFGenerator("G_si") stru = CreateCrystalFromCIF(file(siciffile)) generator_si.setStructure(stru) ## The FitContribution # Add both generators to the FitContribution. Add the Profile. This will # send the metadata to the generators. contribution = FitContribution("nisi") contribution.addProfileGenerator(generator_ni) contribution.addProfileGenerator(generator_si) contribution.setProfile(profile, xname="r") # Write the fitting equation. We want to sum the PDFs from each phase and # multiply it by a scaling factor. We also want a certain phase scaling # relationship between the PDFs which we will enforce with constraints in # the FitRecipe. contribution.setEquation("scale * (G_ni + G_si)") # Make the FitRecipe and add the FitContribution. recipe = FitRecipe() recipe.addContribution(contribution) ## Configure the fit variables # Start by configuring the scale factor and resolution factors. # We want the sum of the phase scale factors to be 1. recipe.newVar("scale_ni", 0.1) recipe.constrain(generator_ni.scale, "scale_ni") recipe.constrain(generator_si.scale, "1 - scale_ni") # We also want the resolution factor to be the same on each. recipe.newVar("qdamp", 0.03) recipe.constrain(generator_ni.qdamp, "qdamp") recipe.constrain(generator_si.qdamp, "qdamp") # Vary the gloabal scale as well. recipe.addVar(contribution.scale, 1) # Now we can configure the structural parameters. Since we're using # ObjCrystCrystalParSets, the space group constraints are automatically # applied to each phase. We must selectively vary the free parameters. # # First the nickel parameters phase_ni = generator_ni.phase for par in phase_ni.sgpars: recipe.addVar(par, name=par.name + "_ni") recipe.addVar(generator_ni.delta2, name="delta2_ni") # Next the silicon parameters phase_si = generator_si.phase for par in phase_si.sgpars: recipe.addVar(par, name=par.name + "_si") recipe.addVar(generator_si.delta2, name="delta2_si") # We have prior information from the earlier examples so we'll use it here # in the form of restraints. # # The nickel lattice parameter was measured to be 3.527. The uncertainty # values are invalid for that measurement, since the data from which it is # derived has no uncertainty. Thus, we will tell the recipe to scale the # residual, which means that it will be weighted as much as the average # data point during the fit. recipe.restrain("a_ni", lb=3.527, ub=3.527, scaled=True) # Now we do the same with the delta2 and Biso parameters (remember that # Biso = 8*pi**2*Uiso) recipe.restrain("delta2_ni", lb=2.22, ub=2.22, scaled=True) recipe.restrain("Biso_0_ni", lb=0.454, ub=0.454, scaled=True) # # We can do the same with the silicon values. We haven't done a thorough # job of measuring the uncertainties in the results, so we'll scale these # as well. recipe.restrain("a_si", lb=5.430, ub=5.430, scaled=True) recipe.restrain("delta2_si", lb=3.54, ub=3.54, scaled=True) recipe.restrain("Biso_0_si", lb=0.645, ub=0.645, scaled=True) # Give the recipe away so it can be used! return recipe
def makeRecipe(ciffile, grdata, iqdata): """Make complex-modeling recipe where I(q) and G(r) are fit simultaneously. The fit I(q) is fed into the calculation of G(r), which provides feedback for the fit parameters of both. """ # Create a PDF contribution as before pdfprofile = Profile() pdfparser = PDFParser() pdfparser.parseFile(grdata) pdfprofile.loadParsedData(pdfparser) pdfprofile.setCalculationRange(xmin = 0.1, xmax = 20) pdfcontribution = FitContribution("pdf") pdfcontribution.setProfile(pdfprofile, xname = "r") pdfgenerator = PDFGenerator("G") pdfgenerator.setQmax(30.0) stru = CreateCrystalFromCIF(file(ciffile)) pdfgenerator.setStructure(stru) pdfcontribution.addProfileGenerator(pdfgenerator) pdfcontribution.setResidualEquation("resv") # Create a SAS contribution as well. We assume the nanoparticle is roughly # elliptical. sasprofile = Profile() sasparser = SASParser() sasparser.parseFile(iqdata) sasprofile.loadParsedData(sasparser) sascontribution = FitContribution("sas") sascontribution.setProfile(sasprofile) from sans.models.EllipsoidModel import EllipsoidModel model = EllipsoidModel() sasgenerator = SASGenerator("generator", model) sascontribution.addProfileGenerator(sasgenerator) sascontribution.setResidualEquation("resv") # Now we set up a characteristic function calculator that depends on the # sas model. cfcalculator = SASCF("f", model) # Register the calculator with the pdf contribution and define the fitting # equation. pdfcontribution.registerCalculator(cfcalculator) # The PDF for a nanoscale crystalline is approximated by # Gnano = f * Gcryst pdfcontribution.setEquation("f * G") # Moving on recipe = FitRecipe() recipe.addContribution(pdfcontribution) recipe.addContribution(sascontribution) # PDF phase = pdfgenerator.phase for par in phase.sgpars: recipe.addVar(par) recipe.addVar(pdfgenerator.scale, 1) recipe.addVar(pdfgenerator.delta2, 0) # SAS recipe.addVar(sasgenerator.scale, 1, name = "iqscale") recipe.addVar(sasgenerator.radius_a, 10) recipe.addVar(sasgenerator.radius_b, 10) # Even though the cfcalculator and sasgenerator depend on the same sas # model, we must still constrain the cfcalculator Parameters so that it is # informed of changes in the refined parameters. recipe.constrain(cfcalculator.radius_a, "radius_a") recipe.constrain(cfcalculator.radius_b, "radius_b") return recipe
mstr.makeAll() # Set up the mPDF calculator. mc=mPDFcalculator(magstruc=mstr,rmin=rmin,rmax=rmax, rstep=rstep, gaussPeakWidth=0.2) def magpdf(parascale, ordscale): mc.paraScale = parascale mc.ordScale = ordscale mc.magstruc.makeAtoms() mc.magstruc.makeSpins() rv = mc.calc(both=True)[2] return rv totpdf = FitContribution('totpdf') totpdf.addProfileGenerator(nucpdf) totpdf.setProfile(profile) # Add mPDF to the FitContribution totpdf.registerFunction(magpdf) totpdf.setEquation("nucscale * nucpdf + magpdf(parascale, ordscale)") # Make magnetic PDF depend on any changes to the atomic structure. # 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()
def makeRecipe(): """Make the recipe for the fit. The instructions for what we want to refine, and how to refine it will be defined within a FitRecipe instance. The job of a FitRecipe is to collect and associate all the data, the fitting equations, fitting variables, constraints and restrations. We will demonstrate each of these within the code. Data is held within a Profile object. The Profile is simply a container that holds the data, and the theoretical profile once it has been calculated. Data is associated with a fitting equation within a FitContribution. The FitContribution defines the equation and parameters that will be adjusted to fit the data. The fitting equation can be defined within a function or optionally within the ProfileGenerator class. We won't need the ProfileGenerator class in this example since the signature of the fitting equation (the 'debye' function defined below) is so simple. The FitContribution also defines the residual function to optimize for the data/equation pair. This can be modified, but we won't do that here. """ ## The Profile # Create a Profile to hold the experimental and calculated signal. profile = Profile() # Load data and add it to the profile. It is our responsibility to get our # data into the profile. xydy = numpy.array(map(float, data.split()), dtype=float).reshape(-1,3) x, y, dy = numpy.hsplit(xydy, 3) profile.setObservedProfile(x, y, dy) ## The FitContribution # The FitContribution associates the profile with the Debye function. contribution = FitContribution("pb") # Tell the contribution about the Profile. We will need to use the # independent variable (the temperature) from the data to calculate the # theoretical signal, so give it an informative name ('T') that we can use # later. contribution.setProfile(profile, xname="T") # We now need to create the fitting equation. We tell the FitContribution # to use the 'debye' function defined below. The 'registerFunction' method # will let us do this. Since we haven't told it otherwise, # 'registerFunction' will extract the name of the function ('debye') and # the names of the arguments ('T', 'm', 'thetaD'). These arguments will # become Parameters of the FitContribution. Since we named the x-variable # 'T' above, the 'T' in the 'debye' equation will refer to this x-variable # whenever it is used. contribution.registerFunction(debye) # Now we can create the fitting equation. We want to extend the 'debye' # equation by adding a vertical offset. We could wrap 'debye' in a new # function with an offset, and register that instead of 'debye', but what # we do here is easier. # # When we set the fitting equation, we do not need to specify the # Parameters to the 'debye' function since the FitContribution already # knows what they are. If we choose to specify the arguments, we can make # adjustments to their input values. We wish to have the thetaD value in # the debye equation to be positive, so we specify the input as abs(thetaD) # in the equation below. Furthermore, we know 'm', the mass of lead, so we # can specify that as well. contribution.setEquation("debye(T, 207.2, abs(thetaD)) + offset") ## The FitRecipe # The FitRecipe lets us define what we want to fit. It is where we can # create variables, constraints and restraints. If we had multiple profiles # to fit simultaneously, the contribution from each could be added to the # recipe. recipe = FitRecipe() recipe.addContribution(contribution) # Specify which Parameters we want to refine. # Vary the offset recipe.addVar(contribution.offset, 0) # We also vary the Debye temperature. recipe.addVar(contribution.thetaD, 100) # We would like to 'suggest' that the offset should remain positive. This # is somethine that we know about the system that might help the refinement # converge to a physically reasonable result. We will do this with a soft # contraint, or restraint. Here we restrain the offset variable to between # 0 and infinity. We tell the recipe that we want to scale the penalty for # breaking the restraint by the point-average chi^2 value so that the # restraint is roughly as significant as any other data point throughout # the fit. recipe.restrain(recipe.offset, lb = 0, scaled = True) # We're done setting up the recipe. We can now do other things with it. return recipe
def makeRecipe(ciffile, grdata, iqdata): """Make complex-modeling recipe where I(q) and G(r) are fit simultaneously. The fit I(q) is fed into the calculation of G(r), which provides feedback for the fit parameters of both. """ # Create a PDF contribution as before pdfprofile = Profile() pdfparser = PDFParser() pdfparser.parseFile(grdata) pdfprofile.loadParsedData(pdfparser) pdfprofile.setCalculationRange(xmin = 0.1, xmax = 20) pdfcontribution = FitContribution("pdf") pdfcontribution.setProfile(pdfprofile, xname = "r") pdfgenerator = PDFGenerator("G") pdfgenerator.setQmax(30.0) stru = loadCrystal(ciffile) pdfgenerator.setStructure(stru) pdfcontribution.addProfileGenerator(pdfgenerator) pdfcontribution.setResidualEquation("resv") # Create a SAS contribution as well. We assume the nanoparticle is roughly # elliptical. sasprofile = Profile() sasparser = SASParser() sasparser.parseFile(iqdata) sasprofile.loadParsedData(sasparser) if all(sasprofile.dy == 0): sasprofile.dy[:] = 1 sascontribution = FitContribution("sas") sascontribution.setProfile(sasprofile) from sas.models.EllipsoidModel import EllipsoidModel model = EllipsoidModel() sasgenerator = SASGenerator("generator", model) sascontribution.addProfileGenerator(sasgenerator) sascontribution.setResidualEquation("resv") # Now we set up a characteristic function calculator that depends on the # sas model. cfcalculator = SASCF("f", model) # Register the calculator with the pdf contribution and define the fitting # equation. pdfcontribution.registerCalculator(cfcalculator) # The PDF for a nanoscale crystalline is approximated by # Gnano = f * Gcryst pdfcontribution.setEquation("f * G") # Moving on recipe = FitRecipe() recipe.addContribution(pdfcontribution) recipe.addContribution(sascontribution) # PDF phase = pdfgenerator.phase for par in phase.sgpars: recipe.addVar(par) recipe.addVar(pdfgenerator.scale, 1) recipe.addVar(pdfgenerator.delta2, 0) # SAS recipe.addVar(sasgenerator.scale, 1, name = "iqscale") recipe.addVar(sasgenerator.radius_a, 10) recipe.addVar(sasgenerator.radius_b, 10) # Even though the cfcalculator and sasgenerator depend on the same sas # model, we must still constrain the cfcalculator Parameters so that it is # informed of changes in the refined parameters. recipe.constrain(cfcalculator.radius_a, "radius_a") recipe.constrain(cfcalculator.radius_b, "radius_b") return recipe
def makeRecipe(stru1, stru2, datname): """Create a fitting recipe for crystalline PDF data.""" ## The Profile profile = Profile() # Load data and add it to the profile parser = PDFParser() parser.parseFile(datname) profile.loadParsedData(parser) profile.setCalculationRange(xmin=1.5, xmax=45, dx=0.1) ## The ProfileGenerator # In order to fit the core and shell phases simultaneously, we must use two # PDFGenerators. # # The generator for the CdS core. We call it "G_CdS" and will use this name # later when we set the fitting equation in the FitContribution. generator_cds = PDFGenerator("G_CdS") generator_cds.setStructure(stru1) generator_cds.setQmax(26) generator_cds.qdamp.value = 0.0396 # The generator for the ZnS shell. We call it "G_ZnS". generator_zns = PDFGenerator("G_ZnS") generator_zns.setStructure(stru2) generator_zns.setQmax(26) generator_zns.qdamp.value = 0.0396 ## The FitContribution # Add both generators and the profile to the FitContribution. contribution = FitContribution("cdszns") contribution.addProfileGenerator(generator_cds) contribution.addProfileGenerator(generator_zns) contribution.setProfile(profile, xname="r") # Set up the characteristic functions. We use a spherical CF for the core # and a spherical shell CF for the shell. Since this is set up as two # phases, we implicitly assume that the core-shell correlations contribute # very little to the PDF. from diffpy.srfit.pdf.characteristicfunctions import sphericalCF, shellCF contribution.registerFunction(sphericalCF, name="f_CdS") contribution.registerFunction(shellCF, name="f_ZnS") # Write the fitting equation. We want to sum the PDFs from each phase and # multiply it by a scaling factor. contribution.setEquation("scale * (f_CdS * G_CdS + f_ZnS * G_ZnS)") # Make the FitRecipe and add the FitContribution. recipe = FitRecipe() recipe.addContribution(contribution) # Vary the inner radius and thickness of the shell. Constrain the core # diameter to twice the shell radius. recipe.addVar(contribution.radius, 15) recipe.addVar(contribution.thickness, 11) recipe.constrain(contribution.psize, "2 * radius") ## Configure the fit variables # Start by configuring the scale factor and resolution factors. # We want the sum of the phase scale factors to be 1. recipe.newVar("scale_CdS", 0.7) recipe.constrain(generator_cds.scale, "scale_CdS") recipe.constrain(generator_zns.scale, "1 - scale_CdS") # We also want the resolution factor to be the same on each. # Vary the gloabal scale as well. recipe.addVar(contribution.scale, 0.3) # Now we can configure the structural parameters. We tag the different # structural variables so we can easily turn them on and off in the # subsequent refinement. phase_cds = generator_cds.phase for par in phase_cds.sgpars.latpars: recipe.addVar(par, name=par.name + "_cds", tag="lat") for par in phase_cds.sgpars.adppars: recipe.addVar(par, 1, name=par.name + "_cds", tag="adp") recipe.addVar(phase_cds.sgpars.xyzpars.z_1, name="z_1_cds", tag="xyz") # Since we know these have stacking disorder, constrain the B33 adps for # each atom type. recipe.constrain("B33_1_cds", "B33_0_cds") recipe.addVar(generator_cds.delta2, name="delta2_cds", value=5) phase_zns = generator_zns.phase for par in phase_zns.sgpars.latpars: recipe.addVar(par, name=par.name + "_zns", tag="lat") for par in phase_zns.sgpars.adppars: recipe.addVar(par, 1, name=par.name + "_zns", tag="adp") recipe.addVar(phase_zns.sgpars.xyzpars.z_1, name="z_1_zns", tag="xyz") recipe.constrain("B33_1_zns", "B33_0_zns") recipe.addVar(generator_zns.delta2, name="delta2_zns", value=2.5) # Give the recipe away so it can be used! return recipe
def makerecipe(structure_file, data_file): """ Basic function for creating and properly constraining a fit recipe. Parameters ---------- structure_file : Path object or str Path to *.cif file, containing a structural model to use to fit the PDF data. data_file : Path object or str Path to data file containing PDF data to fit against. Returns ------- recipe : FitRecipe object An initialized fit recipe object, ready for fitting. """ ######## Profile Section ################## # 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. profile = Profile() parser = PDFParser() parser.parseFile(data_file) profile.loadParsedData(parser) p_cif = getParser('cif') structure = p_cif.parseFile(str(structure_file)) space_group = p_cif.spacegroup.short_name ######## PDF Generator Section ################## # Create a PDF Generator object for a periodic structure model. # Here we name it "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(structure, periodic=True) ######## Fit Contribution Section ################## # Create a Fit Contribution object, and 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) # 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") # Set the experimental profile, within the Fit Contribution object, # to the Profile object we created earlier. contribution.setProfile(profile, xname="r") ######## Recipe Section ################## # Create the Fit Recipe object that holds all the details of the fit, # defined in the lines above. We give the Fit Recipe the Fit # Contribution we created earlier. recipe = FitRecipe() recipe.addContribution(contribution) # Return the Fit Recipe object to be optimized return recipe
def makeRecipe(strufile, datname1, datname2): """Create a recipe that uses the IntensityGenerator. We will create two FitContributions that use the IntensityGenerator from npintensitygenerator.py and associate each of these with a Profile, and use this to define a FitRecipe. Both simulated data sets come from the same structure. We're going to make two FitContributions that are identical, except for the profile that is held in each. We're going to assure that the structures are identical by using the same DiffpyStructureParSet (which is generated by the IntensityGenerator when we load the structure) in both generators. """ ## The Profiles # Create two Profiles for the two FitContributions. profile1 = Profile() profile2 = Profile() # Load data into the Profiles profile1.loadtxt(datname1) x, y, u = profile2.loadtxt(datname2) ## The ProfileGenerators # Create two IntensityGenerators named "I". There will not be a name # conflict, since the name is only meaningful within the FitContribution # that holds the ProfileGenerator. Load the structure into one and make # sure that the second ProfileGenerator is using the same # DiffyStructureParSet. This will assure that both ProfileGenerators are # using the exact same Parameters, and underlying Structure object in the # calculation of the profile. generator1 = IntensityGenerator("I") generator1.setStructure(strufile) generator2 = IntensityGenerator("I") generator2.addParameterSet(generator1.phase) ## The FitContributions # Create the FitContributions. contribution1 = FitContribution("bucky1") contribution1.addProfileGenerator(generator1) contribution1.setProfile(profile1, xname = "q") contribution2 = FitContribution("bucky2") contribution2.addProfileGenerator(generator2) contribution2.setProfile(profile2, xname = "q") # Now we're ready to define the fitting equation for each FitContribution. # The functions registered below will be independent, even though they take # the same form and use the same Parameter names. By default, Parameters # in different contributions are different Parameters even if they have the # same names. FitContributions are isolated namespaces than only share # information if you tell them to by using addParameter or addParameterSet. bkgdstr = "b0 + b1*q + b2*q**2 + b3*q**3 + b4*q**4 + b5*q**5 + b6*q**6 +\ b7*q**7 +b8*q**8 + b9*q**9" contribution1.registerStringFunction(bkgdstr, "bkgd") contribution2.registerStringFunction(bkgdstr, "bkgd") # We will create the broadening function by registering a python function. pi = numpy.pi exp = numpy.exp def gaussian(q, q0, width): return 1/(2*pi*width**2)**0.5 * exp(-0.5 * ((q-q0)/width)**2) contribution1.registerFunction(gaussian) contribution2.registerFunction(gaussian) # Center the gaussian contribution1.q0.value = x[len(x) // 2] contribution2.q0.value = x[len(x) // 2] # Now we can incorporate the scale and bkgd into our calculation. We also # convolve the signal with the gaussian to broaden it. contribution1.setEquation("scale * convolve(I, gaussian) + bkgd") contribution2.setEquation("scale * convolve(I, gaussian) + bkgd") # Make a FitRecipe and associate the FitContributions. recipe = FitRecipe() recipe.addContribution(contribution1) recipe.addContribution(contribution2) # Specify which Parameters we want to refine. We want to refine the # background that we just defined in the FitContributions. We have to do # this separately for each FitContribution. We tag the variables so it is # easy to retrieve the background variables. recipe.addVar(contribution1.b0, 0, name = "b1_0", tag = "bcoeffs1") recipe.addVar(contribution1.b1, 0, name = "b1_1", tag = "bcoeffs1") recipe.addVar(contribution1.b2, 0, name = "b1_2", tag = "bcoeffs1") recipe.addVar(contribution1.b3, 0, name = "b1_3", tag = "bcoeffs1") recipe.addVar(contribution1.b4, 0, name = "b1_4", tag = "bcoeffs1") recipe.addVar(contribution1.b5, 0, name = "b1_5", tag = "bcoeffs1") recipe.addVar(contribution1.b6, 0, name = "b1_6", tag = "bcoeffs1") recipe.addVar(contribution1.b7, 0, name = "b1_7", tag = "bcoeffs1") recipe.addVar(contribution1.b8, 0, name = "b1_8", tag = "bcoeffs1") recipe.addVar(contribution1.b9, 0, name = "b1_9", tag = "bcoeffs1") recipe.addVar(contribution2.b0, 0, name = "b2_0", tag = "bcoeffs2") recipe.addVar(contribution2.b1, 0, name = "b2_1", tag = "bcoeffs2") recipe.addVar(contribution2.b2, 0, name = "b2_2", tag = "bcoeffs2") recipe.addVar(contribution2.b3, 0, name = "b2_3", tag = "bcoeffs2") recipe.addVar(contribution2.b4, 0, name = "b2_4", tag = "bcoeffs2") recipe.addVar(contribution2.b5, 0, name = "b2_5", tag = "bcoeffs2") recipe.addVar(contribution2.b6, 0, name = "b2_6", tag = "bcoeffs2") recipe.addVar(contribution2.b7, 0, name = "b2_7", tag = "bcoeffs2") recipe.addVar(contribution2.b8, 0, name = "b2_8", tag = "bcoeffs2") recipe.addVar(contribution2.b9, 0, name = "b2_9", tag = "bcoeffs2") # We also want to adjust the scale and the convolution width recipe.addVar(contribution1.scale, 1, name = "scale1") recipe.addVar(contribution1.width, 0.1, name = "width1") recipe.addVar(contribution2.scale, 1, name = "scale2") recipe.addVar(contribution2.width, 0.1, name = "width2") # We can also refine structural parameters. We only have to do this once, # since each generator holds the same DiffpyStructureParSet. phase = generator1.phase lattice = phase.getLattice() a = recipe.addVar(lattice.a) # We want to allow for isotropic expansion, so we'll make constraints for # that. recipe.constrain(lattice.b, a) recipe.constrain(lattice.c, a) # We want to refine the thermal parameters as well. We will add a new # variable that we call "Uiso" and constrain the atomic Uiso values to # this. Note that we don't give Uiso an initial value. The initial value # will be inferred from the subsequent constraints. Uiso = recipe.newVar("Uiso") for atom in phase.getScatterers(): recipe.constrain(atom.Uiso, Uiso) # Give the recipe away so it can be used! return recipe
# define range for pdf calculation rmin = 0.01 rmax = 20 rstep = 0.01 # setup calculation range for the PDF simulation profile.setCalculationRange(xmin=rmin, xmax=rmax, dx=rstep) # prepare nucpdf function that simulates the nuclear PDF nucpdf = PDFGenerator("nucpdf") nucpdf.setStructure(mno) nucpdf.setProfile(profile) ### DO AN INITIAL FIT TO THE STRUCTURAL PDF totpdf = FitContribution('totpdf') 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
def makeContribution(name, generator, profile): """Make a FitContribution and add a generator and profile.""" contribution = FitContribution(name) contribution.addProfileGenerator(generator) contribution.setProfile(profile, xname = "r") return contribution
def makeRecipe(stru1, stru2, datname): """Create a fitting recipe for crystalline PDF data.""" ## The Profile profile = Profile() # Load data and add it to the profile parser = PDFParser() parser.parseFile(datname) profile.loadParsedData(parser) profile.setCalculationRange(xmin=1.5, xmax = 45, dx = 0.1) ## The ProfileGenerator # In order to fit the core and shell phases simultaneously, we must use two # PDFGenerators. # # The generator for the CdS core. We call it "G_CdS" and will use this name # later when we set the fitting equation in the FitContribution. generator_cds = PDFGenerator("G_CdS") generator_cds.setStructure(stru1) generator_cds.setQmax(26) generator_cds.qdamp.value = 0.0396 # The generator for the ZnS shell. We call it "G_ZnS". generator_zns = PDFGenerator("G_ZnS") generator_zns.setStructure(stru2) generator_zns.setQmax(26) generator_zns.qdamp.value = 0.0396 ## The FitContribution # Add both generators and the profile to the FitContribution. contribution = FitContribution("cdszns") contribution.addProfileGenerator(generator_cds) contribution.addProfileGenerator(generator_zns) contribution.setProfile(profile, xname = "r") # Set up the characteristic functions. We use a spherical CF for the core # and a spherical shell CF for the shell. Since this is set up as two # phases, we implicitly assume that the core-shell correlations contribute # very little to the PDF. from diffpy.srfit.pdf.characteristicfunctions import sphericalCF, shellCF contribution.registerFunction(sphericalCF, name = "f_CdS") contribution.registerFunction(shellCF, name = "f_ZnS") # Write the fitting equation. We want to sum the PDFs from each phase and # multiply it by a scaling factor. contribution.setEquation("scale * (f_CdS * G_CdS + f_ZnS * G_ZnS)") # Make the FitRecipe and add the FitContribution. recipe = FitRecipe() recipe.addContribution(contribution) # Vary the inner radius and thickness of the shell. Constrain the core # diameter to twice the shell radius. recipe.addVar(contribution.radius, 15) recipe.addVar(contribution.thickness, 11) recipe.constrain(contribution.psize, "2 * radius") ## Configure the fit variables # Start by configuring the scale factor and resolution factors. # We want the sum of the phase scale factors to be 1. recipe.newVar("scale_CdS", 0.7) recipe.constrain(generator_cds.scale, "scale_CdS") recipe.constrain(generator_zns.scale, "1 - scale_CdS") # We also want the resolution factor to be the same on each. # Vary the gloabal scale as well. recipe.addVar(contribution.scale, 0.3) # Now we can configure the structural parameters. We tag the different # structural variables so we can easily turn them on and off in the # subsequent refinement. phase_cds = generator_cds.phase for par in phase_cds.sgpars.latpars: recipe.addVar(par, name = par.name + "_cds", tag = "lat") for par in phase_cds.sgpars.adppars: recipe.addVar(par, 1, name = par.name + "_cds", tag = "adp") recipe.addVar(phase_cds.sgpars.xyzpars.z_1, name = "z_1_cds", tag = "xyz") # Since we know these have stacking disorder, constrain the B33 adps for # each atom type. recipe.constrain("B33_1_cds", "B33_0_cds") recipe.addVar(generator_cds.delta2, name = "delta2_cds", value = 5) phase_zns = generator_zns.phase for par in phase_zns.sgpars.latpars: recipe.addVar(par, name = par.name + "_zns", tag = "lat") for par in phase_zns.sgpars.adppars: recipe.addVar(par, 1, name = par.name + "_zns", tag = "adp") recipe.addVar(phase_zns.sgpars.xyzpars.z_1, name = "z_1_zns", tag = "xyz") recipe.constrain("B33_1_zns", "B33_0_zns") recipe.addVar(generator_zns.delta2, name = "delta2_zns", value = 2.5) # Give the recipe away so it can be used! return recipe
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. As before we use a PDFParser. The # metadata is still passed to the PDFGenerator later on. The interaction # between the PDFGenerator and the metadata does not depend on type of # structure being refined. parser = PDFParser() parser.parseFile(datname) profile.loadParsedData(parser) profile.setCalculationRange(xmax=20) ## The ProfileGenerator # This time we use the CreateCrystalFromCIF method of pyobjcryst.crystal to # create a Crystal object. That object is passed to the PDFGenerator as in # the previous example. generator = PDFGenerator("G") stru = CreateCrystalFromCIF(file(ciffile)) generator.setStructure(stru) generator.setQmax(40.0) ## The FitContribution 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 # As before, we get a handle to the structure parameter set. In this case, # it is a ObjCrystCrystalParSet instance that was created when we called # 'setStructure' above. The ObjCrystCrystalParSet has different Parameters # and options than the DiffpyStructureParSet used in the last example. See # its documentation in diffpy.srfit.structure.objcrystparset. phase = generator.phase # Here is where we created space group constraints in the previous example. # The difference in this example is that the ObjCrystCrystalParSet is aware # of space groups, and the DiffpyStructureParSet is not. Constraints are # created internally when "sgpars" attribute is called for. These # constraints get enforced within the ObjCrystCrystalParSet. Free # Parameters are stored within the 'sgpars' member of the # ObjCrystCrystalParSet, which is the same as the object returned from # 'constrainAsSpaceGroup'. # # As before, we have one free lattice parameter ('a'). We can simplify # things by iterating through all the sgpars. for par in phase.sgpars: recipe.addVar(par) # set the initial thermal factor to a non-zero value assert hasattr(recipe, 'B11_0') recipe.B11_0 = 0.1 # 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
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. As before we use a PDFParser. The # metadata is still passed to the PDFGenerator later on. The interaction # between the PDFGenerator and the metadata does not depend on type of # structure being refined. parser = PDFParser() parser.parseFile(datname) profile.loadParsedData(parser) profile.setCalculationRange(xmax = 20) ## The ProfileGenerator # This time we use the CreateCrystalFromCIF method of pyobjcryst.crystal to # create a Crystal object. That object is passed to the PDFGenerator as in # the previous example. generator = PDFGenerator("G") stru = CreateCrystalFromCIF(file(ciffile)) generator.setStructure(stru) generator.setQmax(40.0) ## The FitContribution 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 # As before, we get a handle to the structure parameter set. In this case, # it is a ObjCrystCrystalParSet instance that was created when we called # 'setStructure' above. The ObjCrystCrystalParSet has different Parameters # and options than the DiffpyStructureParSet used in the last example. See # its documentation in diffpy.srfit.structure.objcrystparset. phase = generator.phase # Here is where we created space group constraints in the previous example. # The difference in this example is that the ObjCrystCrystalParSet is aware # of space groups, and the DiffpyStructureParSet is not. Constraints are # created internally when "sgpars" attribute is called for. These # constriants get enforced within the ObjCrystCrystalParSet. Free # Parameters are stored within the 'sgpars' member of the # ObjCrystCrystalParSet, which is the same as the object returned from # 'constrainAsSpaceGroup'. # # As before, we have one free lattice parameter ('a'). We can simplify # things by iterating through all the sgpars. for par in phase.sgpars: recipe.addVar(par) # set the initial thermal factor to a non-zero value assert hasattr(recipe, 'B11_0') recipe.B11_0 = 0.1 # 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
def makeContribution(name, generator, profile): """Make a FitContribution and add a generator and profile.""" contribution = FitContribution(name) contribution.addProfileGenerator(generator) contribution.setProfile(profile, xname="r") return contribution
def makeRecipe(stru1, stru2, stru3, datname): """Create a fitting recipe for crystalline PDF data.""" ## The Profile profile = Profile() # Load data and add it to the profile parser = PDFParser() parser.parseFile(datname) profile.loadParsedData(parser) profile.setCalculationRange(xmin=1, xmax = 40, dx = 0.01) ## The ProfileGenerator generator_MEF_Cryst_B = PDFGenerator("G_MEF_Cryst_B") generator_MEF_Cryst_B.setStructure(stru1, periodic = True) generator_MEF_Mole_B = DebyePDFGenerator("G_MEF_Mole_B") generator_MEF_Mole_B.setStructure(stru2, periodic = False) generator_MEF_Intra = DebyePDFGenerator("G_MEF_Intra") generator_MEF_Intra.setStructure(stru3, periodic = False) ## The FitContribution # Add both generators to the FitContribution. Add the Profile. This will # send the metadata to the generators. contribution = FitContribution("MEF") contribution.addProfileGenerator(generator_MEF_Cryst_B) contribution.addProfileGenerator(generator_MEF_Mole_B) contribution.addProfileGenerator(generator_MEF_Intra) contribution.setProfile(profile, xname = "r") #write down the fit equation: #(G_MEF_Cryst_B - G_MEF_Mole_B) gives the intermolecular PDF, using a larger atomic displacement parameter #G_MEF_Intra gives intramolecular PDF, using a smaller atomic displacement parameter. #The sum of both parts gives the total PDF. contribution.setEquation("scale * (G_MEF_Cryst_B - G_MEF_Mole_B + G_MEF_Intra)") # Make the FitRecipe and add the FitContribution. recipe = FitRecipe() recipe.addContribution(contribution) qdamp = 0.02902 generator_MEF_Cryst_B.qdamp.value = qdamp generator_MEF_Mole_B.qdamp.value = qdamp generator_MEF_Intra.qdamp.value = qdamp # Vary the gloabal scale as well. recipe.addVar(contribution.scale, 1) ############################################################################################# ############### First the MEF_Cryst_B parameters ############################################ ############################################################################################# phase_MEF_Cryst_B = generator_MEF_Cryst_B.phase lat = phase_MEF_Cryst_B.getLattice() atoms = phase_MEF_Cryst_B.getScatterers() recipe.newVar("Uiso_Inter", 0.05, tag = "T1") recipe.newVar("lat_a", 14.556, tag = "lat") recipe.newVar("lat_b", 6.811, tag = "lat") recipe.newVar("lat_c", 7.657, tag = "lat") recipe.newVar("alpha", 119.57, tag = "lat") recipe.newVar("beta", 103.93, tag = "lat") recipe.newVar("gamma", 91.30, tag = "lat") recipe.constrain(lat.a, "lat_a") recipe.constrain(lat.b, "lat_b") recipe.constrain(lat.c, "lat_c") recipe.constrain(lat.alpha, "alpha") recipe.constrain(lat.beta, "beta") recipe.constrain(lat.gamma, "gamma") for atom in atoms: if atom.element.title() == "N": recipe.constrain(atom.Uiso, "Uiso_Inter") elif atom.element.title() == "O": recipe.constrain(atom.Uiso, "Uiso_Inter") elif atom.element.title() == "C": recipe.constrain(atom.Uiso, "Uiso_Inter") elif atom.element.title() == "H": recipe.constrain(atom.Uiso, "Uiso_Inter") generator_MEF_Cryst_B.delta2.value = 0 ############################################################################################# ############### Second the MEF_Mole_B parameters ############################################ ############################################################################################# phase_MEF_Mole_B = generator_MEF_Mole_B.phase generator_MEF_Mole_B.setQmin(0.0) generator_MEF_Mole_B.setQmax(24.0) recipe.newVar("zoom_Mole_B", 1, tag = "lat2") lat = phase_MEF_Mole_B.getLattice() recipe.constrain(lat.a, "zoom_Mole_B") recipe.constrain(lat.b, "zoom_Mole_B") recipe.constrain(lat.c, "zoom_Mole_B") # Constrain fractional xyz parameters atoms = phase_MEF_Mole_B.getScatterers() # Constrain ADPs for atom in atoms: if atom.element.title() == "C": recipe.constrain(atom.Uiso, "Uiso_Inter") elif atom.element.title() == "O": recipe.constrain(atom.Uiso, "Uiso_Inter") elif atom.element.title() == "N": recipe.constrain(atom.Uiso, "Uiso_Inter") elif atom.element.title() == "H": recipe.constrain(atom.Uiso, "Uiso_Inter") generator_MEF_Mole_B.delta2.value = 0 ############################################################################################# ############### Third the intra molecule parameters########################################## ############################################################################################# phase_MEF_Intra = generator_MEF_Intra.phase generator_MEF_Intra.setQmin(0.0) generator_MEF_Intra.setQmax(24.0) recipe.newVar("zoom_Intra", 1, tag = "lat3") lat = phase_MEF_Intra.getLattice() recipe.constrain(lat.a, "zoom_Intra") recipe.constrain(lat.b, "zoom_Intra") recipe.constrain(lat.c, "zoom_Intra") # Constrain fractional xyz parameters atoms = phase_MEF_Intra.getScatterers() # Constrain ADPs recipe.newVar("Uiso_Intra", 0.005, tag = "T2") for atom in atoms: if atom.element.title() == "C": recipe.constrain(atom.Uiso, "Uiso_Intra") elif atom.element.title() == "O": recipe.constrain(atom.Uiso, "Uiso_Intra") elif atom.element.title() == "N": recipe.constrain(atom.Uiso, "Uiso_Intra") elif atom.element.title() == "H": recipe.constrain(atom.Uiso, "Uiso_Intra") generator_MEF_Intra.delta2.value = 0 # Give the recipe away so it can be used! return recipe
def makeRecipe(strufile, datname1, datname2): """Create a recipe that uses the IntensityGenerator. We will create two FitContributions that use the IntensityGenerator from npintensitygenerator.py and associate each of these with a Profile, and use this to define a FitRecipe. Both simulated data sets come from the same structure. We're going to make two FitContributions that are identical, except for the profile that is held in each. We're going to assure that the structures are identical by using the same DiffpyStructureParSet (which is generated by the IntensityGenerator when we load the structure) in both generators. """ ## The Profiles # Create two Profiles for the two FitContributions. profile1 = Profile() profile2 = Profile() # Load data into the Profiles profile1.loadtxt(datname1) x, y, u = profile2.loadtxt(datname2) ## The ProfileGenerators # Create two IntensityGenerators named "I". There will not be a name # conflict, since the name is only meaningful within the FitContribution # that holds the ProfileGenerator. Load the structure into one and make # sure that the second ProfileGenerator is using the same # DiffyStructureParSet. This will assure that both ProfileGenerators are # using the exact same Parameters, and underlying Structure object in the # calculation of the profile. generator1 = IntensityGenerator("I") generator1.setStructure(strufile) generator2 = IntensityGenerator("I") generator2.addParameterSet(generator1.phase) ## The FitContributions # Create the FitContributions. contribution1 = FitContribution("bucky1") contribution1.addProfileGenerator(generator1) contribution1.setProfile(profile1, xname="q") contribution2 = FitContribution("bucky2") contribution2.addProfileGenerator(generator2) contribution2.setProfile(profile2, xname="q") # Now we're ready to define the fitting equation for each FitContribution. # The functions registered below will be independent, even though they take # the same form and use the same Parameter names. By default, Parameters # in different contributions are different Parameters even if they have the # same names. FitContributions are isolated namespaces than only share # information if you tell them to by using addParameter or addParameterSet. bkgdstr = "b0 + b1*q + b2*q**2 + b3*q**3 + b4*q**4 + b5*q**5 + b6*q**6 +\ b7*q**7 +b8*q**8 + b9*q**9" contribution1.registerStringFunction(bkgdstr, "bkgd") contribution2.registerStringFunction(bkgdstr, "bkgd") # We will create the broadening function by registering a python function. pi = numpy.pi exp = numpy.exp def gaussian(q, q0, width): return 1 / (2 * pi * width**2)**0.5 * exp(-0.5 * ((q - q0) / width)**2) contribution1.registerFunction(gaussian) contribution2.registerFunction(gaussian) # Center the gaussian contribution1.q0.value = x[len(x) / 2] contribution2.q0.value = x[len(x) / 2] # Now we can incorporate the scale and bkgd into our calculation. We also # convolve the signal with the gaussian to broaden it. contribution1.setEquation("scale * convolve(I, gaussian) + bkgd") contribution2.setEquation("scale * convolve(I, gaussian) + bkgd") # Make a FitRecipe and associate the FitContributions. recipe = FitRecipe() recipe.addContribution(contribution1) recipe.addContribution(contribution2) # Specify which Parameters we want to refine. We want to refine the # background that we just defined in the FitContributions. We have to do # this separately for each FitContribution. We tag the variables so it is # easy to retrieve the background variables. recipe.addVar(contribution1.b0, 0, name="b1_0", tag="bcoeffs1") recipe.addVar(contribution1.b1, 0, name="b1_1", tag="bcoeffs1") recipe.addVar(contribution1.b2, 0, name="b1_2", tag="bcoeffs1") recipe.addVar(contribution1.b3, 0, name="b1_3", tag="bcoeffs1") recipe.addVar(contribution1.b4, 0, name="b1_4", tag="bcoeffs1") recipe.addVar(contribution1.b5, 0, name="b1_5", tag="bcoeffs1") recipe.addVar(contribution1.b6, 0, name="b1_6", tag="bcoeffs1") recipe.addVar(contribution1.b7, 0, name="b1_7", tag="bcoeffs1") recipe.addVar(contribution1.b8, 0, name="b1_8", tag="bcoeffs1") recipe.addVar(contribution1.b9, 0, name="b1_9", tag="bcoeffs1") recipe.addVar(contribution2.b0, 0, name="b2_0", tag="bcoeffs2") recipe.addVar(contribution2.b1, 0, name="b2_1", tag="bcoeffs2") recipe.addVar(contribution2.b2, 0, name="b2_2", tag="bcoeffs2") recipe.addVar(contribution2.b3, 0, name="b2_3", tag="bcoeffs2") recipe.addVar(contribution2.b4, 0, name="b2_4", tag="bcoeffs2") recipe.addVar(contribution2.b5, 0, name="b2_5", tag="bcoeffs2") recipe.addVar(contribution2.b6, 0, name="b2_6", tag="bcoeffs2") recipe.addVar(contribution2.b7, 0, name="b2_7", tag="bcoeffs2") recipe.addVar(contribution2.b8, 0, name="b2_8", tag="bcoeffs2") recipe.addVar(contribution2.b9, 0, name="b2_9", tag="bcoeffs2") # We also want to adjust the scale and the convolution width recipe.addVar(contribution1.scale, 1, name="scale1") recipe.addVar(contribution1.width, 0.1, name="width1") recipe.addVar(contribution2.scale, 1, name="scale2") recipe.addVar(contribution2.width, 0.1, name="width2") # We can also refine structural parameters. We only have to do this once, # since each generator holds the same DiffpyStructureParSet. phase = generator1.phase lattice = phase.getLattice() a = recipe.addVar(lattice.a) # We want to allow for isotropic expansion, so we'll make constraints for # that. recipe.constrain(lattice.b, a) recipe.constrain(lattice.c, a) # We want to refine the thermal parameters as well. We will add a new # variable that we call "Uiso" and constrain the atomic Uiso values to # this. Note that we don't give Uiso an initial value. The initial value # will be inferred from the subsequent constraints. Uiso = recipe.newVar("Uiso") for atom in phase.getScatterers(): recipe.constrain(atom.Uiso, Uiso) # Give the recipe away so it can be used! return recipe
def makeRecipe(stru1, stru2, stru3, datname): """Create a fitting recipe for crystalline PDF data.""" ## The Profile profile = Profile() # Load data and add it to the profile parser = PDFParser() parser.parseFile(datname) profile.loadParsedData(parser) profile.setCalculationRange(xmin=1, xmax=20, dx=0.01) ## The ProfileGenerator generator_ROY_Cryst_B = PDFGenerator("G_ROY_Cryst_B") generator_ROY_Cryst_B.setStructure(stru1, periodic=True) generator_ROY_Mole_B = DebyePDFGenerator("G_ROY_Mole_B") generator_ROY_Mole_B.setStructure(stru2, periodic=False) generator_ROY_Intra = DebyePDFGenerator("G_ROY_Intra") generator_ROY_Intra.setStructure(stru3, periodic=False) ## The FitContribution # Add both generators to the FitContribution. Add the Profile. This will # send the metadata to the generators. contribution = FitContribution("ROY") contribution.addProfileGenerator(generator_ROY_Cryst_B) contribution.addProfileGenerator(generator_ROY_Mole_B) contribution.addProfileGenerator(generator_ROY_Intra) contribution.setProfile(profile, xname="r") # Write the fitting equation. We want to sum the PDFs from each phase and # multiply it by a scaling factor. We also want a certain phase scaling # relationship between the PDFs which we will enforce with constraints in # the FitRecipe. #from diffpy.srfit.pdf.characteristicfunctions import sphericalCF #contribution.registerFunction(sphericalCF, name = "f_IMC") contribution.setEquation( "scale * (G_ROY_Cryst_B - G_ROY_Mole_B + G_ROY_Intra)") # Make the FitRecipe and add the FitContribution. recipe = FitRecipe() recipe.addContribution(contribution) qdamp = 0.02902 generator_ROY_Cryst_B.qdamp.value = qdamp generator_ROY_Mole_B.qdamp.value = qdamp generator_ROY_Intra.qdamp.value = qdamp qbroad = 0.017315 generator_ROY_Cryst_B.qbroad.value = qbroad generator_ROY_Mole_B.qbroad.value = qbroad generator_ROY_Intra.qbroad.value = qbroad # Vary the gloabal scale as well. recipe.addVar(contribution.scale, 1) ############################################################################################# ############### First the ROY_Cryst_B parameters ############################################ ############################################################################################# phase_ROY_Cryst_B = generator_ROY_Cryst_B.phase lat = phase_ROY_Cryst_B.getLattice() atoms = phase_ROY_Cryst_B.getScatterers() recipe.newVar("Uiso_Inter", 0.05, tag="T1") recipe.newVar("lat_a", 3.9453, tag="lat") recipe.newVar("lat_b", 18.685, tag="lat") recipe.newVar("lat_c", 16.3948, tag="lat") #recipe.newVar("alpha", 90, tag = "lat") recipe.newVar("beta", 93.83, tag="lat") #recipe.newVar("gamma", 90, tag = "lat") recipe.constrain(lat.a, "lat_a") recipe.constrain(lat.b, "lat_b") recipe.constrain(lat.c, "lat_c") #recipe.constrain(lat.alpha, "alpha") recipe.constrain(lat.beta, "beta") #recipe.constrain(lat.gamma, "gamma") for atom in atoms: if atom.element.title() == "N": recipe.constrain(atom.Uiso, "Uiso_Inter") elif atom.element.title() == "O": recipe.constrain(atom.Uiso, "Uiso_Inter") elif atom.element.title() == "C": recipe.constrain(atom.Uiso, "Uiso_Inter") elif atom.element.title() == "H": recipe.constrain(atom.Uiso, "Uiso_Inter") elif atom.element.title() == "S": recipe.constrain(atom.Uiso, "Uiso_Inter") generator_ROY_Cryst_B.delta2.value = 0 # recipe.addVar(generator_IMC_Cryst_B.delta2, name = "delta2_IMC_Cryst_B", value = # 0, tag = "delta") ############################################################################################# ############### Second the ROY_Mole_B parameters ############################################ ############################################################################################# phase_ROY_Mole_B = generator_ROY_Mole_B.phase generator_ROY_Mole_B.setQmin(0.0) generator_ROY_Mole_B.setQmax(24.0) recipe.newVar("zoom_Mole_B", 1, tag="lat2") lat = phase_ROY_Mole_B.getLattice() recipe.constrain(lat.a, "zoom_Mole_B") recipe.constrain(lat.b, "zoom_Mole_B") recipe.constrain(lat.c, "zoom_Mole_B") # Constrain fractional xyz parameters atoms = phase_ROY_Mole_B.getScatterers() # Constrain ADPs #recipe.newVar("Uiso_Inter", 0.05, tag = "T2") for atom in atoms: if atom.element.title() == "C": recipe.constrain(atom.Uiso, "Uiso_Inter") elif atom.element.title() == "O": recipe.constrain(atom.Uiso, "Uiso_Inter") elif atom.element.title() == "N": recipe.constrain(atom.Uiso, "Uiso_Inter") elif atom.element.title() == "H": recipe.constrain(atom.Uiso, "Uiso_Inter") elif atom.element.title() == "S": recipe.constrain(atom.Uiso, "Uiso_Inter") generator_ROY_Mole_B.delta2.value = 0 # recipe.addVar(generator_IMC_Mole_B.delta2, name = "delta2_IMC_Mole_B", value # = 5.66086478091, tag = "delta") ############################################################################################# ############### Third the intra molecule parameters########################################## ############################################################################################# phase_ROY_Intra = generator_ROY_Intra.phase generator_ROY_Intra.setQmin(0.0) generator_ROY_Intra.setQmax(24.0) recipe.newVar("zoom_Intra", 1, tag="lat3") lat = phase_ROY_Intra.getLattice() recipe.constrain(lat.a, "zoom_Intra") recipe.constrain(lat.b, "zoom_Intra") recipe.constrain(lat.c, "zoom_Intra") # Constrain fractional xyz parameters atoms = phase_ROY_Intra.getScatterers() # Constrain ADPs recipe.newVar("Uiso_Intra", 0.005, tag="T2") for atom in atoms: if atom.element.title() == "C": recipe.constrain(atom.Uiso, "Uiso_Intra") elif atom.element.title() == "O": recipe.constrain(atom.Uiso, "Uiso_Intra") elif atom.element.title() == "N": recipe.constrain(atom.Uiso, "Uiso_Intra") elif atom.element.title() == "H": recipe.constrain(atom.Uiso, "Uiso_Intra") elif atom.element.title() == "S": recipe.constrain(atom.Uiso, "Uiso_Intra") generator_ROY_Intra.delta2.value = 0 # Give the recipe away so it can be used! return recipe
def makeRecipe(): """Make a FitRecipe for fitting a Gaussian curve to data. The instructions for what we want to refine, and how to refine it will be defined within a FitRecipe instance. The job of a FitRecipe is to collect and associate all the data, the fitting equations, fitting variables, constraints and restraints. The configured recipe provides a 'residual' function and the initial variable values that an optimizer can use to refine the variables to minimize the disagreement between the calculated profile and the data. Once we define the FitRecipe, we can send it an optimizer to be optimized. See the 'scipyOptimize' function. """ ## The Profile # Create a Profile to hold the experimental and calculated signal. profile = Profile() # Load data and add it to the profile. This uses the loadtxt function from # numpy. profile.loadtxt("data/gaussian.dat") ## The FitContribution # The FitContribution associates the Profile with a fitting equation. The # FitContribution also stores the parameters of the fitting equation. We # give our FitContribution then name "g1". We will be able to access the # FitContribution by that name within the FitRecipe. contribution = FitContribution("g1") # Tell the FitContribution about the Profile. The FitContribution will give # us access to the data held within the Profile. Here, we can tell it what # name we want to use for the independent variable. We tell it to use the # name "x". contribution.setProfile(profile, xname="x") # Now we need to create a fitting equation. We do that by writing out the # equation as a string. The FitContribution will turn this into a callable # function internally. In the process, it extracts all the parameters from # the equation (A, x, x0, sigma) and turns them into Parameter objects # internally. These objects can be accessed as attributes of the # contribution by name. Since we told the contribution that our # independent variable is named "x", this value will be substituted into # the fitting equation whenever it is called. contribution.setEquation("A * exp(-0.5*(x-x0)**2/sigma**2)") # To demonstrate how these parameters are used, we will give "A" an initial # value. Note that Parameters are not numbers, but are containers for # numbers. To get or modify the value of a parameter, use its 'value' # attribute. Parameters also have a 'name' attribute. contribution.A.value = 1.0 ## The FitRecipe # The FitRecipe lets us define what we want to fit. It is where we can # create variables, constraints and restraints. recipe = FitRecipe() # Here we tell the FitRecipe to use our FitContribution. When the FitRecipe # calculates its residual function, it will call on the FitContribution to # do part of the work. recipe.addContribution(contribution) # Specify which Parameters we want to vary in the fit. This will add # Variables to the FitRecipe that directly modify the Parameters of the # FitContribution. # # Here we create a Variable for the 'A' Parameter from our fit equation. # The resulting Variable will be named 'A' as well, but it will be accessed # via the FitRecipe. recipe.addVar(contribution.A) # Here we create the Variable for 'x0' and give it an initial value of 5. recipe.addVar(contribution.x0, 5) # Here we create a Variable named 'sig', which is tied to the 'sigma' # Parameter of our FitContribution. We give it an initial value through the # FitRecipe instance. recipe.addVar(contribution.sigma, name = "sig") recipe.sig.value = 1 return recipe
def makeRecipe(strufile, datname): """Create a recipe that uses the IntensityGenerator. This will create a FitContribution that uses the IntensityGenerator, associate this with a Profile, and use this to define a FitRecipe. """ ## The Profile # Create a Profile. This will hold the experimental and calculated signal. profile = Profile() # Load data and add it to the profile x, y, u = profile.loadtxt(datname) ## The ProfileGenerator # Create an IntensityGenerator named "I". This will be the name we use to # refer to the generator from within the FitContribution equation. We also # need to load the model structure we're using. generator = IntensityGenerator("I") generator.setStructure(strufile) ## The FitContribution # Create a FitContribution, that will associate the Profile with the # ProfileGenerator. The ProfileGenerator will be accessible as an # attribute of the FitContribution by its name ("I"). We also want to tell # the FitContribution to name the x-variable of the profile "q", so we can # use it in equations with this name. contribution = FitContribution("bucky") contribution.addProfileGenerator(generator) contribution.setProfile(profile, xname = "q") # Now we're ready to define the fitting equation for the FitContribution. # We need to modify the intensity calculation, and we'll do that from # within the fitting equation for the sake of instruction. We want to # modify the calculation in three ways. We want to scale it, add a # polynomial background, and broaden the peaks. # # There is added benefit for defining these operations outside of the # IntensityGenerator. By combining the different parts of the calculation # within the fitting equation, the time-consuming iofq calculation is only # performed when a structural Parameter is changed. If only non-structural # parameters are changed, such as the background and broadening Parameters, # then then previously computed iofq value will be used to compute the # contribution equation. The benefit in this is very apparent when # refining the recipe with the LM optimizer, which only changes two # variables at a time most of the time. Note in the refinement output how # many times the residual is calculated, versus how many times iofq is # called when using the scipyOptimize function. # We will define the background as a string. bkgdstr = "b0 + b1*q + b2*q**2 + b3*q**3 + b4*q**4 + b5*q**5 + b6*q**6 +\ b7*q**7 + b8*q**8 + b9*q**9" # This creates a callable equation named "bkgd" within the FitContribution, # and turns the polynomial coefficients into Parameters. eq = contribution.registerStringFunction(bkgdstr, "bkgd") # We will create the broadening function that we need by creating a python # function and registering it with the FitContribution. pi = numpy.pi exp = numpy.exp def gaussian(q, q0, width): return 1/(2*pi*width**2)**0.5 * exp(-0.5 * ((q-q0)/width)**2) # This registers the python function and extracts the name and creates # Parameters from the arguments. contribution.registerFunction(gaussian) # Center the Gaussian so it is not truncated. contribution.q0.value = x[len(x)/2] # Now we can incorporate the scale and bkgd into our calculation. We also # convolve the signal with the Gaussian to broaden it. Recall that we don't # need to supply arguments to the registered functions unless we want to # make changes to their input values. contribution.setEquation("scale * convolve(I, gaussian) + bkgd") # Make the FitRecipe and add the FitContribution. recipe = FitRecipe() recipe.addContribution(contribution) # Specify which parameters we want to refine. recipe.addVar(contribution.b0, 0) recipe.addVar(contribution.b1, 0) recipe.addVar(contribution.b2, 0) recipe.addVar(contribution.b3, 0) recipe.addVar(contribution.b4, 0) recipe.addVar(contribution.b5, 0) recipe.addVar(contribution.b6, 0) recipe.addVar(contribution.b7, 0) recipe.addVar(contribution.b8, 0) recipe.addVar(contribution.b9, 0) # We also want to adjust the scale and the convolution width recipe.addVar(contribution.scale, 1) recipe.addVar(contribution.width, 0.1) # We can also refine structural parameters. Here we extract the # DiffpyStructureParSet from the intensity generator and use the parameters # like we would any others. phase = generator.phase # We want to allow for isotropic expansion, so we'll constrain the lattice # parameters to the same value (the lattice is cubic). Note that we # constrain to the "a" Parameter directly. In previous examples, we # constrained to a Variable by name. This has the same effect. lattice = phase.getLattice() a = lattice.a recipe.addVar(a) recipe.constrain(lattice.b, a) recipe.constrain(lattice.c, a) # We want to refine the thermal parameters as well. We will add a new # Variable that we call "Uiso" and constrain the atomic Uiso values to # this. Note that we don't give Uiso an initial value. The initial value # will be inferred from the following constraints. Uiso = recipe.newVar("Uiso") for atom in phase.getScatterers(): recipe.constrain(atom.Uiso, Uiso) # Give the recipe away so it can be used! return recipe
def makeRecipe(ciffile, xdatname, ndatname): """Create a fitting recipe for crystalline PDF data.""" ## The Profiles # We need a profile for each data set. This means that we will need two # FitContributions as well. xprofile = Profile() nprofile = Profile() # Load data and add it to the proper Profile. parser = PDFParser() parser.parseFile(xdatname) xprofile.loadParsedData(parser) xprofile.setCalculationRange(xmax = 20) parser = PDFParser() parser.parseFile(ndatname) nprofile.loadParsedData(parser) nprofile.setCalculationRange(xmax = 20) ## The ProfileGenerators # We need one of these for the x-ray data. xgenerator = PDFGenerator("G") stru = CreateCrystalFromCIF(file(ciffile)) xgenerator.setStructure(stru) # And we need one for the neutron data. We want to refine the same # structure object in each PDFGenerator. This would suggest that we add the # same Crystal to each. However, if we do that then we will have two # Parameters for each Crystal data member (two Parameters for the "a" # lattice parameter, etc.), held in different ObjCrystCrystalParSets, each # managed by its own PDFGenerator. Thus, changes made to the Crystal # through one PDFGenerator will not be known to the other PDFGenerator # since their ObjCrystCrystalParSets don't know about each other. The # solution is to share ObjCrystCrystalParSets rather than Crystals. This # way there is only one Parameter for each Crystal data member. (An # alternative to this is to constrain each structure Parameter to be varied # to the same variable. The present approach is easier and less error # prone.) # # Tell the neutron PDFGenerator to use the phase from the x-ray # PDFGenerator. ngenerator = PDFGenerator("G") ngenerator.setPhase(xgenerator.phase) ## The FitContributions # We associate the x-ray PDFGenerator and Profile in one FitContribution... xcontribution = FitContribution("xnickel") xcontribution.addProfileGenerator(xgenerator) xcontribution.setProfile(xprofile, xname = "r") # and the neutron objects in another. ncontribution = FitContribution("nnickel") ncontribution.addProfileGenerator(ngenerator) ncontribution.setProfile(nprofile, xname = "r") # This example is different than the previous ones in that we are composing # a residual function from other residuals (one for the x-ray contribution # and one for the neutron contribution). The relative magnitude of these # residuals effectively determines the influence of each contribution over # the fit. This is a problem in this case because the x-ray data has # uncertainty values associated with it (on the order of 1e-4), and the # chi^2 residual is proportional to 1 / uncertainty**2. The neutron has no # uncertainty, so it's chi^2 is proportional to 1. Thus, my optimizing # chi^2 we would give the neutron data practically no weight in the fit. To # get around this, we will optimize a different metric. # # The contribution's residual can be either chi^2, Rw^2, or custom crafted. # In this case, we should minimize Rw^2 of each contribution so that each # one can contribute roughly equally to the fit. xcontribution.setResidualEquation("resv") ncontribution.setResidualEquation("resv") # Make the FitRecipe and add the FitContributions. recipe = FitRecipe() recipe.addContribution(xcontribution) recipe.addContribution(ncontribution) # Now we vary and constrain Parameters as before. recipe.addVar(xgenerator.scale, 1, "xscale") recipe.addVar(ngenerator.scale, 1, "nscale") recipe.addVar(xgenerator.qdamp, 0.01, "xqdamp") recipe.addVar(ngenerator.qdamp, 0.01, "nqdamp") # delta2 is a non-structual material propery. Thus, we constrain together # delta2 Parameter from each PDFGenerator. delta2 = recipe.newVar("delta2", 2) recipe.constrain(xgenerator.delta2, delta2) recipe.constrain(ngenerator.delta2, delta2) # We only need to constrain phase properties once since there is a single # ObjCrystCrystalParSet for the Crystal. phase = xgenerator.phase for par in phase.sgpars: recipe.addVar(par) recipe.B11_0 = 0.1 # Give the recipe away so it can be used! return recipe
def makeRecipe(strufile, datname): """Create a recipe that uses the IntensityGenerator. This will create a FitContribution that uses the IntensityGenerator, associate this with a Profile, and use this to define a FitRecipe. """ ## The Profile # Create a Profile. This will hold the experimental and calculated signal. profile = Profile() # Load data and add it to the profile x, y, u = profile.loadtxt(datname) ## The ProfileGenerator # Create an IntensityGenerator named "I". This will be the name we use to # refer to the generator from within the FitContribution equation. We also # need to load the model structure we're using. generator = IntensityGenerator("I") generator.setStructure(strufile) ## The FitContribution # Create a FitContribution, that will associate the Profile with the # ProfileGenerator. The ProfileGenerator will be accessible as an # attribute of the FitContribution by its name ("I"). We also want to tell # the FitContribution to name the x-variable of the profile "q", so we can # use it in equations with this name. contribution = FitContribution("bucky") contribution.addProfileGenerator(generator) contribution.setProfile(profile, xname = "q") # Now we're ready to define the fitting equation for the FitContribution. # We need to modify the intensity calculation, and we'll do that from # within the fitting equation for the sake of instruction. We want to # modify the calculation in three ways. We want to scale it, add a # polynomial background, and broaden the peaks. # # There is added benefit for defining these operations outside of the # IntensityGenerator. By combining the different parts of the calculation # within the fitting equation, the time-consuming iofq calculation is only # performed when a structural Parameter is changed. If only non-structural # parameters are changed, such as the background and broadening Parameters, # then then previously computed iofq value will be used to compute the # contribution equation. The benefit in this is very apparent when # refining the recipe with the LM optimizer, which only changes two # variables at a time most of the time. Note in the refinement output how # many times the residual is calculated, versus how many times iofq is # called when using the scipyOptimize function. # We will define the background as a string. bkgdstr = "b0 + b1*q + b2*q**2 + b3*q**3 + b4*q**4 + b5*q**5 + b6*q**6 +\ b7*q**7 + b8*q**8 + b9*q**9" # This creates a callable equation named "bkgd" within the FitContribution, # and turns the polynomial coefficients into Parameters. eq = contribution.registerStringFunction(bkgdstr, "bkgd") # We will create the broadening function that we need by creating a python # function and registering it with the FitContribution. pi = numpy.pi exp = numpy.exp def gaussian(q, q0, width): return 1/(2*pi*width**2)**0.5 * exp(-0.5 * ((q-q0)/width)**2) # This registers the python function and extracts the name and creates # Parameters from the arguments. contribution.registerFunction(gaussian) # Center the Gaussian so it is not truncated. contribution.q0.value = x[len(x)/2] # Now we can incorporate the scale and bkgd into our calculation. We also # convolve the signal with the Gaussian to broaden it. Recall that we don't # need to supply arguments to the registered functions unless we want to # make changes to their input values. contribution.setEquation("scale * convolve(I, gaussian) + bkgd") # Make the FitRecipe and add the FitContribution. recipe = FitRecipe() recipe.addContribution(contribution) # Specify which parameters we want to refine. recipe.addVar(contribution.b0, 0) recipe.addVar(contribution.b1, 0) recipe.addVar(contribution.b2, 0) recipe.addVar(contribution.b3, 0) recipe.addVar(contribution.b4, 0) recipe.addVar(contribution.b5, 0) recipe.addVar(contribution.b6, 0) recipe.addVar(contribution.b7, 0) recipe.addVar(contribution.b8, 0) recipe.addVar(contribution.b9, 0) # We also want to adjust the scale and the convolution width recipe.addVar(contribution.scale, 1) recipe.addVar(contribution.width, 0.1) # We can also refine structural parameters. Here we extract the # DiffpyStructureParSet from the intensity generator and use the parameters # like we would any others. phase = generator.phase # We want to allow for isotropic expansion, so we'll constrain the lattice # parameters to the same value (the lattice is cubic). Note that we # constrain to the "a" Parameter directly. In previous examples, we # constrained to a Variable by name. This has the same effect. lattice = phase.getLattice() a = lattice.a recipe.addVar(a) recipe.constrain(lattice.b, a) recipe.constrain(lattice.c, a) # We want to refine the thermal paramters as well. We will add a new # Variable that we call "Uiso" and constrain the atomic Uiso values to # this. Note that we don't give Uiso an initial value. The initial value # will be inferred from the following constraints. Uiso = recipe.newVar("Uiso") for atom in phase.getScatterers(): recipe.constrain(atom.Uiso, Uiso) # Give the recipe away so it can be used! return recipe
def setUp(self): self.f = FitContribution('f') self.f.setEquation('7') self.w = weak_ref(self.f._eq._flush, fallback=_fallback_example) return
def make_contribution(config: ConConfig) -> FitContribution: """ Make a FitContribution according to the ConConfig. Parameters ---------- config : ConConfig The configuration instance for the FitContribution. Returns ------- contribution : FitContribution The FitContribution built from ConConfig. """ contribution = FitContribution(config.name) fit_range = config.fit_range profile = make_profile(config.data_file, fit_range) contribution.setProfile(profile, xname="r") for phase in config.phases: generator = make_generator(phase) generator.qdamp.value = config.qparams[0] generator.qbroad.value = config.qparams[1] contribution.addProfileGenerator(generator) for base_line in config.base_lines: contribution.addProfileGenerator(base_line) for function in config.functions: name = function.name func_type = function.func_type argnames = function.argnames contribution.registerFunction(func_type, name, argnames) contribution.setEquation(config.eq) contribution.setResidualEquation(config.res_eq) return contribution