# npintensity.pyΒΆ

```
#!/usr/bin/env python
########################################################################
#
# diffpy.srfit by DANSE Diffraction group
# Simon J. L. Billinge
# (c) 2009 The Trustees of Columbia University
# in the City of New York. All rights reserved.
#
# File coded by: Chris Farrow
#
# See AUTHORS.txt for a list of people who contributed.
# See LICENSE_DANSE.txt for license information.
#
########################################################################
"""Example of using ProfileGenerators in FitContributions.
This is an example of building a ProfileGenerator and using it in a
FitContribution in order to fit theoretical intensity data. ProfileGenerators
are used to organize profile calculators that require more information than can
be conveniently passed into a function call. The IntensityGenerator class is
an example of a ProfileGenerator that can be used by a FitContribution to help
generate a the profile.
Instructions
Run the example and note the last line of the output. It will be described in
the code. Then read through 'IntensityGenerator' class. This will motivate the
need for the ProfileGenerator class. Next read the 'makeRecipe' code. This
will demonstrate how to use the generator, the structure container needed by
the generator and introduce new operations that can be used in a registered
equation.
Extensions
- The IntensityGenerator class uses the 'addParameterSet' method to associate
the structure adapter (DiffpyStructureParSet) with the generator. Most SrFit
classes have an 'addParameterSet' class and can store ParameterSet objects.
Grab the phase object from the IntensityGenerator and try to add it to other
objects used in the fit recipe. Create variables from the moved Parameters
rather than from the 'phase' that lives in the IntensityGenerator and see if
everything still refines.
"""
from __future__ import print_function
import numpy
from diffpy.srfit.fitbase import ProfileGenerator, Profile
from diffpy.srfit.fitbase import FitContribution, FitRecipe
from diffpy.srfit.fitbase import FitResults
from diffpy.srfit.structure.diffpyparset import DiffpyStructureParSet
from gaussianrecipe import scipyOptimize
####### Example Code
class IntensityGenerator(ProfileGenerator):
"""A class for calculating intensity using the Debye equation.
Calculating intensity from a structure is difficult in general. This class
takes a diffpy.structure.Structure instance and from that generates a
theoretical intensity signal. Unlike the example in gaussianrecipe.py, the
intensity generator is not simple. It must take a structure object and some
Parameters, and from that generate a signal. At the same time, the
structure itself (the lattice, atom positions, thermal parameters, etc.)
needs to be refinable. Thus we define this ProfileGenerator to help us
interface which exposes the Parameters required by the calculation and
provides a way for a FitContribution to perform that calculation.
The purpose of a ProfileGenerator is to
1) provide a function that generates a profile signal
2) organize the Parameters required for the calculation
This generator wraps the 'iofq' function defined below. Knowledge of this
function is not required for this example.
"""
def __init__(self, name):
"""Define our generator.
In this example we will keep count of how many times the calculation
gets performed. The 'count' attribute will be used to store the count.
"""
ProfileGenerator.__init__(self, name)
# Count the calls
self.count = 0
return
def setStructure(self, strufile):
"""Set the structure used in the calculation.
strufile -- The name of a structure file. A
diffpy.structure.Structure object will be created from
the file, and that object will be passed to the 'iofq'
function whenever it is called.
This will create the refinement Parameters using the
DiffpyStructureParSet adapter from diffpy.srfit.structure.diffpyparset.
DiffpyStructureParSet is a ParameterSet object that organizes and gives
attribute access to Parameters and ParameterSets adapted from a diffpy
Structure object. The Parameters embedded in the DiffpyStructureParSet
are proxies for attributes of the diffpy.structure.Structure object
that is needed by the 'iofq' function. The Parameters will be
accessible by name under the 'phase' attribute of this generator, and
are organized hierarchically:
phase
- lattice (retrieved with 'getLattice')
- a
- b
- c
- alpha
- beta
- gamma
- scatterers (retrieved with 'getScatterers')
- atom1 (the name depends on the element)
- x
- y
- z
- occ
- U11
- U22
- U33
- U12
- U13
- U23
- Uiso
- etc.
The diffpy.structure.Structure instance is held within the
DiffpyStructureParSet as the 'stru' attribute.
"""
# Load the structure from file
from diffpy.structure import Structure
stru = Structure()
stru.read(strufile)
# Create a ParameterSet designed to interface with
# diffpy.structure.Structure objects that organizes the Parameter
# hierarchy. Note that the DiffpyStructureParSet holds a handle to the
# loaded structure that we use in the __call__ method below.
#
# We pass the diffpy.structure.Structure instance, and give the
# DiffpyStructureParSet the name "phase".
parset = DiffpyStructureParSet("phase", stru)
# Put this ParameterSet in the ProfileGenerator.
self.addParameterSet(parset)
return
def __call__(self, q):
"""Calculate the intensity.
This ProfileGenerator will be used in a FitContribution that will be
optimized to fit some data. By the time this function is evaluated,
the diffpy.structure.Structure instance has been updated by the
optimizer via the DiffpyStructureParSet defined in setStructure. Thus,
we need only call iofq with the internal structure object.
"""
self.count += 1
print("iofq called", self.count)
return iofq(self.phase.stru, q)
# End class IntensityGenerator
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.
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 main():
# Make the data and the recipe
strufile = "data/C60.stru"
q = numpy.arange(1, 20, 0.05)
makeData(strufile, q, "C60.iq", 1.0, 100.68, 0.005, 0.13, 2)
# Make the recipe
recipe = makeRecipe(strufile, "C60.iq")
# Optimize
scipyOptimize(recipe)
# Generate and print the FitResults
res = FitResults(recipe)
# We want to see how much speed-up we get from bringing the scale and
# background outside of the intensity generator. Get the number of calls
# to the residual function from the FitRecipe, and the number of calls to
# 'iofq' from the IntensityGenerator.
rescount = recipe.fithooks[0].count
calcount = recipe.bucky.I.count
footer = "iofq called %i%% of the time"%int(100.0*calcount/rescount)
res.printResults(footer = footer)
# Plot!
plotResults(recipe)
return
def plotResults(recipe):
"""Plot the results contained within a refined FitRecipe."""
# All this should be pretty familiar by now.
q = recipe.bucky.profile.x
I = recipe.bucky.profile.y
Icalc = recipe.bucky.profile.ycalc
bkgd = recipe.bucky.evaluateEquation("bkgd")
diff = I - Icalc
import pylab
pylab.plot(q,I,'ob',label="I(Q) Data")
pylab.plot(q,Icalc,'r-',label="I(Q) Fit")
pylab.plot(q,diff,'g-',label="I(Q) diff")
pylab.plot(q,bkgd,'c-',label="Bkgd. Fit")
pylab.xlabel(r"$Q (\AA^{-1})$")
pylab.ylabel("Intensity (arb. units)")
pylab.legend(loc=1)
pylab.show()
return
def iofq(S, q):
"""Calculate I(Q) (X-ray) using the Debye Equation.
I(Q) = 2 sum(i,j) f_i(Q) f_j(Q) sinc(rij Q) exp(-0.5 ssij Q**2)
(The exponential term is the Debye-Waller factor.)
S -- A diffpy.structure.Structure instance. It is assumed that the
structure is that of an isolated scatterer. Periodic boundary
conditions are not applied.
q -- The q-points to calculate over.
This uses cctbx for the calculation of the f_i if it is available,
otherwise f_i = 1.
"""
# The functions we need
sinc = numpy.sinc
exp = numpy.exp
pi = numpy.pi
# The brute-force calculation is very slow. Thus we optimize a little bit.
# The precision of distance measurements
deltad = 1e-6
dmult = int(1/deltad)
deltau = deltad**2
umult = int(1/deltau)
pairdict = {}
elcount = {}
n = len(S)
for i in range(n):
# count the number of each element
eli = S[i].element
m = elcount.get(eli, 0)
elcount[eli] = m + 1
for j in range(i + 1, n):
elj = S[j].element
# Get the pair
els = [eli, elj]
els.sort()
# Get the distance to the desired precision
d = S.distance(i, j)
D = int(d*dmult)
# Get the DW factor to the same precision
ss = S[i].Uisoequiv + S[j].Uisoequiv
SS = int(ss*umult)
# Record the multiplicity of this pair
key = (els[0], els[1], D, SS)
mult = pairdict.get(key, 0)
pairdict[key] = mult + 1
# Now we can calculate IofQ from the pair dictionary. Making the dictionary
# first reduces the amount of calls to sinc and exp we have to make.
# First we must cache the scattering factors
fdict = {}
for el in elcount:
fdict[el] = getXScatteringFactor(el, q)
# Now we can compute I(Q) for the i != j pairs
y = 0
x = q * deltad / pi
for key, mult in pairdict.items():
eli = key[0]
elj = key[1]
fi = fdict[eli]
fj = fdict[elj]
D = key[2]
SS = key[3]
# Note that numpy's sinc(x) = sin(x*pi)/(x*pi)
y += fi * fj * mult * sinc(x * D) * exp(-0.5 * SS * deltau * q**2)
# We must multiply by 2 since we only counted j > i pairs.
y *= 2
# Now we must add in the i == j pairs.
for el, f in fdict.items():
y += f**2 * elcount[el]
# And that's it!
return y
def getXScatteringFactor(el, q):
"""Get the x-ray scattering factor for an element over the q range.
If cctbx is not available, f(q) = 1 is used.
"""
try:
import cctbx.eltbx.xray_scattering as xray
wk1995 = xray.wk1995(el)
g = wk1995.fetch()
# at_stol - at sin(theta)/lambda = Q/(4*pi)
f = numpy.asarray( map( g.at_stol, q/(4*numpy.pi) ) )
return f
except ImportError:
return 1
def makeData(strufile, q, datname, scale, a, Uiso, sig, bkgc, nl = 1):
"""Make some fake data and save it to file.
Make some data to fit. This uses iofq to calculate an intensity curve, and
adds to it a background, broadens the peaks, and noise.
strufile-- A filename holding the sample structure
q -- The q-range to calculate over.
datname -- The name of the file we're saving to.
scale -- The scale factor
a -- The lattice constant to use
Uiso -- The thermal factor for all atoms
sig -- The broadening factor
bkgc -- A parameter that gives minor control of the background.
nl -- Noise level (0, inf), default 1, larger -> less noise.
"""
from diffpy.structure import Structure
S = Structure()
S.read(strufile)
# Set the lattice parameters
S.lattice.setLatPar(a, a, a)
# Set a DW factor
for a in S:
a.Uisoequiv = Uiso
y = iofq(S, q)
# We want to broaden the peaks as well. This simulates instrument effects.
q0 = q[len(q) // 2]
g = numpy.exp(-0.5*((q-q0)/sig)**2)
y = numpy.convolve(y, g, mode='same')/sum(g)
# Add a polynomial background.
bkgd = (q + bkgc)**2 * (1.5*max(q) - q)**5
bkgd *= 0.2 * max(y) / max(bkgd)
y += bkgd
# Multipy by a scale factor
y *= scale
# Calculate the uncertainty
u = (y/nl)**0.5
# And apply the noise
if nl > 0:
y = numpy.random.poisson(y*nl) / nl
# Now save it
numpy.savetxt(datname, numpy.transpose([q, y, u]))
return
if __name__ == "__main__":
main()
# End of file
```