Multipeak Fit: Fit your experimental data with ... your experimental data?!

Igor's Multipeak Fit is convenient to fit analytical peak functions to data. But there might be times when you want to include some other data (wave) into your fit. For example, to find a match between different measurements or to include reference data into the fit. This code snippet provides the framework to fit experimental data including Gaussian broadening with Multipeak Fit (MPF).

The data is prepared in numbered folders (such as 'root:myData_1:') with numbered wave names (such as 'myPeak0', 'myPeak1' ...). Since MPF can only interact via numerical values, these numbers are later used to select the folder and desired data. After loading MPF, first navigate to Analysis => Multipeak Fit => Prepare Data Peak Function. A small prompt appears where the base folder and wave names are set ("root:myData_" and "myPeak" in above example).

Then start a fit set. Now, the DataPeak fit function is available from within MPF. Here, the last two parameters control the folder and wave number of the data you have just set up. These two parameters need to be on Hold at all times, or the fit will fail. Dial in the desired value to select the data to fit (e.g., folder 1 and wave 2), then run the fit. The first three parameters control the relative x-shift, Gaussian broadening and height (scale) of the data. If you want to switch off broadening, just set and hold this parameter at zero.

You can provide data in two variants: as-is or normalized. If you provide data as-is, then the first parameter can be seen as a 'relative shift' and the third parameter is a 'relative scale'. But you might want to normalize the data before using it in the fit: This would mean to normalize the data height to a maximum of 1 and/or adjust the x-scaling to be centered at zero (so that the 'peak' in your data is placed at x=0). For normalized data, the first parameter can be seen as a 'peak position' and the third parameter is 'height'.

Some notes and caveats:

  • You might want to at least remove the baseline from your input data or this baseline will appear in (and be scaled by) the fit.
  • If the data's x-range is smaller than needed for the fit, endpoints will be repeated to extend the range.
  • The output Location, Amplitude, Area, and FWHM values are numerically determined and may be junk depending on how much your data resembles 'a peak'.
#pragma TextEncoding = "UTF-8"
#pragma rtGlobals = 3
#pragma IgorVersion = 8.00
#pragma moduleName = MPFDataFit
#pragma DefaultTab = {3,20,4}

static StrConstant kMPF_WorkingDir = "root:Packages:MultiPeakFit2:"

Menu "Analysis"
    Submenu MPFDataFit#CreateDataFitMenu()  // add menu entry after loading MPF
        "Prepare Data Peak Function", /Q, MPF_PrepareDataFit()
    End
End

static Function/S CreateDataFitMenu()
    return SelectString(DataFolderExists(kMPF_WorkingDir),"-","Multipeak Fit")
End

// +++++++++

Function MPF_PrepareDataFit()   // setup for data locations
    String setBaseFolder = "", setBaseName = ""
    SVAR/Z baseFolder = $(kMPF_WorkingDir+"MPF_DataFit_baseFolder") // load previous settings
    SVAR/Z baseName = $(kMPF_WorkingDir+"MPF_DataFit_baseName")
    if (SVAR_Exists(baseFolder))
        setBaseFolder = baseFolder
    endif
    if (SVAR_Exists(baseName))
        setBaseName = baseName
    endif
   
    Prompt setBaseFolder,"Set base folder name without trailing number (e.g., \"root:myFit_\"):"
    Prompt setBaseName,"Set base data name without trailing number (e.g., \"peak_\"):"
    DoPrompt "Setup MPF data fit", setBaseFolder, setBaseName
    if (V_Flag)
        return -1
    endif
    if (!strlen(setBaseFolder) || !strlen(setBaseName))
        Abort "You need to enter both a folder path and base data name."
    endif
   
    int i, j, sourceFound = 0
    if (!DataFolderExists(kMPF_WorkingDir)) // if MPF was not started yet => create basic folder structure
        DFREF saveDF = GetDataFolderDFR()
        SetDataFolder root:
        for (i = 1; i < ItemsInList(kMPF_WorkingDir,":"); i++)
            NewDataFolder/O/S $StringFromList(i,kMPF_WorkingDir,":")
        endfor
        Variable/G currentSetNumber = 0
        SetDataFolder saveDF
    endif
    setBaseFolder = RemoveEnding(setBaseFolder , ":")   // make sure no colon was entered
    String/G $(kMPF_WorkingDir+"MPF_DataFit_baseFolder") = setBaseFolder
    String/G $(kMPF_WorkingDir+"MPF_DataFit_baseName") = setBaseName
   
    for (i = 0; i < 10 && !sourceFound; i++)    // try to look for the data in the first 10 folder / waves
        for (j = 0; j < 10 && !sourceFound; j++)
            Wave/Z source = $(setBaseFolder+num2str(i)+":"+setBaseName+num2str(j))
            sourceFound = WaveExists(source)
        endfor
    endfor
   
    if (!sourceFound)   // still nothing found? => have a simple check to remind the user
        DoAlert/T="Checking folder and data" 0, "Your first folder and/or data does not seem to exist yet. Make sure to prepare the data in the format: "+setBaseFolder+"[NUM]:"+setBaseName+"[NUM]"
    endif
    return 0
