Custom peak function in MPF2 - what happened to the fit wave?
josh.liptonduffin
DS(a, G, A, x0) = A*( cos( Pi*a/2 + (1-a)*arctan((x-x0)/G) ) / (G^2 + (x-x0)^2)^(1/2-a/2)
Here A is the amplitude, G is the width, x0 is the centroid, and a is the asymmetry parameter. With a=0 the function simplifies to a Lorentzian, and with a between 0 and 1 it is asymmetric towards lower values of x. Typical fit values of a should be 0.1-0.3.
I believe I have successfully programmed this as an MPF2 fit function, and I can get the peaks to show up in the subcomponent axis. The fit appears to complete correctly, giving me reasonable-looking peaks. However, the fitted curve is not present on the graph - the original data is there, and the residuals look as if no fit is present (direct copy of the data, so it's subtracting zero from the data), see the attached image.
If I switch the peak type to Gaussian, it works correctly. The fit function reappears, and the residuals are calculated properly as well...
Have I messed up the peak definition? Why is it drawing the individual peaks, but not the fit function? Is it the arctangent function screwing me up?
January 25, 2012 at 09:36 am - Permalink
The DS peak shape should be generally no problem for MPF2 as even non-analytic peaks are possible. Have you actually thought about peak broadening of your experiment (?) via Gaussian convolution or is this not relevant? Also, you want to be extra careful of what to give back as fit results, since especially the area in this case is useless.
Anyway, it would make things easier if you could provide your code or an example experiment (attached to you entry).
Maybe it would also be a good idea to work together to create a generalized DS peak snippet for the community to use as it is a relatively common peak shape.
January 26, 2012 at 02:23 am - Permalink
I embarked upon this because I have a bunch of synchrotron XPS to fit. In the past I actually had a nice XOP compiled by a group where I did a postdoc, and this XOP included a gaussian convolution. Unfortunately that ran under windows only, and I am loath to switch operating systems back and forth just for running this one particular fitting. I wanted to see how fast (or slow) it would run without an XOP (I don't have the code, nor the XOP toolkit so I can't replicate this).
Here's the definitions for MPF2 - see if you get the same disappearing fit wave that I do?
Variable InfoDesired
String info=""
Switch (InfoDesired)
case PeakFuncInfo_ParamNames:
info = "Location;width;asymmetry;Height;"
break;
case PeakFuncInfo_PeakFName:
info = "MPFDS"
break;
case PeakFuncInfo_GaussConvFName:
info = "GausstoDSGuess"
break;
case PeakFuncInfo_ParameterFunc:
info = "DSParams"
break;
case PeakFuncInfo_DerivedParamNames:
info = "Location;Height;FWHM;Asymmetry;"
break;
default:
break;
endSwitch
return info
end
Function MPFDS(w,yw,xw)
wave w
wave yw, xw
yw = w[3]*cos(Pi*w[2]/2+(1-w[2])*atan((x-w[0])/w[1]))/((x-w[0])^2+w[1]^2)^((1-w[2])/2)
end
Function GaussToDSGuess(w)
wave w
variable height=w[2]
Redimension/N=4 w
w[2]=0.05 //just put in a small asymmetry to start the fit with.
w[3]=height
return 0
end
Function DSParams(cw, sw, outWave)
Wave cw, sw, outWave
Variable width = cw[1]
Variable widthSigma = sqrt(sw[1][1])
Variable asym=cw[2]
Variable asymsigma=sqrt(sw[2][2])
Variable amp = cw[3]
Variable ampSigma = sqrt(sw[3][3])
// location
outWave[0][0] = cw[0]
outWave[0][1] = sqrt(sw[0][0])
// amplitude
outWave[1][0] = amp
outWave[1][1] = ampSigma
// area ..... haven't what to return here, the integral of DS diverges at large values of E, so the area is a bit meaningless.
outWave[2][0] = nan
outWave[2][1] = nan
// FWHM
outWave[3][0] = width*2*sqrt(ln(2))
outWave[3][1] = widthSigma*2*sqrt(ln(2))
end
January 26, 2012 at 06:48 am - Permalink
John Weeks
WaveMetrics, Inc.
support@wavemetrics.com
January 26, 2012 at 09:26 am - Permalink
January 26, 2012 at 11:09 am - Permalink
But when I returned the result I used 'x' instead of 'xw'!
yw = w[3]*cos(Pi*w[2]/2+(1-w[2])*atan((xw-w[0])/w[1]))/((xw-w[0])^2+w[1]^2)^((1-w[2])/2) //this is right!!
Works a champ now, but no broadening yet.
January 26, 2012 at 12:54 pm - Permalink
For the record here, the function MPFDS() needs to have "x" replaced with "xw" so that X values come from the input xw wave. One of those easy-to-make, hard-to-see mistakes.
John Weeks
WaveMetrics, Inc.
support@wavemetrics.com
January 26, 2012 at 12:55 pm - Permalink
I used basically the approach described in ‘All-At-Once Fitting Functions’ in the manual with some modifications. That is, generating to waves with the peak and the Gaussian, convolving them, and then let the fit function pick the results.
There is now an additional GaussWidth parameter which determines the Gaussian broadening (set to zero for no broadening). Inside the fit function itself the approach is now to generate a wave which is more extended and fine-grained than the incoming peak-wave (controlled by a resolution factor inside the function). The DS-peak is written into this wave which can then be used for convolution. Looks like this:
Variable Resolution = 10 // the sample resolution (higher = slower)
Variable Xdelta = abs(xw[0] - xw[NumPnts(xw)-1]) / (Resolution * NumPnts(xw)) // calculate an high-res xdelta for scaling
Variable yPoints = Resolution * NumPnts(yw) // make the wave X times bigger than the original y wave
Make/FREE/D/N=(2*yPoints+1) yWave // create a wave two times as big to have space before and after the peak
SetScale/P x xw[0]-yPoints/2*Xdelta, Xdelta, yWave // reserve 1/2 size of space before the x wave starts (1/2 after)
yWave = w[2]*cos(Pi*w[3]/2+(1-w[3])*atan((x-w[0])/w[1]))/((x-w[0])^2+w[1]^2)^((1-w[3])/2)
if (w[4] != 0) // convolve with gaussian if width is set
Variable width = w[4]/Xdelta // width normalized by x scaling
yPoints = 10*width // create enough points to fit in the full gaussian
Make/FREE/D/N=(yPoints + 1) GaussWave // create a gaussian for the convolution
GaussWave = Gauss(x,yPoints/2,width)
Convolve/A GaussWave yWave // convolve with the prepared gaussian
endif
yw = yWave(xw[p]) // write the calculated values into the resultwave
...
There, your first version of the equation with the x (instead of xw) comes into play again. ;) The wave has to be more extended to avoid funny effects of the convolution at the edge in the final product. The waves created here might be overkill for most cases, but I don’t have any performance problems even with 5 of these peaks. Suggestions for optimizations are always welcome.
With the convolution in effect the calculated values for height, FWHM, and so on are rather meaningless. I use a function to calculate all these values numerically. I don’t have a clue how to get fit errors for these values, so I leave them blank. Maybe someone has a good idea for this.
I have also modified the guess function a bit to work better with automatic guesses. Don’t know if there are cases which let the guess fail this way (enormous asymmetry for example). Finally, the fit starts to look funny if the DS width gets close to zero, so one needs to look out for this case.
I have attached the full peak procedure. Please report any issues you run into.
January 27, 2012 at 10:36 am - Permalink
This works quite well for me! A little slower than non-convolved lineshapes, but quite useable.
I'm beginning to realize that what would be most useful would be 'global' asymmetry and gaussian width values - for example, I don't the gausswidth or asymmetry to be different when decomposing spectral data from a single peak (or doublet). I wonder if there is some hackery to be done by applying a constraint wave post hoc to fix these to be identically valued for all DS peaks, even though they are nominally being varied?
I was also thinking we could add a "DS_doublet" lineshape that produces two DS lineshapes with a preset energy splitting and intensity ratio - this would make fitting of closely spaced (say, Si 2p) XPS peaks easier than having a separate DS for each SO-split component and trying to fix the known energy splitting values. I'm sure this can be trivially implemented with chozo's code, I just don't have the time to do it myself right now - perhaps next week, but if anyone is feeling enthusiastic about working on this...
February 2, 2012 at 12:21 pm - Permalink
As mentioned, this thing is not so optimized. I opted for a relatively high resolution (ten times the original points) to represent even extremely thin peaks and lots of space (double the initial size) to fit in the convolution. I’m having a hard time spying on the doings of MPF2 with the debugger, but usually there is a lot of unused space in the waves while going through the various steps. I can’t estimate the edge cases very well this way. I sure there are ways to adjust these things to the actual requirements of the particular fit. Also there are still some quirks of MPF2 which increase the difficulty here; like that the individual peak waves of the fit are not representing the correct size of the peaks (i.e. may be much too wide or too narrow).
As for the doublet peak, global asymmetry and so on, this would be possible with linked parameters (like global fit has for multiple sets). Maybe with some magic this can be implemented into MPF2 in the future. I still have it at the end of my crazy feature suggestion list. For now you could look out manually for the correct parameters while pushing the peaks around in the fit. Another lazy hack could be to synchronize parameters via a global variable or the Kx ones at runtime. Don’t know what this would break.
February 3, 2012 at 07:08 am - Permalink
John Weeks
WaveMetrics, Inc.
support@wavemetrics.com
February 3, 2012 at 09:02 am - Permalink