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 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 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
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
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(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
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