End

//########

static Function/Wave getSource(Wave cw)     // get source data from saved base names
    SVAR/Z baseFolder = $(kMPF_WorkingDir+"MPF_DataFit_baseFolder")
    SVAR/Z baseName   = $(kMPF_WorkingDir+"MPF_DataFit_baseName")
    if (SVAR_Exists(baseFolder) && SVAR_Exists(baseName))
        Wave/Z source = $(baseFolder+num2str(cw[3])+":"+baseName+num2str(cw[4]))
        if (WaveExists(source) && WaveType(source,1) == 1 && WaveDims(source) == 1)
            return source
        endif
    endif
    return $""
End

//########

static Function MPF_setHoldStateForPeak(Wave cw, String holdStr)    // injects hold state for the current peak
    DFREF DFpath = GetWavesDataFolderDFR(cw)
    int peakNum
    sscanf NameOfWave(cw), "Peak %d Coefs", peakNum
    if (!DataFolderRefStatus(DFpath) || numType(peakNum) != 0 || peakNum < 0)
        return -1
    endif
    SVAR gName = DFPath:GraphName
    String pName = gName+"#MultiPeak2Panel" // the MPF panel
    if (WinType(pName) == 7)
        Wave/T HoldStrings = DFPath:HoldStrings
        Execute/Z "WMMultiPeakFit#MPF2_RefreshHoldStrings(\""+pName+"\")"   // first, grab the latest hold settings from the panel
        HoldStrings[peakNum+1] = holdStr    // inject hold string
        Execute/Z "WMMultiPeakFit#SetHoldCheckboxesFromWave(WMMultiPeakFit#GetSetNumberFromWinName(\""+pName+"\"))" // rewrite to panel
    endif
    return 0
End

//####### MPF peak format functions #####

Function/S DataPeak_PeakFuncInfo(Variable which)    // pass names to MPF
    Switch (which)
        case 0:
            return "LocShift;Broaden;Height;Folder;DataNo;"
        case 1:
            return "MPF_DataPeakFunc"
        case 2:
            return "MPF_GaussToDataPeakGuess"
        case 3:
            return "MPF_DataPeakParams"
        case 4:
            return "Location;Height;Area;FWHM;"
    EndSwitch
End

//+++++++

Function MPF_GaussToDataPeakGuess(Wave cw)  // initializes parameters, and set hold state
    // cw values are: "PosShift;GaussWidth;Height;Folder;DataNo;"
    Redimension/N=5 cw
    int i,j, sourceFound = 0
    for (i = 0; i < 10 && !sourceFound; i++)
        for (j = 0; j < 10 && !sourceFound; j++)
            cw[3] = i
            cw[4] = j
            Wave/Z source = getSource(cw)   // grabs the first valid source wave
            sourceFound = WaveExists(source)
        endfor
    endfor
   
    int normInt = 1, normPos = 1
    if (sourceFound)
        Variable wMax  = WaveMax(source)
        Variable left  = leftx(source) < rightx(source) ? leftx(source) : rightx(source)
        Variable right = leftx(source) < rightx(source) ? rightx(source) : leftx(source)
        normInt = wMax > 0.9 && wMax < 1.1 ? 0 : 1
        normPos = left < 0 && right > 0 ? 0 : 1
    endif
   
    cw[0] = normPos ? 0 : cw[0] // if data seems centered around zero, peak position can stay
    cw[1] = 0.5 // initial Gaussian broadening
    cw[2] = normInt ? 1 : cw[2] // if data is normalized, peak height can stay

    // this needs to be executed later or the hold state will be reset
    Execute/P/Q "MPFDataFit#MPF_setHoldStateForPeak("+GetWavesDataFolder(cw,2)+", \"00011\")"
    return 0
End

//+++++++

Function MPF_DataPeakFunc(Wave cw, Wave yw, Wave xw) : FitFunc  // the fit function
    Wave/Z source = getSource(cw)
    if (!WaveExists(source))
        yw = NaN
        if (Exists("MPF2_DisplayStatusMessage"))    // display an error in the MPF graph
            Execute/Z "MPF2_DisplayStatusMessage(\"Base folder setup wrong or data not found.\","+GetWavesDataFolder(cw,1)+")"
        endif
        return -1
    endif
   
    Duplicate/free source, yWave
    SetScale/P x, DimOffset(source,0)+cw[0], DimDelta(source,0), ywave  // prepare shifted convolution wave
    PeakConvolveGaussHelper(yWave, cw[1])   // Gauss convolution
   
    Variable minX = pnt2x(yWave,0), maxX = pnt2x(yWave,numpnts(yWave)-1)        // make sure to not run out-of-bounds (end-point data is repeated)
    yw = xw[p] >= minX ? ( xw[p] <= maxX ? cw[2]*yWave(xw[p]) : cw[2]*yWave(maxX) ) : cw[2]*yWave(minX)
    return 0
