#pragma rtGlobals=3 #pragma version=4.03 #pragma IgorVersion=6 #pragma ModuleName=BaselineSpline #include // written by tony withers, tony.withers@uwo.ca // please tell me when you find bugs! // How to use Baseline Spline Fit // A 1d wave must be plotted in a graph window before selecting Spline // Baseline from the macros menu. The wave must be plotted on the default // bottom and left axes. A control panel will be added to the left of the // graph window. When 'edit mode' is selected you can drag the nodes. The // spline should update as the nodes are repositioned. When in edit mode // you can add a new node at any position on the graph by positioning the // mouse cursor and clicking while holding down the control key. Nodes // may be deleted by clicking them with the option key held down. // Clicking anywhere in the graph window with the shift key held down // will toggle between node editing mode and 'normal' mode where you can // interact with the graph as usual. execute 'DisplayHelpTopic // Interpolate2' for details of the methods of spline interpolation. // Panel controls // Data wave: select a trace for baseline correction. When switching // between traces the nodes will retain their X-positions but will be // placed on the selected data wave at those positions (unless they lie // outside of the range of the data wave). // Akima: fit an Akima style spline through the nodes. Akima splines can // have sharper bends than cubic splines // Cubic: fit a cubic spline through the nodes. // Ends: At the ends of the spline where the cubic function is not fully // constrained select a method to add a constraint. 1st deriv.: match 1st // derivative; natural: match 2nd derivative. Affects only cubic spline // type. // Linear: connect the nodes with linear segments. // Smoothing: fit a smoothing spline, which does not have to pass exactly // through the nodes. The degree of smoothing is adjustable. // Edit mode: toggle between node editing and normal modes // Reset: set node positions by spacing a set number of nodes at regular // intervals along the section of the data wave displayed in the graph // window. No nodes will be set beyond the range of the graph axes. // Load: reload the node positions from a previous fit. // Subtract: creates a copy of the baseline and a baseline-subtracted // output spectrum in the current data folder and appends them to the // plot; saves the node positions in a 2d wave in the current data // folder. // Version history // 4.03 11/11/17 Should be backward compatible for Igor 6. // 4.02 Catch interpolate errors. // 4.01 Panel size decreased for compatibility with smaller or lower // resolution screens. Some bugs fixed. // 4.00 Changed to panel interface. Major rewrite. // 3.20 Removed option to prevent nodes from being inserted. Now the /NI // no insert flag is used by default, and we handle node insertion by // using a hook to collect ctrl-clicks. // 3.10 Some changes to the global strings and variables in the package // folder. Tried and failed to add improved guesses for node positions. // 3.01 11/4/16 Bug fix - waves with deltaX<0 produced nodes that were // reverse sorted in X, but Akima calculation cannot deal with reverse // sorted nodes. // 3.00 4/11/16 Added option for Akima style spline. // 2.60 3/30/16 Added option to supress node addition. This is a toggle // that switches on the /NI option in GraphWaveEdit. Updated a lot of // other code. Changed some of the defaults (now saves nodes by default), // so behaviour is not the same as previous version. // 2.50 Log mode to save node positions to file. Can load node positions // from a file. // 2.40 Added textbox reminder of how to toggle graph mode, renamed // procedures for consistency. // 2.30 5/16/11 Bug fix - code was choking on non-existent wave in first // call to SBL_Clear(). Added panel to change fitting options. // 2.10 Call SBL_Clear() from SBL_Subtract(). // 2.0 8/6/09 Incorporated suggestions from JJ Weimer that improve // functionality and clarity of code and compatibility with 6.10 ... and // then wrecked his nice programming with my clumsy code. // 1.43b Allow refit function to switch interpolate flag when number of // nodes is small to allow linear fit. // 1.42 6/8/08 Set y axis to manual range to avoid crazy rescaling if // spline shoots out of range. Show subtracted spectrum whilst fitting. // 1.41 10/9/07 For XY data we now interpolate directly into XY baseline. // Improved guesses for initial node positions for unevenly spaced XY // data. // 1.40 10/9/07 Fixed bug that occurred with baseline subtraction of XY // data. // 1.30 7/24/07 Handles XY data. Baseline is waveform, but data wave need // not be (though it should be reasonably closely spaced). // 1.20 7/3/07 Uses new graphwaveedit flag. Now requires version 6.0.1. Menu "Analysis" Submenu "Packages" "Spline Baseline",/Q, SBL_init() end end Menu "Macros" "Spline Baseline",/Q, SBL_init() end // initialize spline baseline package function SBL_init() if (strlen(WinList("*",";","WIN:1"))==0) // no graphs doalert 0, "Spline baseline requires a trace plotted in a graph window." return 0 endif // make sure the top graph is visible dowindow /F $WinName(0,1) // make sure graph has default axes getaxis /Q left if (v_flag==0) getaxis /Q bottom endif if (v_flag) doalert 0, "Trace must be plotted on bottom/left axes." return 0 endif // clear any package detritus from the last used spline graph SBL_clear(1) // initialize (or reinitialize) the package data folder SBL_resetDFR() DFREF dfr = SBL_getDFREF() wave /SDFR=dfr W_spline_dependency SBL_makePanel() string s_graph=SBL_getGraph() controlinfo /W=$s_graph+"#SBL_panel" popTrace string s_trace=s_value variable success // place nodes within window success=SBL_resetNodes() if (!success) doalert 0, "Spline baseline requires a trace plotted in a graph window." SBL_clear(1) return 0 endif // set the sub and base waves for this trace success=SBL_resetGraph(s_trace) if (!success) doalert 0, "Spline baseline requires a trace plotted in a graph window." SBL_Clear(1) return 0 endif ModifyGraph zero(left)=1 // set a dependency to trigger interpolation AND update graph when we adjust a node position W_spline_dependency:=SBL_update(root:Packages:SplineFit:W_nodesY) // won't work if we change package folder GraphWaveEdit /W=$s_graph /NI/T=1 /M $"W_nodesY" setWindow $s_graph hook(SBL_hook)=SBL_graphHook return 1 end function SBL_graphHook(s) STRUCT WMWinHookStruct &s if (s.eventcode!=5) // mouseup return 0 endif variable v_key=GetKeyState(0) if (v_key==0) return 0 endif if(v_key&2^2) // shift click // toggle mode SBL_toggleEdit() return 0 endif controlInfo /W=$s.winName+"#SBL_panel" check_edit if(v_value==0) return 0 endif variable insert=0 insert = (stringmatch(igorinfo(2),"Macintosh")) ? (v_key&2^4) : (v_key&2^0) // ctrl if (insert) DFREF dfr = SBL_getDFREF() wave /SDFR=dfr w_nodesY, w_nodesX variable v_X, v_Y v_X=AxisValFromPixel(s.winName, "bottom", s.mouseLoc.h) v_Y=AxisValFromPixel(s.winName, "left", s.mouseLoc.v) // deal with offset controlInfo /W=$s.winName+"#SBL_panel" popTrace string s_info=traceinfo(s.winName,s_value,0) v_Y-=GetNumFromModifyStr(s_info,"offset","{",1) w_nodesX[numpnts(w_nodesX)]={v_X} w_nodesY[numpnts(w_nodesY)]={v_Y} sort w_nodesX, w_nodesX, w_nodesY SBL_update({nan}) endif return 0 end function SBL_toggleEdit() string s_graph=SBL_getGraph() controlinfo /W=$s_graph+"#SBL_panel" check_edit if (v_value) GraphNormal /W=$s_graph ModifyGraph /W=$s_graph mode(W_nodesY)=2 checkbox check_edit win=$s_graph+"#SBL_panel", value=0 else // manually scale y axis getaxis /W=$s_graph /Q left setAxis /W=$s_graph left, V_min, V_max ModifyGraph /W=$s_graph mode(W_nodesY)=3 GraphWaveEdit /W=$s_graph /NI/T=1 /M $"W_nodesY" checkbox check_edit win=$s_graph+"#SBL_panel", value=1 endif end function SBL_resetGraph(s_trace) string s_trace DFREF dfr = SBL_getDFREF() string s_graph=SBL_getGraph() wave /Z w_data=TraceNameToWaveRef(s_graph, s_trace) if (waveexists(w_data)==0) return 0 endif wave /Z w_x=XWaveRefFromTrace(s_graph, s_trace) if (waveexists(w_x)) // check for monotonic X wave make /free w_diff Differentiate w_x /D=w_diff Wavestats /Q/M=0 W_diff if (V_max>0 && V_min<0) doalert 0, "X wave is not monatonic! Quitting :(" SBL_clear(1) return 0 endif endif // string xAxisName= StringByKey("XAXIS", traceinfo(s_graph,s_trace,0)) // string yAxisName= StringByKey("YAXIS", traceinfo(s_graph,s_trace,0)) // manually scale y axis getaxis /Q left setAxis left, V_min, V_max duplicate /O w_data dfr:W_base, dfr:W_sub wave /SDFR=dfr w_nodesX, w_nodesY, w_spline_dependency, w_base, w_sub checkdisplayed /W=$s_graph W_spline_dependency if (v_flag==0) appendtograph /W=$s_graph W_spline_dependency endif // make sure base and sub waves are displayed properly removefromgraph /Z /W=$s_graph w_base, w_sub if (waveexists(w_x)) appendtograph /W=$s_graph W_base vs w_x appendtograph /W=$s_graph W_sub vs w_x else appendtograph /W=$s_graph W_base appendtograph /W=$s_graph W_sub endif ModifyGraph /W=$s_graph hideTrace(W_spline_dependency)=1 // if data are offset, then apply offsets to baseline string s_info=traceinfo(s_graph,s_trace,0) variable trace_offset=GetNumFromModifyStr(s_info,"offset","{",1) make /n=3 /free w_c={0,10000,65535} SBL_chooseColour(w_c, s_graph, s_trace) ModifyGraph /W=$s_graph offset(W_base)={0,trace_offset} ModifyGraph /W=$s_graph rgb(W_base)=(w_c[0],w_c[1],w_c[2]) modifygraph /W=$s_graph live(W_base)=1 ModifyGraph /Z /W=$s_graph offset(W_nodesY)={0,trace_offset} ModifyGraph /W=$s_graph rgb(W_sub)=(0,0,0) ReorderTraces /W=$s_graph $"w_nodesY",{$"w_base"} return 1 end function SBL_resetNodes() string s_graph=SBL_getGraph() DFREF dfr = SBL_getDFREF() wave /SDFR=dfr W_nodesX, W_nodesY // define some local variables variable i, p_low, p_high, delP, delX, numNodes=5 string s_info variable trace_offset=0 controlinfo /W=$s_graph+"#SBL_panel" popTrace string s_trace=s_value wave /Z w_data=TraceNameToWaveRef(s_graph, s_trace) wave /Z w_x=XWaveRefFromTrace(s_graph, s_trace) if (waveexists(w_data)==0) return 0 endif redimension/N=(numNodes) W_nodesX, W_nodesY // determine where to place nodes on the bottom axis // keep nodes within window GetAxis/Q bottom make /n=2 /free xrange={v_min, v_max} sort xrange, xrange GetAxis/Q left make /n=2 /free yrange={v_min, v_max} sort yrange, yrange // keep nodes within extent of w_data if (waveexists(w_x)) xrange[0]=max(xrange[0],wavemin(w_x)) xrange[1]=min(xrange[1],wavemax(w_x)) else xrange[0]=max(xrange[0], min(pnt2x(w_data, 0), pnt2x(w_data, numpnts(w_data)-1)) ) xrange[1]=min(xrange[1], max(pnt2x(w_data, 0), pnt2x(w_data, numpnts(w_data)-1)) ) endif delX=(xrange[1]-xrange[0])/(numNodes-1) // initialize node positions W_nodesX=xrange[0]+p*delX for (i=0;i= 7 // remove duplicate nodes, // in case some were outside of the range of w_data duplicate /free w_nodesX w_duplicates findduplicates /SN=(nan) /SNDS=w_duplicates w_nodesX i=0 do if(numtype(w_duplicates[i])!=0) deletepoints i, 1, w_nodesX, w_nodesY, w_duplicates else i+=1 endif while(i= 7 movewindow /W=$s_graph 200, V_top, -1, -1 #else movewindow /W=$s_graph 200, V_top, 200+(V_right-V_left), v_bottom #endif endif // make panel NewPanel /K=1/N=SBL_panel/W=(200,0,0,355)/HOST=$s_graph/EXT=1 as "Spline Controls" ModifyPanel /W=$s_graph#SBL_panel, noEdit=1 string s_trace=stringfromlist(0, SBL_traces()) wave /Z w=TraceNameToWaveRef(s_graph, s_trace) variable i=0, deltaY=25, font=12, groupw=180 v_left=30 v_top=5 GroupBox group0,pos={v_left-20,v_top+deltaY*i},size={groupw,deltaY*2},title="Data wave" GroupBox group0,fSize=font i+=1 // store values internally in these controls PopupMenu popTrace, mode=1, Value=SBL_traces(), title="",pos={v_left,v_top+deltaY*i},size={130,20} PopupMenu popTrace, help={"select data wave" }, proc=SBL_popup, fSize=font i+=1.5 GroupBox group1 pos={v_left-20,v_top+deltaY*i},size={groupw,deltaY*5.3},title="Spline parameters" GroupBox group1 fSize=font i+=1 Checkbox check_akima, pos={v_left,v_top+deltaY*i}, fSize=font, value=0, proc=SBL_checkboxes, title="Akima" Checkbox check_akima, mode=1,help={"use Akima spline"} i+=0.8 Checkbox check_cubic, pos={v_left,v_top+deltaY*i}, fSize=font, value=1, proc=SBL_checkboxes, title="Cubic" Checkbox check_cubic, mode=1,help={"use cubic spline"} i+=0.8 PopupMenu popE,pos={v_left+35,v_top+deltaY*i},size={46,21}, fSize=font, title="Ends:", proc=SBL_popup PopupMenu popE, value="1st deriv.;natural", mode=2 i+=0.8 Checkbox check_linear, pos={v_left,v_top+deltaY*i}, fSize=font, value=0, proc=SBL_checkboxes, title="Linear" Checkbox check_linear, mode=1,help={"lines between nodes"} i+=0.8 Checkbox check_smoothing, pos={v_left,v_top+deltaY*i}, fSize=font, value=0, proc=SBL_checkboxes, title="Smoothing" Checkbox check_smoothing, mode=1,help={"use smoothing spline"} SetVariable setvarF, pos={v_left+100,v_top+deltaY*i},size={50,16},title="",bodyWidth=50 SetVariable setvarF, value=_NUM:1, limits={0,inf,1}, fSize=font, proc=SBL_setvar i+=1.5 GroupBox group2,pos={v_left-20,v_top+deltaY*i},size={groupw,deltaY*4.5},title="Node control" GroupBox group2,fSize=font i+=1 Checkbox check_edit, pos={v_left+30,v_top+deltaY*i}, fSize=font, value=1, proc=SBL_Checkboxes, title="Edit mode " Checkbox check_edit, mode=0,help={"Toggle to edit nodes"}, side=1 i+=0.8 SetDrawLayer ProgBack SetDrawEnv textyjust=2 DrawText v_left-12,v_top+deltaY*i,"Shift-click to toggle mode, ctrl-\r/option-click to add/zap nodes" i+=1.5 Button SBL_nodes,pos={v_left,v_top+deltaY*i},size={50,20},title="Reset", proc=SBL_Buttons Button SBL_nodes,help={"Distribute nodes over x-range of graph"}, fSize=font Button SBL_load,pos={v_left+80,v_top+deltaY*i},size={50,20},title="Load...", proc=SBL_Buttons Button SBL_load,help={"Load nodes used in a previous fit"} , fSize=font i+=1.5 Button SBL_sub,pos={65,v_top+deltaY*i},size={70,20},title="Subtract", proc=SBL_Buttons Button SBL_sub,help={"Subtract baseline and save a copy"}, fSize=font return 1 end function SBL_setvar(s) STRUCT WMSetVariableAction &s if(s.eventCode==-1) return 0 endif SBL_Update({nan}) end function SBL_popup(s) STRUCT WMPopupAction &s if(s.eventCode==-1) return 0 endif if (stringmatch(s.ctrlName, "popTrace")) if ( SBL_ResetGraph(s.popStr) ) SBL_setNodes() SBL_Update({nan}) endif controlInfo /W=$s.win+"#SBL_panel" check_edit // not sure why this is needed, there's no reason why it should get out of sync if (v_value) string s_graph=parseFilepath(0, s.win, "#", 0, 0) GraphWaveEdit /W=$s_graph /NI/T=1 /M $"W_nodesY" endif else SBL_Update({nan}) endif end function SBL_checkboxes(s) STRUCT WMCheckboxAction &s if(s.eventCode==-1) SBL_clear(0) return 0 endif strswitch(s.ctrlName) case "check_cubic": Checkbox check_akima, win=$s.win, value=0 Checkbox check_smoothing, win=$s.win, value=0 Checkbox check_linear, win=$s.win, value=0 SBL_Update({nan}) return 1 case "check_akima": Checkbox check_cubic, win=$s.win, value=0 Checkbox check_smoothing, win=$s.win, value=0 Checkbox check_linear, win=$s.win, value=0 SBL_Update({nan}) return 1 case "check_smoothing": Checkbox check_cubic, win=$s.win, value=0 Checkbox check_akima, win=$s.win, value=0 Checkbox check_linear, win=$s.win, value=0 SBL_Update({nan}) return 1 case "check_linear": Checkbox check_cubic, win=$s.win, value=0 Checkbox check_akima, win=$s.win, value=0 Checkbox check_smoothing, win=$s.win, value=0 SBL_Update({nan}) return 1 case "check_edit": string s_graph=parseFilepath(0, s.win, "#", 0, 0) if (s.checked) // manually scale y axis getaxis /W=$s_graph /Q left setAxis /W=$s_graph left, V_min, V_max ModifyGraph /W=$s_graph mode(W_nodesY)=3 GraphWaveEdit /W=$s_graph /NI/T=1 /M $"W_nodesY" else GraphNormal /W=$s_graph ModifyGraph /W=$s_graph mode(W_nodesY)=2 endif endswitch return 0 end function SBL_buttons(s) STRUCT WMButtonAction &s if(s.eventCode!=2) return 0 endif if (stringmatch(s.ctrlname, "SBL_sub")) SBL_subtract() endif if (stringmatch(s.ctrlname, "SBL_nodes")) SBL_resetNodes() endif if (stringmatch(s.ctrlname, "SBL_load")) SBL_loadNodes() endif return 0 end function /S SBL_traces() string s_graph=SBL_getGraph() string s_list=TraceNameList(s_graph,";",1+4) variable i=0 do if(stringmatch(stringfromlist(i,s_list), "w_base")) s_list=RemoveListItem(i, s_list) elseif(stringmatch(stringfromlist(i,s_list), "*_sub")) s_list=RemoveListItem(i, s_list) elseif(stringmatch(stringfromlist(i,s_list), "*_BL")) s_list=RemoveListItem(i, s_list) elseif(stringmatch(stringfromlist(i,s_list), "w_NodesY")) s_list=RemoveListItem(i, s_list) else i+=1 endif while (i x) ? 1 : 0 while(!done && (i2 < numKnots-1)) // edited this to allow extrapolation beyond end knot i1 = i2 - 1 // this edit to allow extrapolation beyond start knot if (i1<0) i1+=1 i2+=1 endif Variable x1, x2, y1, y2, iota1, iota2 x1 = knotX[i1] y1 = knotY[i1] iota1 = knotIota[i1] x2 = knotX[i2] y2 = knotY[i2] iota2 = knotIota[i2] Variable p0, p1, p2, p3, tmp p0 = y1 p1 = iota1 p2 = ( 3 * (y2 - y1)/(x2 - x1) - 2*iota1 - iota2 ) / (x2 - x1) p3 = (iota1 + iota2 - 2 * (y2 - y1)/(x2 - x1)) / (x2 - x1)^2 tmp = x - x1 return p0 + p1 * tmp + p2 * tmp^2 + p3*tmp^3 End // for Igor 6 compatibility function /T SBL_getGraph() #if IgorVersion() >= 7 GetWindow /Z SBL_panel activeSW return parseFilepath(0, s_value, "#", 0, 0) #endif string s_list=WinList("*", ";","WIN:1") string s_panel, s_graph variable i do s_graph=stringfromlist(i, s_list) s_panel=s_graph+"#SBL_panel" if (wintype(s_panel)) return s_graph endif i+=1 while (i