def interpForVelocity(kuruczAtms, pradks, TEff, LogG, Metal, VTurb): # This functions finds the set of 'bracketing models for mass, and interpolates # between them. # Note that we only have ATLAS models for VTurb = 2.0... loVIdx, hiVIdx = u.bracket(VTurb, sorted(kuruczAtms.keys())) loV = sorted(kuruczAtms.keys())[loVIdx] hiV = sorted(kuruczAtms.keys())[hiVIdx] if VTurb == loV or loV == hiV: # if False: print('\tMatched low VTurb {0:+4.2f} :'.format(loV)) return interpForMetal(kuruczAtms[loV], pradks[loV], Metal, TEff, LogG) elif VTurb == hiV: # if False: print('\tMatched high VTurb {0:+4.2f} :'.format(hiV)) return interpForMetal(kuruczAtms[hiV], pradks[hiV], Metal, TEff, LogG) else: # if False: print('\tLow VTurb {0:+4.2f} :'.format(loV)) loVModel, lopradk = interpForMetal(kuruczAtms[loV], pradks[loV], Metal, TEff, LogG) # if False: print('\tHigh VTurb {0:+4.2f} :'.format(hiV)) hiVModel, hipradk = interpForMetal(kuruczAtms[hiV], pradks[hiV], Metal, TEff, LogG) linInterpFactor = (VTurb - loV) / (hiV - loV) return ((1 - linInterpFactor) * loVModel + linInterpFactor * hiVModel), ((1 - linInterpFactor) * lopradk + linInterpFactor * hipradk)
def interpForTEff(kuruczAtms, pradks, TEff, LogG): # Perform a linear interpolation between the closest two TEff models TeffLogGChoices = kuruczAtms.keys() TeffChoices = sorted( set([ tLogGTuple[0] for tLogGTuple in TeffLogGChoices if tLogGTuple[1] == LogG ])) loTeffIdx, hiTeffIdx = u.bracket(TEff, TeffChoices) loTeff = TeffChoices[loTeffIdx] hiTeff = TeffChoices[hiTeffIdx] if TEff == loTeff or hiTeff == loTeff: # # if False: print('\t\t\tMatched low TEff. {0:5.0f} :'.format(loTeff)) theModel = np.array(kuruczAtms[(loTeff, LogG)]), pradks[(loTeff, LogG)] elif TEff == hiTeff: # # if False: print('\t\t\tMatched high TEff. {0:5.0f} :'.format(hiTeff)) theModel = np.array(kuruczAtms[(hiTeff, LogG)]), pradks[(hiTeff, LogG)] else: # # if False: print('\t\t\tLow TEff {0:5.0f} :'.format(loTeff)) loTEffModel = np.array(kuruczAtms[(loTeff, LogG)]) lopradk = pradks[(loTeff, LogG)] # # if False: print('\t\t\tHigh TEff {0:5.0f} :'.format(hiTeff)) hiTEffModel = np.array(kuruczAtms[(hiTeff, LogG)]) hipradk = pradks[(hiTeff, LogG)] linInterpFactor = (TEff - loTeff) / (hiTeff - loTeff) theModel = ( (1 - linInterpFactor) * loTEffModel + hiTEffModel * linInterpFactor ), (1 - linInterpFactor) * lopradk + hipradk * linInterpFactor return theModel
def interpForLogG(kuruczAtms, pradks, TEff, LogG): # Perform a log interpolation between the closest two LogG models TeffLogGChoices = kuruczAtms.keys() logGChoices = sorted(set([tLogGTuple[1] for tLogGTuple in TeffLogGChoices])) loLogGIdx, hiLogGIdx = u.bracket(LogG, logGChoices) loLogG = logGChoices[loLogGIdx] hiLogG = logGChoices[hiLogGIdx] if len(logGChoices) == 1 or LogG == loLogG or hiLogGIdx == 0: # This will also handle calls where LogG is below the range # # if False: print('\t\tMatched low logG. {0:+4.2f} :'.format(loLogG)) return interpForTEff(kuruczAtms, pradks, TEff, loLogG) elif LogG == hiLogG: # # if False: print('\t\tMatched high logG. {0:+4.2f} :'.format(hiLogG)) return interpForTEff(kuruczAtms, pradks, TEff, hiLogG) elif LogG > hiLogG: # In the case of LogGs above the available range, we will extrapolate # using the highest two available tables. This should be the only case # where the linear interpolation factor is greater than 1. # Note that we assume that the limit of how high we will interpolate # is set and enforced elsewhere. It's probably not good to # interpolate more than 0.5-1.0 above the actual grids' range. # Included, but not required # hiLogGIdx = -1; loLogGIdx = -2 loLogG = logGChoices[-2] hiLogG = logGChoices[-1] loLogGModel, lopradk = interpForTEff(kuruczAtms, pradks, TEff, loLogG) hiLogGModel, hipradk = interpForTEff(kuruczAtms, pradks, TEff, hiLogG) # Note the interpolation is slightly different, as we are above the high value, # and we want to use the high value, as opposed to the "low", as our "base". linInterpFactor = ((10**LogG - 10**hiLogG) / (10**hiLogG - 10**loLogG)) return ((1 + linInterpFactor) * hiLogGModel - loLogGModel * linInterpFactor), ( (1 + linInterpFactor) * hipradk - lopradk * linInterpFactor) else: # # if False: print('\t\tLow logG {0:+4.2f} :'.format(loLogG)) loLogGModel, lopradk = interpForTEff(kuruczAtms, pradks, TEff, loLogG) # # if False: print('\t\tHigh logG {0:+4.2f} :'.format(hiLogG)) hiLogGModel, hipradk = interpForTEff(kuruczAtms, pradks, TEff, hiLogG) linInterpFactor = u.linlogfraction(loLogG, hiLogG, LogG) return ((1 - linInterpFactor) * loLogGModel + hiLogGModel * linInterpFactor), ( (1 - linInterpFactor) * lopradk + hipradk * linInterpFactor)
def interpForMass(kuruczAtms, pradks, TEff, LogG, Metal, VTurb, mass): # The atmospheric models we are passed are 5-D (MARCS Spherical), 4-D # (MARCS Plane Parallel), or 3-D (ATLAS) grids with Mass, (micro-) turbulent # velocity, metallicity, Teff, and LogG as axes. 4-D models omit mass, and 3-D # models also omit turbulent velocity. # This functions finds the set of 'bracketing models for mass, and interpolates # between them. # Note: 4-D MARCS, and 3-D ATLAS models assume mass=1.0, but index it as 0.0 loMassIdx, hiMassIdx = u.bracket(mass, sorted(kuruczAtms.keys())) loMass = sorted(kuruczAtms.keys())[loMassIdx] hiMass = sorted(kuruczAtms.keys())[hiMassIdx] if mass == loMass or loMass == hiMass: # if False: print('\tMatched low mass {0:+4.2f} :'.format(loMass)) return interpForVelocity(kuruczAtms[loMass], pradks[loMass], TEff, LogG, Metal, VTurb) elif mass == hiMass: # if False: print('\tMatched high mass {0:+4.2f} :'.format(hiMass)) return interpForVelocity(kuruczAtms[hiMass], pradks[hiMass], TEff, LogG, Metal, VTurb) else: # if False: print('\tLow mass {0:+4.2f} :'.format(loMass)) loMassModel, lopradk = interpForVelocity(kuruczAtms[loMass], pradks[loMass], TEff, LogG, Metal, VTurb) # if False: print('\tHigh mass {0:+4.2f} :'.format(hiMass)) hiMassModel, hipradk = interpForVelocity(kuruczAtms[hiMass], pradks[hiMass], TEff, LogG, Metal, VTurb) linInterpFactor = (mass - loMass) / (hiMass - loMass) return ((1 - linInterpFactor) * loMassModel + linInterpFactor * hiMassModel), ( (1 - linInterpFactor) * lopradk + linInterpFactor * hipradk) return interpForVelocity(kuruczAtms[mass], pradks[mass], TEff, LogG, Metal, VTurb)
def interpForMetal(kuruczAtms, pradks, Metal, TEff, LogG): # Perform a log interpolation between the closest two metallicity models loMetIdx, hiMetIdx = u.bracket(Metal, sorted(kuruczAtms.keys())) loMetal = sorted(kuruczAtms.keys())[loMetIdx] hiMetal = sorted(kuruczAtms.keys())[hiMetIdx] if Metal == loMetal or loMetal == hiMetal: # if False: print('\tMatched low Met. {0:+4.2f} :'.format(loMetal)) return interpForLogG(kuruczAtms[loMetal], pradks[loMetal], TEff, LogG) elif Metal == hiMetal: # if False: print('\tMatched high Met. {0:+4.2f} :'.format(hiMetal)) return interpForLogG(kuruczAtms[hiMetal], pradks[hiMetal], TEff, LogG) else: # if False: print('\tLow Met. {0:+4.2f} :'.format(loMetal)) loMetModel, lopradk = interpForLogG(kuruczAtms[loMetal], pradks[loMetal], TEff, LogG) # if False: print('\tHigh Met. {0:+4.2f} :'.format(hiMetal)) hiMetModel, hipradk = interpForLogG(kuruczAtms[hiMetal], pradks[hiMetal], TEff, LogG) linInterpFactor = u.linlogfraction(loMetal, hiMetal, Metal) return ((1 - linInterpFactor) * loMetModel + linInterpFactor * hiMetModel), ( (1 - linInterpFactor) * lopradk + linInterpFactor * hipradk)
def EvenSpacedIso(IsoPoints, numPoints=200): # The PARSEC isochrones have points placed along the isochrone in such a way as # to produce an exact curve as possible to the model. This means that there are # a higher concentration of points along areas of the curve that are changing # rapidly. For probability calculations, we need points that are equidistantly # spaced along the curve (in terms of arc length). This function takes a passed # set of isochrone data and returns data set with the passed number of points # spaced along the isochrone. Note: for future reference, it would probably be # more physically appropriate to concentrate more points along the curve, based # on the (IMF) stellar population. # x = IsoPoints[:, 0] xmin = min(x) x = x - xmin xnorm = max(x) x = [i / xnorm for i in x] y = IsoPoints[:, 1] ymin = min(y) y = y - ymin ynorm = max(y) y = [i / ynorm for i in y] Dx = np.diff(x) Dy = np.diff(y) Dl = np.sqrt(Dx**2 + Dy**2) # L contains the arclength coordinate for our given X-Y system L = np.cumsum(Dl) totalArclen = L[-1] # also = np.sum(Dl) # S contains the interpolated, evenly spaced arclength coordinates S = np.linspace(0., totalArclen, numPoints) xS = [x[0]] yS = [y[0]] # Remember that x and y are not monotonic, so the usual interpolation # won't work... for sLen in S[1:-1]: loCoord, hiCoord = u.bracket(sLen, L.tolist()) if (L[hiCoord] - L[loCoord]) != 0.: delta = (sLen - L[loCoord]) / (L[hiCoord] - L[loCoord]) elif loCoord == 0 and sLen < L[0]: # Want a point before we started measuring hiCoord = 1 delta = (sLen - L[0]) / (L[1] - L[0]) # delta will be negative else: delta = 0. loCoord = loCoord + 1 hiCoord = hiCoord + 1 xS.append(x[loCoord] + delta * (x[hiCoord] - x[loCoord])) yS.append(y[loCoord] + delta * (y[hiCoord] - y[loCoord])) xS.append(x[-1]) yS.append(y[-1]) xN = [i * xnorm + xmin for i in xS] yN = [i * ynorm + ymin for i in yS] evenIso = np.array(zip(xN, yN)) return evenIso
def measureVoigtEQWs(specFilename, lineData=None): global gBaselineSpline objectName, numApertures, apertureSize, apBoundaries = SD.ReadSpectraInfo( specFilename) specList = SD.LoadAndSmoothSpectra(specFilename) measuredLines = [] badLines = [] dopplerCheck = 0. dopplerCount = 0. for smoothSpec in specList: wls = smoothSpec[0] fluxes = smoothSpec[1] if not SD.IsGoodAperture(smoothSpec): continue if lineData == None: lineData, unusedRefs = LL.getLines(wavelengthRange=[(wls[0], wls[-1])], dataFormat=None) for line in lineData: # lineData entries are variable length, depending on how many # optional parameters are included. So, we have to manually assign # the ones we need. wl = line[0] ion = line[1] ep = line[2] gf = line[3] if len(line) > 4: # This is the only optional parameter we might need. c6 = line[4] else: c6 = '' wlUpper = wl + k.HalfWindowWidth wlLower = wl - k.HalfWindowWidth loApPix = u.closestIndex(wls, wlLower) centerPix = u.closestIndex(wls, wl) hiApPix = u.closestIndex(wls, wlUpper) # The algorithm for curve_fit needs values "close" to one. So, # we normalize our wavelength-calibrated spectrum window to be # centered at "0", and range from -2 to +2. We also assume that # our continuum normalized spectrum has amplitudes near 1. x = wls[loApPix:hiApPix + 1] - wls[centerPix] y = fluxes[loApPix:hiApPix + 1] # At some point, I suppose we should use the actual S/N of the spectra # for the error array... e = 0.01 * np.ones(len(x)) if len(x) < 10: # Line is too close to the order boundary, so skip it badLines.append([ wl, ion, k.badSpectralRegion, 'Too close to order boundary' ]) continue try: # Because of the normalization needed for curve fitting, we need to perform # some shenanigans on the baseline spline rawSpline = BL.FitSplineBaseline(loApPix, hiApPix, smoothSpec) gBaselineSpline = interp.UnivariateSpline( x, rawSpline(x + wls[centerPix])) except: # We've had errors when trying to fit bad regions - just skip to the next one print('Fitting error for {0:4.3f}, ({1:2.1f}).'.format( wl, ion)) continue maxIdxs = [ mx for mx in sig.argrelmax(y)[0].tolist() if y[mx] < 2.5 ] maxIdxs.append(0) maxIdxs.sort() maxIdxs.append(-1) minIdxs = [ mn for mn in sig.argrelmin(y)[0].tolist() if y[mn] > 0.05 ] minIdxs.append(0) minIdxs.sort() minIdxs.append(-1) mins = [x[idx] for idx in minIdxs] closestMin = mins.index(u.closest(0, mins)) # Find the two local maxes which bracket the central wavelength (offset=0.) maxRange = u.bracket(0., [x[idx] for idx in maxIdxs]) fitRange = [maxIdxs[maxRange[0]], maxIdxs[maxRange[1]] + 1] # guess that the scale is the difference between the local max and mins, # although we could opt to use the difference between the continuum and the min... scaleGuess = (y[maxIdxs[maxRange[0]]] + y[maxIdxs[maxRange[1]]] ) / 2. - y[minIdxs[closestMin]] # The line center should occur at, or really close to the offset offsetGuess = 0.0 # Gamma and sigma are Lorenzian and Gaussian parms...need a better idea for guesses gammaGuess = 0.10 sigmaGuess = 0.10 # The core fraction is basically how much of the line is "pure" Gaussian coreFractionGuess = 0.65 guessParms = [ scaleGuess, offsetGuess, gammaGuess, sigmaGuess, coreFractionGuess ] curveFitted = False if len(x[fitRange[0]:fitRange[1]]) < 5: # Way too few points to even do an interpolation. Try extending to the # two "maxes" on either side of the current loRange = 0 if maxRange[0] < 2 else maxRange[0] - 1 hiRange = -1 if maxRange[1] > len( maxIdxs) - 2 or maxRange[1] == -1 else maxRange[1] + 1 fitRange = [ maxIdxs[loRange], maxIdxs[hiRange] + 1 if hiRange > 0 else -1 ] xToFit = x[fitRange[0]:fitRange[1]] yToFit = y[fitRange[0]:fitRange[1]] eToFit = e[fitRange[0]:fitRange[1]] if len(x[fitRange[0]:fitRange[1]]) < 5: # STILL too small of a range - Bail, with a bad line. badLines.append( [wl, ion, k.badSpectralRegion, 'Line region too small']) continue if len(x[fitRange[0]:fitRange[1]]) < 20: # Too few points? # Try adding more (interpolated) data points, along a spline if fitRange[1] == -1: ySpline = interp.interp1d(x[fitRange[0]:], y[fitRange[0]:], kind='cubic') eSpline = interp.interp1d(x[fitRange[0]:], e[fitRange[0]:], kind='cubic') xToFit = np.linspace(x[fitRange[0]], x[-1], num=100) yToFit = ySpline(xToFit) eToFit = eSpline(xToFit) else: ySpline = interp.interp1d(x[fitRange[0]:fitRange[1] + 1], y[fitRange[0]:fitRange[1] + 1], kind='cubic') eSpline = interp.interp1d(x[fitRange[0]:fitRange[1] + 1], e[fitRange[0]:fitRange[1] + 1], kind='cubic') xToFit = np.linspace(x[fitRange[0]], x[fitRange[1]], num=100) yToFit = ySpline(xToFit) eToFit = eSpline(xToFit) else: xToFit = x[fitRange[0]:fitRange[1]] yToFit = y[fitRange[0]:fitRange[1]] eToFit = e[fitRange[0]:fitRange[1]] try: popt, pcov = fitCurve(xToFit, yToFit, eToFit, guessParms) if popt[4] > 0. and popt[4] < 1.: # Even though we have a fit, if it's "pure" Lorenzian or Gaussian # pretend we can do better with some data manipulation curveFitted = True # else: # curveFitted = False except RuntimeError: # Occurs when curve_fit cannot converge (Line not fit) # We need to catch it in order to try our own data manipulation # for a good fit. # print('Initial fit failure for line at: %.3f' % wl) pass if not curveFitted: # Try to refit, by addressing typical issue(s) # Assume not enough of the line is visible (no wings) # Add ~10 points along the continuum, in the "wings" loBound = fitRange[0] - 5 if loBound < 0: loBound = 0 if fitRange[1] == -1: hiBound = -1 newY = np.concatenate( (gBaselineSpline(x[loBound:fitRange[0]]), y[fitRange[0]:hiBound])) else: hiBound = fitRange[1] + 5 if hiBound > len(x) - 1: hiBound = -1 newY = np.concatenate( (gBaselineSpline(x[loBound:fitRange[0]]), y[fitRange[0]:fitRange[1] + 1], gBaselineSpline(x[fitRange[1] + 1:hiBound]))) newX = x[loBound:hiBound] newE = e[loBound:hiBound] if len(newX) != len(newY) or len(newX) != len(newE): # Probably just have an indexing problem in the code immediately above, # but just skip the line for now. badLines.append([wl, ion, k.unknownReason, k.unknownStr]) continue try: popt, pcov = fitCurve(newX, newY, newE, guessParms) # As long as something fits, here - we'll take it. curveFitted = True except RuntimeError: # Well, and truly screwed? # pyplot.errorbar(x+smoothSpec.xarr[centerPix], y, yerr=e, linewidth=1, color='black', fmt='none') # pyplot.show() badLines.append([wl, ion, k.unknownReason, k.unknownStr]) continue eqw, eqwError = calcEQW(popt, pcov, x) # showLineFit(popt, pcov, x, y, e, smoothSpec.xarr[centerPix].value) # FWHM of the Gaussian core is sqrt(8*ln(2))*sigma = 2.355*sigma # The factor of 1000 is to convert to miliangstroms. measuredLines.append( [wl, ion, ep, gf, c6, eqw, wl + popt[1], 2355. * popt[3]]) thisDoppler = ((popt[1]) / wl) * k.SpeedofLight dopplerCheck += thisDoppler dopplerCount += 1 # if popt[4] <= 0.: # print('Best fit is a pure Lorenzian') # elif popt[4] >= 1.: # print('Best fit is a pure Gaussian') # nans = len([x for x in measuredLines if np.isnan(x[5])]) badLines.extend([[x[0], x[1], k.badEQWMValue, k.nanStr] for x in measuredLines if np.isnan(x[5])]) badLines.extend( [[x[0], x[1], k.emissionLine, "Emission strength: %.3f" % (abs(x[5]))] for x in measuredLines if x[5] < 0.]) badLines.extend([[ x[0], x[1], k.centerWLOff, "Line center delta: %.3f" % (x[0] - x[6]) ] for x in measuredLines if abs(x[0] - x[6]) > 0.25]) # print("nan eqw: %d - emission: %d - bad offset: %d" % (nans, emiss, bads)) # # for line in sorted(measuredLines, key=lambda x: x[0]): # if not np.isnan(line[5]) and line[5]>0. and abs(line[0]-line[6]) < 0.25: # print("%2.1f :%4.3f(%4.3f) = %.1f (%.1f)" % (line[1], line[0], line[6], line[5], line[7])) # Measured WL within 250mA of expected, non-emission lines, only filteredLines = [ l for l in measuredLines if not np.isnan(l[5]) and l[5] > 0. and abs(l[0] - l[6]) < 0.25 ] # Want ascending wls within ascending ions filteredLines.sort(key=lambda x: x[0]) filteredLines.sort(key=lambda x: x[1]) # print "*****************" # print "*******{0} bad *********".format(len(badLines)) # print "*******{0} measured *********".format(len(measuredLines)) # print "*******{0} filtered **********".format(len(filteredLines)) return filteredLines, badLines, dopplerCount, dopplerCheck