End

//+++++++

Function MPF_DataPeakParams(Wave cw, Wave sw, Wave outWave)
    Variable Amplitude = NaN, Location = NaN, FWHM = NaN, PeakArea = NaN        // set to NaN for now
    FindPeakStatsHelper(cw, Amplitude, Location, FWHM, PeakArea)    // approximately calculates all results
    // cw values are: "PosShift;GaussWidth;Height;Folder;DataNo;"
    // output values are: "Location;Height;Area;FWHM;"
    outWave[0][0] = Location        // position
    outWave[0][1] = NaN
    outWave[1][0] = Amplitude       // height
    outWave[1][1] = NaN
    outWave[2][0] = PeakArea        // area
    outWave[2][1] = NaN
    outWave[3][0] = FWHM            // FWHM
    outWave[3][1] = NaN
    return 0
End

//#######

Static Function PeakConvolveGaussHelper(Wave in, Variable width)    // convolve with a prepared Gaussian
    if (width == 0 || numtype(width) != 0)
        return 0
    endif
   
    Variable pnt, dx  = DimDelta(in,0), size = DimSize(in,0)
    width = abs(width/dx)/sqrt(2)   // width normalized by x scaling and convert to expected Gauss 'width'
    pnt = round(max(abs(10*width),11))  // create a wave 10 times the Gauss width (5 sigma on each side)
    pnt = min(pnt, 2*numpnts(in)+1)     // doesn't have to be larger than two times the full input data
    pnt = pnt+!mod(pnt, 2)  // force odd size

    Make/FREE/D/N=(pnt) GaussWave  
    GaussWave = Gauss(x, (pnt-1)/2, width)
    Variable A = sum(GaussWave) // make sure the Gauss area limited even for a few points
    if (A > 1)
        GaussWave /= A
    endif
   
    Make/FREE/D/N=(2*pnt+size) temp = in[0]     // enlarge the wave to get the full convolution range
    temp[pnt,size+pnt-1] = in[p-pnt]
    temp[size+pnt,size+2*pnt-1] = in[size-1]
    MatrixOP/Free temp = replaceNaNs(temp,0)    // makes sure there are no NaN values in-between
    Convolve/A GaussWave temp
    in[0,size-1] = temp[p+pnt]
End

//#########

// recalculate the (almost) full peak and extract all result values numerically
Static Function FindPeakStatsHelper(Wave cw, Variable &Amplitude, Variable &Location, Variable &FWHM, Variable &PeakArea)
    Wave/Z sw = getSource(cw)
    if (!WaveExists(sw))
        return -1
    endif
    // cw values are: "PosShift;GaussWidth;Height;Folder;DataNo;"
    Variable left = pnt2x(sw,0)+cw[0], right = pnt2x(sw,numpnts(sw)-1)+cw[0]
    Variable pntSize = DimSize(sw,0)*10 // create a big wave (fine increments)
    pntSize = pntSize > 100000 ? 100000 : pntSize   // make sure its not too big
    Make/FREE/D/N=(pntSize+1) yw, xw
    Make/FREE/D/N=2 Levels
    SetScale/I x, left, right, xw; xw = x
    //+++++ find values +++++
    MPF_DataPeakFunc(cw, yw, xw)    // recalculate the peak with the coef set
    PeakArea = AreaXY(xw, yw, left, right)      // the area is calculated for the full source range
    WaveStats/Q yw
    Amplitude = cw[2] > 0 ? V_max : V_min
    Location  = cw[2] > 0 ? xw[V_maxloc] : xw[V_minloc]
    FindLevels/Q/D=Levels yw, Amplitude/2   // find the half-maximum positions
    if (V_LevelsFound == 2)
        left  = xw[Levels[0]]
        right = xw[Levels[1]]  
        FWHM  = abs(left - right)
    endif
    print/D PeakArea, Amplitude, Location, FWHM
    if (numtype(FWHM) != 0)
        return 0
    endif
    //++++++ recalculate with higher resolution ++++++
    // choose a range just a bit bigger than the FWHM
    SetScale/I x, left - 2*FWHM/(pntsize+1), right + 2*FWHM/(pntsize+1), xw; xw = x
    MPF_DataPeakFunc(cw, yw, xw)
    WaveStats/Q yw
    Amplitude = cw[2] > 0 ? V_max : V_min
    Location  = cw[2] > 0 ? xw[V_maxloc] : xw[V_minloc]
    FindLevels/Q/D=Levels yw, Amplitude/2
    if (V_LevelsFound == 2)
        left  = xw[Levels[0]]
        right = xw[Levels[1]]  
        FWHM  = abs(left - right)
    endif
    print/D PeakArea, Amplitude, Location, FWHM
    return 0
End

 

MPF Fit Data Peaks.ipf

Forum

Support

Gallery

Igor Pro 9

Learn More

Igor XOP Toolkit

Learn More

Igor NIDAQ Tools MX

Learn More