https://github.com/longjp/rr-templates
Tip revision: 926570f132964b4a421292a8ec37ed9ef1c21acc authored by James P Long on 28 November 2021, 04:49:20 UTC
removed two comments
removed two comments
Tip revision: 926570f
fit_template.R
## functions for fitting rr lyrae light curves
## fits RR Lyrae template
##
## arguments
## lc : light curve, data frame with columns time, band, mag, error
## omegas : vector of frequencies
## tem : input templates, see make_template.R for a description
## NN : number of newton steps at each frequency
## use.errors : should photometric errors be used
## use.dust : should dust (E[B-V]) be fit
##
##
## value
## rss : the residual sum of squares at each frequency in omegas
FitTemplate <- function(lc,omegas,tem,NN=5,use.errors=TRUE,use.dust=TRUE){
if(use.dust){
use.dust <- CheckNumberBands(lc)
}
CheckLC(lc)
## center times at 0, makes phi a smoother function of omega
lc[,1] <- lc[,1] - mean(lc[,1])
tem <- CheckTemLC(tem,lc)
dat <- AugmentData(lc,tem,use.errors)
m <- dat[[1]]$mag
dust <- dat[[1]]$dust
t <- dat[[1]]$time
weights <- 1 / dat[[1]]$error^2
nb <- dat[[2]]
coeffs <- c(0,0,0,runif(1))
##rss_max <- sum((lm(m~dust,weights=weights)$residuals^2)*weights)
rss <- rep(0,length(omegas))
betas <- tem$abs_mag(1/omegas,tem) ## obtain absolute magnitudes at all frequencies
for(ii in 1:length(omegas)){
m_temp <- m - rep.int(betas[ii,],nb) ## correct for absolute magnitude
for(jj in 1:NN){
coeffs <- NewtonUpdate(coeffs[4],omegas[ii],m_temp,t,dust,weights,nb,
tem$template_funcs,tem$templated_funcs,use.errors,use.dust)
}
gammaf <- ConstructGamma(t,nb,coeffs[4],omegas[ii],tem$template_funcs)
resid <- m_temp - coeffs[1] - coeffs[2]*dust - coeffs[3]*gammaf
##rss[ii] <- min(sum(weights*resid^2),rss_max)
rss[ii] <- sum(weights*resid^2)
}
return(rss)
}
## for a given omega, return coefficients
## example usage: run FitTemplate to determine rss as function of omegas,
## find omega which minimizes rss, then find coeffs for this omega using ComputeCoeffs
##
## arguments
## lc : light curve, data frame with columns time, band, mag, error
## omega : frequency
## tem : input templates
## NN : number of newton steps, probably 10+ since no warm start
## use.errors : should photometric errors be used
## use.dust : should dust (E[B-V]) be fit
##
##
## value
## coeffs : vector of [distance mod,ebv,peak-to-peak g amp,phase]
ComputeCoeffs <- function(lc,omega,tem,NN=20,use.errors=TRUE,use.dust=TRUE){
if(use.dust){
use.dust <- CheckNumberBands(lc)
}
CheckLC(lc)
## center times at 0, makes phi a smoother function of omega
mean_time <- mean(lc[,1])
lc[,1] <- lc[,1] - mean_time
tem <- CheckTemLC(tem,lc)
dat <- AugmentData(lc,tem,use.errors)
m <- dat[[1]]$mag
dust <- dat[[1]]$dust
t <- dat[[1]]$time
weights <- 1 / dat[[1]]$error^2
nb <- dat[[2]]
##lc$mag <- lc$mag - rep.int(tem$betas,nb)
m <- m - rep.int(tem$abs_mag(1/omega,tem)[1,],nb)
coeffs <- c(0,0,0,runif(1))
J <- 0
## J prevents infinite loops
while(coeffs[3]==0 & J < 10){
for(jj in 1:NN){
coeffs <- NewtonUpdate(coeffs[4],omega,m,t,dust,weights,nb,
tem$template_funcs,tem$templated_funcs,use.errors,use.dust)
}
J <- J + 1
}
## shift phase back to original scale
coeffs[4] <- (coeffs[4] - omega*mean_time) %% 1
return(coeffs)
}
## for a given omega, phi return coefficients mu, dust, amp
## example usage: run FitTemplate to determine rss as function of omegas,
## find omega which minimizes rss, then find coeffs for this omega using ComputeCoeffs
##
## arguments
## lc : light curve, data frame with columns time, band, mag, error
## omega : frequency
## phi : phase
## tem : input templates
## use.errors : should photometric errors be used
## use.dust : should dust (E[B-V]) be fit
##
##
## value
## coeffs : vector of [distance mod,ebv,peak-to-peak g amp,phase]
ComputeCoeffsPhase <- function(lc,omega,phi,tem,use.errors=TRUE,use.dust=TRUE){
if(use.dust){
use.dust <- CheckNumberBands(lc)
}
CheckLC(lc)
tem <- CheckTemLC(tem,lc)
dat <- AugmentData(lc,tem,use.errors)
m <- dat[[1]]$mag
dust <- dat[[1]]$dust
t <- dat[[1]]$time
weights <- 1 / dat[[1]]$error^2
nb <- dat[[2]]
m <- m - rep.int(tem$abs_mag(1/omega,tem)[1,],nb)
coeffs <- AmpMuDustUpdate(phi,omega,m,t,dust,weights,nb,tem$template_funcs,use.errors,use.dust)
return(c(coeffs,phi))
}
## make predictions at a set of times (in single band)
##
##
## arguments
## times : times to make predictions
## band : band to make prediction
## omega : frequency
## coeffs : coefficients from model fit
## tem : input templates
##
##
## value
## m : vector of predicted magnitudes
PredictSingleBand <- function(times,band,omega,coeffs,tem){
t_temp <- (times*omega + coeffs[4]) %% 1.0
m <- (coeffs[1] + tem$abs_mag(1/omega,tem)[1,band] + coeffs[2]*tem$dust[band] +
coeffs[3]*tem$template_funcs[[band]](t_temp))
return(m)
}
## make predictions at a set of times (in all bands)
##
##
## arguments
## times : times to make predictions
## omega : frequency
## coeffs : coefficients from model fit
## tem : input templates
##
##
## value
## m : matrix of predictions, columns are bands, rows times
PredictAllBand <- function(times,omega,coeffs,tem){
bands <- colnames(tem$betas)
m <- vapply(bands,function(x){PredictSingleBand(times,x,omega,coeffs,tem)},rep(0,length(times)))
return(m)
}
## make predictions at a set of times in particular bands
##
##
## arguments
## times : times to make predictions
## bands : bands[ii] is band of observation times[ii]
## omega : frequency
## coeffs : coefficients from model fit
## tem : input templates
##
##
## value
## m : vector of predicted magnitudes
PredictTimeBand <- function(times,bands,omega,coeffs,tem){
m <- rep(0,length(times))
bands_unique <- unique(bands)
for(ii in bands_unique){
m[bands==ii] <- PredictSingleBand(times[bands==ii],ii,omega,coeffs,tem)
}
return(m)
}
## grid search across phase at fixed omega.
## possible uses:
## 1. optimize parameter fits AFTER selecting frequency
## 2. test that newton algorithm is finding best parameter fits
##
## arguments
## lc : light curve, data frame with columns time, band, mag, error
## omega : frequency
## tem : input templates
## phis : grid of phases to try
## use.errors : should photometric errors be used
## use.dust : should dust (E[B-V]) be fit
##
##
## value
## rss : the residual sum of squares at each phase in grid
ComputeRSSPhase <- function(lc,omega,tem,phis=(1:100)/100,use.errors=TRUE,use.dust=TRUE){
if(use.dust){
use.dust <- CheckNumberBands(lc)
}
CheckLC(lc)
tem <- CheckTemLC(tem,lc)
dat <- AugmentData(lc,tem,use.errors)
m <- dat[[1]]$mag
dust <- dat[[1]]$dust
t <- dat[[1]]$time
weights <- 1 / dat[[1]]$error^2
nb <- dat[[2]]
m <- m - rep.int(tem$abs_mag(1/omega,tem)[1,],nb)
rss_max <- sum((lm(m~dust,weights=weights)$residuals^2)*weights)
rss <- rep(0,length(phis))
for(ii in 1:length(phis)){
coeffs <- AmpMuDustUpdate(phis[ii],omega,m,t,dust,weights,nb,
tem$template_funcs,use.errors,use.dust)
gammaf <- ConstructGamma(t,nb,phis[ii],omega,tem$template_funcs)
resid <- m - coeffs[1] - coeffs[2]*dust - coeffs[3]*gammaf
rss[ii] <- min(sum(weights*resid^2),rss_max)
}
return(rss)
}
##### NOTE:
##### below are mostly helper functions that
##### are unlikely to be useful for direct calling
## coerces lc into form for model to fit
AugmentData <- function(lc,tem,use.errors){
lc <- lc[order(lc$band),]
nb <- table(lc$band)
lc$dust <- rep.int(tem$dust,nb)
##lc$mag <- lc$mag - rep.int(tem$betas,nb)
lc$band <- NULL
if(use.errors){
lc$error <- sqrt(lc$error^2 + rep.int(tem$model_error,nb)^2) ## adds model error to photometric error
} else {
lc$error <- 1
}
return(list(lc=lc,nb=nb))
}
## makes \gamma_b(omega*t + \phi) for times t where
## t is ordered (by band) set of times
ConstructGamma <- function(t,nb,phi,omega,temp_funcs){
t <- (t*omega + phi) %% 1
bix <- c(0,cumsum(nb))
gammaf <- rep(0,length(t))
for(jj in 1:(length(bix)-1)){
ix1 <- (bix[jj]+1)
ix2 <- bix[jj+1]
if(ix2 >= ix1){
gammaf[ix1:ix2] <- temp_funcs[[jj]](t[ix1:ix2])
}
}
return(gammaf)
}
## computes a newton update for the (mu,a,d,phi) parameter vector
NewtonUpdate <- function(phi,omega,m,t,dust,weights,nb,template_funcs,templated_funcs,use.errors,use.dust){
gammaf <- ConstructGamma(t,nb,phi,omega,template_funcs)
if(use.dust){
est <- ComputeBeta(m,dust,gammaf,weights,use.errors)
mu <- est["mu"]
a <- est["a"]
d <- est["d"]
} else {
est <- ComputeBetaOne(m,gammaf,weights,use.errors)
mu <- est["mu"]
a <- est["a"]
d <- 0
}
if(a > 0){
gammafd <- ConstructGamma(t,nb,phi,omega,templated_funcs)
mp <- m - mu - d*dust
if(use.errors){
del <- sum(gammafd*(mp-a*gammaf)*weights)
h <- a*sum(gammafd*gammafd*weights)
} else {
del <- sum(gammafd*(mp-a*gammaf))
h <- a*sum(gammafd*gammafd)
}
phi <- (phi + h^{-1}*del) %% 1
} else {
a <- 0
phi <- runif(1)
}
out <- c(mu,d,a,phi=phi)
names(out) <- NULL
return(out)
}
## update for the (mu,a,d) parameter vector (closed form because phi fixed)
AmpMuDustUpdate <- function(phi,omega,m,t,dust,weights,nb,template_funcs,use.errors,use.dust){
gammaf <- ConstructGamma(t,nb,phi,omega,template_funcs)
if(use.dust){
est <- ComputeBeta(m,dust,gammaf,weights,use.errors)
mu <- est["mu"]
a <- est["a"]
d <- est["d"]
} else {
est <- ComputeBetaOne(m,gammaf,weights,use.errors)
mu <- est["mu"]
a <- est["a"]
d <- 0
}
if(a < 0) {
a <- 0
}
out <- c(mu,d,a)
names(out) <- NULL
return(out)
}
## finds best fitting mu (distance mod) , d (i.e. ebv), a (amplitude)
ComputeBeta <- function(m,dust,gammaf,weights,use.errors){
X <- cbind(mu=1,d=dust,a=gammaf)
if (use.errors) {
B <- t(X)%*%(X*weights)
d <- t(X)%*%(m*weights)
} else {
B <- t(X)%*%X
d <- t(X)%*%m
}
z <- solve(B,d)
return(z[,1])
}
## finds best fitting mu (distance mod) , a (amplitude)
## does not fit dust, so ebv set to 0
ComputeBetaOne <- function(m,gammaf,weights,use.errors){
X <- cbind(mu=1,a=gammaf)
if (use.errors) {
B <- t(X)%*%(X*weights)
d <- t(X)%*%(m*weights)
} else {
B <- t(X)%*%X
d <- t(X)%*%m
}
z <- solve(B,d)
return(z[,1])
}
## check and make tem and lc consistent
## if lc has bands not in tem, stop
## if lc has fewer bands then tem, get rid
## of these bands in tem
CheckTemLC <- function(tem,lc){
if(prod(unique(lc$band) %in% names(tem$dust)) != 1){
print("template bands are:")
print(names(tem$dust))
print("lc is:")
print(lc)
stop("all lc bands must match template names")
}
bs <- names(tem$dust)[names(tem$dust) %in% unique(lc$band)]
if(length(bs) < length(tem$dust)){
tem$betas <- tem$betas[,bs,drop=FALSE]
tem$dust <- tem$dust[bs]
tem$model_error <- tem$model_error[bs]
tem$templates <- tem$templates[bs,]
tem$templatesd <- tem$templatesd[bs,]
tem$template_funcs <- tem$template_funcs[bs]
tem$templated_funcs <- tem$templated_funcs[bs]
}
return(tem)
}
## useful for calling these functions from python, see template.py for example
TBMEtoLC <- function(time,band,mag,error){
return(data.frame(time,band,mag,error,stringsAsFactors=FALSE))
}
## if lc has only one band, need to use ComputeBetaOne rather than ComputeBeta
## this function checks / warns if using single band
CheckNumberBands <- function(lc){
if(length(unique(lc$band))==1){
print("warning: light curve has only 1 band, setting E[B-V] = 0 (i.e. assume no dust). the distance modulus is now the band mean and has no physical interpretation unless the light curve was already dust corrected. set use.dust=FALSE to prevent this warning message from being displayed.")
use.dust <- FALSE
} else {
use.dust <- TRUE
}
return(use.dust)
}
## checks that lc has correct column structure / sufficient rows
CheckLC <- function(lc){
if(nrow(lc)<5){
stop("lc must have at least 5 rows (observations)")
}
if(sum(names(lc) %in% c("time","band","mag","error")) != 4){
stop("lc must have columns named: time, band, mag, error")
}
if(typeof(lc$time)!="double"){
stop("typeof(lc$time) must be double")
}
if(typeof(lc$mag)!="double"){
stop("typeof(lc$mag) must be double")
}
if(typeof(lc$error)!="double"){
stop("typeof(lc$error) must be double")
}
if(typeof(lc$band)!="character"){
stop("typeof(lc$band) must be character")
}
}