;+ ; NAME: ; ULY_FIT_LIN ; PURPOSE: ; Fit the linear coefficients and return the best fit ; ; USAGE: ; bestfit = uly_fit_lin(ADEGREE=adegree, ; PAR_LOSVD=par_losvd, CMP=cmp, ; SPECTRUM=SignalLog, ; GOODPIXELS=goodPixels, ; VOFF=voff, ; MPOLY=mpoly, ADDCONT=addcont, ; POLPEN=polpen [,/LUM_WEIGHT] [,/QUIET]) ; ; DESCRIPTION: ; Evaluate the different components of the model to fit and convolve ; them with the LOSVD for a given set of the free parameters. ; Determine the optimal weight of each component and the coefficients ; of the multiplicative and additive continuum (linear fit). ; Return the best fitting model. ; ; This function is called at each step of the Levenberg-Marquart ; minimization performed in ULY_FIT to compute the values of the ; non-linear parameters of the components and LOSVD. ; ; For performance reasons, the large data arrays are passed through ; commons (not as arguments). ; ; KEYWORDS: ; See also the documentation of uly_fit ; ADEGREE (input) ; Degree of additive polynomial, -1 if no additive polynomial ; POLPEN (input) ; Bias level of the multiplicative polynomial. This keyword can be ; used to reduce the effect of insignificant terms of the multiplicative ; polynomial. ; Terms whose coefficients are smaller, in absolute value, than <polpen> ; times their statistical error are amorted by the factor ; (abs(coef)/(<polpen>*err))^2. ; This feature is active only if MDEGREE>0. <polpen>=2 seems a ; reasonable choice. ; By default, no bias is applied. ; PAR_LOSVD (input) ; Parameters of the LOSVD, array of 0 to 6 numbers ordered as: ; [cz, sigma, h3, h4, h5, h6], cz and sigma are in pixels. ; SPECTRUM (input) ; Structure containing the spectrum to analyse and its associated error. ; It is used to determine the weight of each pixel (.err) and to ; fit the multiplicative continuum (.data) ; GOODPIXELS (input) ; List of pixels to be used in the fit. ; VOFF (input) ; Velocity offset between the spectrum to analyse and the model. km/s. ; CMP (Input & output) ; Array of components to fit (see documentation in ULY_FIT) ; In output CMP.WEIGHT is updated with the weight of each component ; computed by this routine. ; MPOLY (Input/output) ; multiplicative continuum Legendre polynomial ; structure {lmdegree, mpolcoefs, poly} ; .LMDEGREE (integer), Input, maximum degree of legendre polynomials. ; Unchanged in output. ; .MPOLCOEFS (double 1D array), LMDEGREE+1 terms. ; Output: updated values of the coefficients ; The input values of the coefficients are ignored. ; .POLY (1D array) ; Input: polynomial determined at a previous iterration ; Output: updated value of the polynomial (consistent with .MPOLCOEFS) ; Usually MPOLY is initialized by ULY_FIT, but if it is NULL in input, ; it is initialized to contain a degree 0 polynomial (scaling factor). ; ADDCONT (output) ; Additive polynomial (array). ; /LUM_WEIGHT ; Instruct to compute the luminosity weights and the errors on the ; weights. ; /QUIET ; Set this keyword to run silently. ; ; AUTHOR: ; Mina Koleva, Philippe Prugniel ; with credit to Michele Cappellari & Igor Chilingarian ; for initial development ; HISTORY: ; The principle of solving the linear dependences within the Levenberg-Marquart ; function evaluation is borrowed from M. Cappellari in his ppxf package. ; v.1.0 From early development by M. Cappelleri & I. Chilingarian ; v.2.0 Separated from the main program, Mina Koleva Sep 2005 ; v.2.3 Changing the interpolation (improve performance): mk 2007/03/01 ; v.3.0 Interpolating between different Mg/Fe: mk 2007/04/17 ; v.4.0 Ph. Prugniel 2007/12/14: linear fit of mult pol. Cache Legendre ; v.5.0 Ph. Prugniel, 2008/05/27: general n-components fit ; v.6 Ph. Prugniel, 2011/02: Implement Tikhonov regularisation ; v.7 Ph. Prugniel, 2015/09: Major debugging and improvements ;- ; CATEGORY: ULY ;------------------------------------------------------------------------------ ; ; uly_fit_fractsolve: ; This routine determines the weight of the different 'components' of the ; model: one or several components and 'npoly' terms of the additive ; polynomial. ; 'a' can be a vector (if there is a single template and no polynomial) ; or an array whose first 'npoly' lines are the polynomial basis, and ; the next line(s) the templates. ; 'b' is the 'observed' spectrum ; bvls_state is a I/O array used to store the state of BVLS_PS, for ; the warm start ; ; The function returns an array containing the weight of each component. ; It is called by 'uly_fit_lin_weight'. function uly_fit_fractsolve, a, b, npoly, BOUNDS=bounds, STATE=bvls_state compile_opt idl2, hidden s = size(a) if s[0] eq 1 then begin ; Fit a single cmp, no add polynomial soluz = total(a*b,/DOUBLE)/total(a^2,/DOUBLE) endif else if s[2] eq npoly+1 then begin ; Fitting a single cmp (no bound) soluz = la_least_squares(transpose(a), b, /DOUBLE) endif else begin ; Fitting multiple templates (bounds) bnd = replicate(1d,2,s[2]) ; faster than dblarr ; bnd = dblarr(2,s[2], /NOZERO) mx = (machar(/DOUBLE)).xmax if npoly gt 0 then begin bnd[0,0:npoly-1] = -mx ; No bounds on additive polynomials bnd[1,0:npoly-1] = mx ; No bounds on additive polynomials endif bnd[*,npoly:*] = double(bounds) bvls_ps, a, b, bnd, soluz, bvls_state, ITMAX=15*s[2], STATUS=ierr ; bvls_lh, a, b, bnd, soluz, ITMAX=15*s[2], STATUS=ierr if ierr ne 0 then message, 'BVLS returned with error #' + strtrim(ierr,2) endelse return, soluz end ;----------------------------------------------------------------------------- ; Determination (or refining) of the multiplicative Legendre polynomial function uly_fit_lin_mulpol, bestfit, CMP=cmp, MPOLY=mpoly, $ SPECTRUM=SignalLog, $ GOODPIXELS=goodPixels, $ QUIET=quiet, $ STATUS=status status = 0 npix = (size(*SignalLog.data, /DIM))[0] ; number of wavelength bins galaxy = double((*SignalLog.data)[goodPixels]) ; signal for non-masked points noise = double((*SignalLog.err)[goodPixels]) ; error for non-masked points if mpoly.lmdegree eq 0 then return, bestfit ; The polynomial may get crazy in the unconstrained regions, and this ; may affect the edges of these regions... ; It is critical when we want to fit spectral segment separated by a large ; range without any data. ; To prevent this, we may fit the polynomial everywhere, replacing the ; missing data with values in a correct range, and giving them low weight. ; like: ; galtmp = galaxy*0 + 1e ; galtmp[goodPixels] = galaxy[goodPixels] ; meas_err = galaxy*0 + (mean(noise[goodPixels]) * 100) ; meas_err[goodPixels] = noise[goodPixels] ; and then fit as: ; coefs_pol = mregress(cmul, galtmp, MEASURE_ERRORS=meas_err, INVERSE=inverse, STATUS=status) ; This should work in most places, and replacing the '1' by an ; interpolation of the data should make this solution quite general. ; Though, for the moment, we prefer to keep the conservative solution. ; So, the problem still exists. bestfitp = bestfit / mpoly.poly cmul = mpoly.leg_array * rebin(bestfitp,n_elements(bestfitp),mpoly.lmdegree+1) coefs_pol = mregress(cmul[goodPixels,*], galaxy, MEASURE_ERRORS=noise, STATUS=status_mregress) if status_mregress eq 1 then status = 1 ; note that la_least_squares or bvls_ps are about 50% slower than mregress ; coefs_pol = la_least_squares(transpose(cmul[goodPixels,*]), galaxy, /DOUBLE, STATUS=status) ; coefs_pol = transpose(coefs_pol) if n_elements(polpen) ne 0 then begin ; penalization of unsignificant terms in the pol. penal_pol = abs(coefs_pol)/(polpen*inverse.sigma) pen = 1+[where (penal_pol[1:*] lt 1)] coefs_pol[pen] *= penal_pol[pen]^2 endif mpoly.poly = mpoly.leg_array[*,0:mpoly.lmdegree] # transpose(coefs_pol) minmaxpol = minmax(mpoly.poly[goodPixels]) if minmaxpol[0] lt 0 then begin lmd = mpoly.lmdegree while min(mpoly.poly) le 0 and lmd gt 0 do begin lmd -= 1 coefp = mregress(cmul[goodPixels,0:lmd], galaxy, $ MEASURE_ERRORS=noise, $ STATUS=status_mregress) if status_mregress eq 1 then status = 1 if n_elements(polpen) ne 0 then begin penal_pol = abs(coefp) / (polpen*inv.sigma) pen = 1+[where (penal_pol[1:*] lt 1)] coefs_pol[pen] *= penal_pol[pen]^2 endif mpoly.poly = (mpoly.leg_array[*,0:lmd] # transpose(coefp)) endwhile if not keyword_set(quiet) then $ print, 'WARNING: the polynomial became negative, min:'+strtrim(minmaxpol[0],2),' its degree is reduced to:', lmd coefs_pol = 0*coefs_pol + coefp if min(mpoly.poly) le 0 then if not keyword_set(quiet) then begin message, /INFO, 'polynomial is negative' status = 1 return, 0 endif endif bestfitp *= mpoly.poly if keyword_set(lum_weight) and n_elements(cmp.weight) gt 0 then begin cmp.e_weight /= coefs_pol[0] cmp.l_weight /= coefs_pol[0] endif mpoly.mpolcoefs = coefs_pol return, bestfitp end ;------------------------------------------------------------------------------ ; Determination of the weight of each component function uly_fit_lin_weight, SignalLog, goodpixels, tmp, mpoly, $ ADEGREE=adegree, $ LUM_WEIGHT=lum_weight, $ CMP=cmp, $ ADDCONT=addcont, $ STATE=bvls_state, $ QUIET=quiet, $ STATUS=status ; Input arguments: ; SignalLog : observation (spect structure) ; goodpixels : list of pixels to use ; tmp : templates ; mpoly : structure containing the multiplicative polynomial ; Keywords (input) ; adegree : degree of additive polynomial ; lum_weight : keyword switch: if set, compute e_weight and l_weight ; Keyword (output) ; cmp : result returned as cmp.weight (and .e_weight .l_weight if /LUM_WEIGHT) ; addcont : additive continuum (array) ; status : error status: 0= no error ; RETURN ; bestfit : array with the best matching model n_cmp = n_elements(cmp) ; number of components npix = (size(*SignalLog.data, /DIM))[0] ; number of wavelength bins n_cmp = n_elements(cmp) ; number of components galaxy = double((*SignalLog.data)[goodPixels]) ; signal for non-masked points noise = double((*SignalLog.err)[goodPixels]) ; error for non-masked points status = 0 ; Fill the columns of the design matrix of the least-squares problem c = rebin(double(mpoly.poly), npix, n_cmp) * tmp ; Fill with models*mulpol if adegree ge 0 then begin ; If there is an addpol, prepend addpol*mulpol c1 = rebin(double(mpoly.poly), npix,(adegree+1)) for j=0,adegree do begin x = 2d*dindgen(npix)/npix - 1d c1[0,j] *= legendre(x,j) endfor c = [[c1],[c]] endif a = c[goodpixels,*] ; Used to solve the system for j=0,(adegree+1)+n_cmp-1 do a[*,j] /= noise ; Weight with errors ;----------------------------------------------------------------------------- ; Experiment with Tikhonov regularization (2011/02/05) ; We add to the system above some linear constraints , weighted with "lambda" ; See the chapters 18.4 and 18.5 of Numerical Recipes lambda = 0 ; regularization weight : 0 => no regularization ;lambda = 1d-13 * double(npix)/( n_cmp-1) ; ; For the moment, we experimented with 1st order regularization, i.e. ; the additional constraints asymptotically lead to a solution where all ; the weights are equal. ; The solution that we tried should still be finalized: The regularization ; weight should be scaled more intelligently. lambda should be insensitive ; to: (i) the number of wavelength bins (or VELSCALE), (ii) the flux scale ; in the data and (iii) the flux scale in the models. 5This should not be ; an issue to implement this ; We shall also explore these other aspects: ; - Other types of regularization, like 2nd order (linear dependence) ; or regularization matrix passed by the user (we may provide a different ; routine to help the creation of this matrix) ; - We may want to regularize on the l_weight rather than on the weights. ; We have to find a compromize between the flexibility of the program and ; its ease of use. The regularization is a complex business: How can the ; user choose the proper value of lambda, and the type of regularization? ; If we just give a single regularization (say 1st order) and a default ; lambda ... the user may be happy to have something functional at low ; expense of brain. But it is not fully correct. if lambda gt 0 then begin; ng = n_elements(goodpixels) nrel = ng + n_cmp - 1 aa = dblarr(nrel, n_cmp) for k = 0, n_cmp-1 do aa[0,k] = a[*, k] a = temporary(aa) cr = dblarr((size(c, /DIM))[1]) cr[adegree+1:adegree+2] = lambda * [-1, 1] ; 1st order regularization for k=ng, nrel-1 do begin a[k,*] = shift(cr, k-ng) endfor goodpixels = [goodpixels, lindgen(n_cmp-1)+npix] galaxy = [galaxy,fltarr(n_cmp-1)] noise = [noise,fltarr(n_cmp-1)+1] endif npoly = (adegree+1) ; Number of additive polynomials in the fit status_math = check_math(MASK=32) ; galaxy/noise may cause an underflow: handle if(adegree eq -1) then begin cmp.weight = uly_fit_fractsolve(a, galaxy/noise, npoly, BOUNDS=cmp.lim_weig, STATE=bvls_state) bestfit = c # cmp.weight addcont = dblarr(n_elements(bestfit)) ; needed to return arg addcont endif else begin wght = uly_fit_fractsolve(a, galaxy/noise, npoly, BOUNDS=cmp.lim_weig, STATE=bvls_state) bestfit = c # wght cmp.weight = wght[adegree+1:*] addcont = (c[*,0:adegree] # wght[0:adegree]) endelse if status_math eq 0 then status_math = check_math(MASK=32) nan = where(finite(cmp.weight) eq 0 or abs(cmp.weight) gt 1d36, cnt) if cnt gt 0 then begin message, /INFO, 'Could not determine the weight of cmp ' + $ strjoin(strtrim(nan,2),',')+' ... assume 0' cmp[nan].weight = 0 endif zeros = where(abs(cmp.weight) lt 1d-36, cnt) if cnt gt 0 then cmp[zeros].weight = 0d if abs(total(cmp.weight)) lt 1d-36 then begin if not keyword_set(quiet) then $ message, /INFO, 'The total weight is null' status = 1 common MPFIT_ERROR, error_code error_code = -2 ; tell MPFIT to stop, the best fit is a null spectrum return, 0 endif if keyword_set(lum_weight) and n_elements(cmp.weight) gt 0 then begin ; we call this only when we want to evaluate the errors on the ; weights and the luminosity weights w_notnull = where(cmp.weight ne 0, nw_notnull) cmp.e_weight = dblarr(n_cmp) cmp.l_weight = dblarr(n_cmp) if nw_notnull ne 0 then begin inv = 0 if adegree gt -1 then $ w_nn = [indgen(adegree+1), w_notnull+adegree+1] $ else $ w_nn = w_notnull ww = mregress((c[goodPixels,*])[*,w_nn], galaxy, $ MEASURE_ERRORS=noise, INVERSE=inv, STATUS=status) cmp[w_notnull].e_weight = inv.sigma[adegree+1:*] cmp.l_weight = (total(tmp, 1) * cmp.weight) / npix endif endif else begin cmp.e_weight = 0 cmp.l_weight = 1 endelse return, bestfit end ;------------------------------------------------------------------------------ FUNCTION uly_fit_lin, ADEGREE=adegree, $ PAR_LOSVD=par_losvd, $ CMP=cmp, $ SPECTRUM=SignalLog, $ GOODPIXELS=goodPixels, $ VOFF=voff, $ MPOLY=mpoly, $ ADDCONT=addcont, $ LUM_WEIGHT=lum_weight, $ POLPEN=polpen, $ CACHE=cache, $ STATE=bvls_state, $ MODECVG=modecvg, $ QUIET=quiet, $ STATUS=status compile_opt idl2 status = 0 ; Clear the accumulated stack of math error, in order to diagnos errors ; from the current routine. if !EXCEPT gt 0 then status_math = check_math(/PRINT) else $ if check_math() then print, $ 'An arithmetic error occured before calling uly_fit_lin' npix = (size(*SignalLog.data, /DIM))[0] ; number of wavelength bins n_cmp = n_elements(cmp) ; number of components npar_losvd = n_elements(par_losvd) ; number of free params for LOSVD galaxy = double((*SignalLog.data)[goodPixels]) ; signal for non-masked points noise = double((*SignalLog.err)[goodPixels]) ; error for non-masked points if n_elements(mpoly) eq 0 then begin ; default poly initialization mpoly = {lmdegree:0, mpolcoefs:dblarr(1), poly:dblarr(npix)} mpoly.poly = 1d endif ; check that noise is strictly positive, at least on the place of goodpixels if min(noise) le 0 then message, 'NOISE vector must be strictly positive' ;----------------------------------------------------------------------------- ; evaluate all the components if size(cache,/TYPE) ne 8 then begin cache = {para:ptrarr(n_elements(cmp), /ALLOCATE_HEAP), spectra:dblarr(npix, n_cmp)} for i=0, n_elements(cmp)-1 do *(cache.para)[i] = -1D35 endif ;init. models spectra models = dblarr(npix,n_cmp,/NOZERO) for i=0, n_cmp-1 do begin cached = 0 if n_elements(*cmp[i].para) gt 0 then begin if min((*cmp[i].para).value eq *(cache.para)[i]) eq 1 then begin models[0,i] = cache.spectra[*,i] cached = 1 endif endif if cached eq 0 then begin ; We treat separately SSPs and stars because it is (a bit) faster ; than doing everything with call_function case cmp[i].eval_fun of 'SSP' : mdl = $ uly_ssp_interp(*cmp[i].eval_data, (*cmp[i].para).value) 'STAR' : mdl = *cmp[i].eval_data else : mdl = $ call_function(cmp[i].eval_fun, *cmp[i].eval_data, $ (*cmp[i].para).value) endcase nmd = min([npix, (size(mdl,/DIM))[0]]) - 1 models[0:nmd,i] = mdl[0:nmd] if n_elements(*cmp[i].para) gt 0 then $ *(cache.para)[i] = (*cmp[i].para).value cache.spectra[*,i] = models[*,i] endif endfor if n_elements(par_losvd) gt 0 then begin ;----------------------------------------------------------------------------- ; generate the LOSVD kernel and convolve ; Detailed tests have shown that this kernel is correct as long ; as sigma > 0.8 (it is still acceptable down to sig=0.6) ; For low values of sigma: ; - The total flux of the kernel is too large (need a normalization) ; - The function is too much picked (for sigma around 0.3) ; when the kernel is centered and too flat when the kernel is ; shifted by half a pixel ; This low-sigma bias of the kernel can be modelled as: ; sigmac = sqrt(sigma^2 + 0.125*exp(-(sigma-0.28)^2/(0.04))) ; Where sigma is the value used in the program and sigmac the de-biased ; value corresponding to the real broadening. For the lack of simplicity, ; this correction is not applied in the program, but the user should ; be aware of the bias. A healthy solution consist in rebinning the ; spectrum to a scale where sigma>0.6 maxvel = abs(par_losvd[0]) maxsig = par_losvd[1] dx = ceil(abs(voff)+abs(maxvel)+5d*maxsig) ; Sample the LOSVD at least to vel+5*sigma dx = min([dx, (npix-1.)/2.]) n = 2*dx + 1 x = dx - findgen(n) losvd = dblarr(n,/NOZERO) vel = voff + par_losvd[0] sigma_pix = (par_losvd[1] gt 0.1) ? par_losvd[1] : 0.1d w = (x - vel)/sigma_pix w2 = w^2 ; Replace the values larger then 5*sigma with zeros wlarge = where(abs(w) gt 5., cnt, COMPLEMENT=wnorm) if cnt gt 0 then losvd[wlarge] = 0. losvd[wnorm] = exp(-0.5d*w2[wnorm])/(sqrt(2d*!dpi)*sigma_pix) ; Hermite polynomials normalized as in Appendix A of van der Marel & ; Franx (1993). They are given e.g. in Appendix C of Cappellari et al. (2002) nherm = (size(par_losvd,/DIM))[0] - 2 if nherm gt 0 then begin poly = 1d + par_losvd[2]/sqrt(3d)*(w*(2d*w2-3d)) ; h3 if nherm gt 1 then $ poly += par_losvd[3]/sqrt(24d)*(w2*(4d*w2-12d)+3d) ; h4 if nherm gt 2 then $ poly += par_losvd[4]/sqrt(60d)*(w*(w2*(4d*w2-20d)+15d)) ; h5 if nherm gt 3 then $ ; h6 poly += par_losvd[5]/sqrt(720d)*(w2*(w2*(8d*w2-60d)+90d)-15d) losvd *= poly endif losvd /= total(losvd) ; Normalized total(Gaussian)=1 (need for sigma<0.6) ; convolution with the LOSVD status_math = check_math(MASK=32) ; convolution may cause an underflow: handle tmp = convol(temporary(models),losvd,/EDGE_TRUNCATE) if status_math eq 0 then status_math = check_math(MASK=32) endif else tmp = temporary(models) ; Iterative loop to get the weight and mulcont ; when n_cmp>1 and md>0 the determination of the weights ; and of mulpol is not a linear process, and at present we ; need iteration to converge... and unfortunately, our ; current approach is slow (we tried to accelarete it but ; we failed for the moment to obtain a stable solution). ; However, in most cases, the LM iteration will converge ; even if uly_fit_lin does not converge at each step. ; So, we are currently offering three options: ; MODECVG=0 (the default one), this is the fastest, but ; it happens that the solution is missed ; MODECVG=1 (the convergence is achieved only once for each ; LM iteration, it is not reached for the derivatives ; MODECVG=2, uly_fit_lin always converge, but this is slow ; We are actively working at solving the problem and make ; something fast and reliable. itermax = 0 if n_elements(modecvg) eq 0 then modecvg=0 if modecvg eq 1 or modecvg eq 2 then itermax=499 fstop = 0 niter = 0 while fstop eq 0 do begin ; Determination of the weight of each component bestfitw = uly_fit_lin_weight(SignalLog, goodpixels, tmp, mpoly, $ ADEGREE=adegree, $ LUM_WEIGHT=lum_weight, $ CMP=cmp, $ ADDCONT=addcont, $ STATE=bvls_state, $ QUIET=quiet, $ STATUS=status) if status ne 0 then return, bestfitw posw = where(cmp.weight gt 0, cposw) if cposw eq 0 then begin if not keyword_set(quiet) then $ message, /INFO, 'All the cmp have a null weight... cannot continue' common MPFIT_ERROR, error_code error_code = -2 ; tell MPFIT to stop status = 1 return, 0 endif ; Determination of the multiplicative polynomials bestfit = uly_fit_lin_mulpol(bestfitw, CMP=cmp, MPOLY=mpoly, $ SPECTRUM=SignalLog, $ GOODPIXELS=goodPixels, $ QUIET=quiet, $ STATUS=status) if status ne 0 then return, float(bestfit) mpoly.poly /= mpoly.mpolcoefs[0] cmp.weight *= mpoly.mpolcoefs[0] mpoly.mpolcoefs /= mpoly.mpolcoefs[0] if cposw eq 1 then break ; no need to iterate when only on cmp is active niter += 1 if abs(1d0-total(bestfit,/NAN)/total(bestfitw,/NAN)) lt 5d-9 or niter gt itermax then fstop = 1 ; if abs(mean(1d0-bestfit/bestfitw)) lt 5d-9 or niter gt itermax then fstop = 1 endwhile bestfit = float(bestfit) ; The test below is useful to identify that an error occured during ; the execution of uly_fit_lin. ; Note that we cannot use a CATCH handler, because it does not catch ; math errors !! ; Note that to find the actual statement causing an error, the ; variable !EXCEPT can be set to 3 in the IDL session. if check_math(/PRINT) ne 0 then begin message, /INFO, 'an arithmetic error occured' common MPFIT_ERROR, error_code error_code = -2 ; tell MPFIT to stop status = 1 return, 0 endif return, bestfit end