R/HeadingCorrection.R

#' @title CorrectHeading
#' @description Calculate an estimate of the sequence of errors in heading.
#' @details For a data.frame supplied with variables representing the attitude angles,
#' body accelerations, GPS-measured ground velocities, and altitude and latitude,
#' estimate the error in the heading via comparison of the measured body accelerations,
#' transformed to the Earth-relative frame, to the accelerations determined via 
#' differentiation of the GPS-measured ground-speed components. Find the correction
#' to heading that provides the best match between these two sources of acceleration.
#' The result is the estimated error, so this should be **subtracted** from the 
#' measured value to get the corrected value. Results are sensitive to timing
#' delays among the variables, so these should be corrected prior to calling this routine.
#' In addition, if there is a variable "Valid" in the data.frame, it should be logical
#' and will be used to restrict the calculation of the corrections to the subset where
#' Valid is TRUE. This can be used, for example, to exclude slow-flight regions where
#' flaps might be deployed, high-rate-of-climb regions, etc.
#' @author William Cooper
#' @export CorrectHeading
#' @import zoo
#' @importFrom signal sgolayfilt
#' @importFrom stats sd weighted.mean smooth.spline predict
#' @param .data A data.frame containing measurements of body accelerations (called
#' BLATA, BLONGA, BNORMA), ground-speed components from GPS (GGVEW, GGVNS), attitude
#' angles in units of degrees (PITCH, ROLL, THDG, the latter the heading relative to
#' true north), and the latitude and altitude (LAT and GGALT), the latter two for
#' finding the local acceleration of gravity. If variables named PITCHC, ROLLC, and
#' LATC are present those will be used instead. The first two might be produced by
#' prior use of the CorrectPitch() function. The data.frame variables must retain
#' the original attributes from the netCDF file so that, if arbitrary "CalibrationCoefficients"
#' have been applied these can be removed. The reason is that this routine needs the
#' original unmodified value from the IRU for variables like THDG.
#' @param .span The number of points (default 21) used for estimating the accelerations
#' by differentiation and for smoothing the results. 
#' @param .default The default value to use if the routine does not find enough
#' qualified turns to develop a valid estimate of the heading error. The default
#' is -0.08 deg., as applies to the GV data for DEEPWAVE.
#' @param .Valid An optional logical vector of length matching the rows in .data,
#' which should be TRUE for qualified measurements and FALSE for measurements to
#' exclude from the calculation. This can be used to exclude low-airspeed cases
#' (where flaps might be deployed) or cases of rapid climbs, for example. The default
#' is NULL, in which case the test will be skipped.
#' @param .plotfile The name of a plot to generate (e.g., "./Plot1.pdf") that shows
#' the results from the algorithm. The default is 'inLine', in which case the
#' plot is generated and displayed normally. Suppress the plot with a NULL argument.
#' @return A vector of the same length as the supplied data.frame that gives an
#' estimate of the error in heading, the negative of the correction needed.
#' @examples 
#' HeadingCorrection <- CorrectHeading (RAFdata)
CorrectHeading <- function (.data, .span=21, .default=-0.08, .Valid=NULL, .plotfile='inLine') {
  ## note: before calling, should apply timing corrections and pitch/roll corrections
  ##       if desired.
  GeneratePlot <- !is.null (.plotfile) 
  Cradeg <- pi/180
  ## The next correction calculates the correction needed to account for the rotation
  ## of the Earth and of the l-frame (ENU frame). See Noureldin et al., 2013, 
  ## Eqs. 5.55--5.57. Subtract this from the transformed accelerations before using them.
  RotationCorrection <- function (.data, .V) {
    Cradeg <- pi/180
    omegaE <- StandardConstant ('Omega')  ## Earth'r angular velocity
    Re <- StandardConstant ('Re')         ## representative Earth radius, m
    DL <- nrow (.data)
    C <- vector ('numeric', 3*DL); dim(C) <- c(DL,3)
    lat <- .data$LAT * Cradeg
    sinLat <- sin(lat); cosLat <- cos(lat); tanLat <- tan(lat)
    M12 <- -2*omegaE*sinLat-.V[,1]*tanLat/Re
    M13 <- 2*omegaE*cosLat+.V[,1]/Re
    M21 <- 2*omegaE*sinLat+.V[,1]*tanLat/Re
    M23 <- .V[,2]/Re
    M31 <- -2*omegaE*cosLat-.V[,1]/Re
    M32 <- -.V[,2]/Re
    C[,1] <- M12*.V[,2]+M13*.V[,3]
    C[,2] <- M21*.V[,1]+M23*.V[,3]
    C[,3] <- M31*.V[,1]+M32*.V[,2]
    return (C)
  }

  
  ## check for required variables:
  Required <- c("BLATA", "BLONGA", "BNORMA", "GGVNS", "GGVEW", "GGALT",
                "THDG", "PITCH", "ROLL", "VEW", "VNS", "VSPD")
  .names <- names(.data)
  for (.R in Required) {
    if (.R %in% .names) {next}
    print (sprintf ("in CorrectHeading, required variable %s not found; returning 0", .R))
    return(0)
  }
  if (!("LATC" %in% .names) && !("LAT" %in% .names)) {
    print (sprintf ("in CorrectHeading, required variable LAT or LATC not found; returning 0"))
    return(0)
  }
  
  ## get the data rate -- only works with rates of 1 or 25 at present
  Rate <- 1  
  if ((.data$Time[2]-.data$Time[1]) <= 0.04) {Rate <- 25} 
  LD <- nrow(.data)
  D <- .data[!is.na (.data$Time), ]  ## eliminate bad-time records
  
  if (Rate > 1) {
    D1 <- D[(as.numeric(D$Time) %% 1) < 0.01,]  ## extract a 1-Hz data.frame
  } else {
    D1 <- D
  }
  D1 <- transferAttributes(.data, D1)
  
  ## substitute corrected values if present
  if ("PITCHC" %in% names (D1)) {D1$PITCH <- D1$PITCHC}
  if ("ROLLC" %in% names (D1)) {D1$ROLL <- D1$ROLLC}
  if ("LATC" %in% names (D1)) {D1$LAT <- D1$LATC}
  
  # Remove "calibrations" if they have been applied:
  CC <- attr(D1$THDG, 'CalibrationCoefficients')
  if (!is.null(CC[1]) && CC[1] != 0) {
    D1$THDG <- (D1$THDG - CC[1] - 0.05) / CC[2]  ## Temporary small offset seems to help
    # print (sprintf('heading offset of %.2f removed', CC[1]))
  }
  ## The timing of the heading measurement is critical because, if there is a lag,
  ## it will cause a shift dependent on turn direction. Try some shifts to see which
  ## minimizes the difference between right and left turns:
  ## (However, if GGVEW/GGVNS are already shifted, skip this):
  if (is.null (attr(D1$GGVEW, 'TimeLag'))) {
    D1$THDG <- ShiftInTime(D1$THDG, 1, 90, .mod=360)
  }
  ## It is not necessary to record the changes in modified attributes because the
  ## changes are applied to a copy of the original. The conventional time shifts
  ## should be retained in the original, however, for this to work; if they are
  ## removed or changes, e.g., in the KalmanFilter.R script, the original version
  ## of the data file should be used.
  CC <- attr(D1$PITCH, 'CalibrationCoefficients')
  if (!is.null(CC[1])) {
    D1$PITCH <- (D1$PITCH - CC[1]) / CC[2]
  }
  CC <- attr(D1$ROLL, 'CalibrationCoefficients')
  if (!is.null(CC[1])) {
    D1$ROLL <- (D1$ROLL - CC[1]) / CC[2]
  }
  
  #interpolate if necessary:
  MaxGap <- 1000
  D1$ggvns <- zoo::na.approx (as.vector(D1$GGVNS), maxgap=MaxGap, na.rm = FALSE, rule=2)
  D1$ggvew <- zoo::na.approx (as.vector(D1$GGVEW), maxgap=MaxGap, na.rm = FALSE, rule=2)
  D1$BLONGA <- zoo::na.approx (as.vector (D1$BLONGA), maxgap=MaxGap, na.rm=FALSE, rule=2)
  D1$BLATA <- zoo::na.approx (as.vector (D1$BLATA), maxgap=MaxGap, na.rm=FALSE, rule=2)
  D1$BNORMA <- zoo::na.approx (as.vector (D1$BNORMA), maxgap=MaxGap, na.rm=FALSE, rule=2)
  D1$GGALT <- zoo::na.approx (as.vector (D1$GGALT), maxgap=MaxGap, na.rm=FALSE, rule=2)
  D1$PITCH <- zoo::na.approx (as.vector (D1$PITCH), maxgap=MaxGap, na.rm=FALSE, rule=2)
  D1$ROLL <- zoo::na.approx (as.vector (D1$ROLL), maxgap=MaxGap, na.rm=FALSE, rule=2)
  D1$THDG <- zoo::na.approx (as.vector (D1$THDG), maxgap=MaxGap, na.rm=FALSE, rule=2)
  D1$LAT <- zoo::na.approx (as.vector (D1$LAT), maxgap=MaxGap, na.rm=FALSE, rule=2)
  D1$vndot <- signal::sgolayfilt (D1$ggvns, 3, .span, m=1)  # m=1 for first deriv.
  D1$vedot <- signal::sgolayfilt (D1$ggvew, 3, .span, m=1)
  G <- Gravity (D1$LAT, D1$GGALT)
  AB <- matrix(c(D1$BLONGA, D1$BLATA, D1$BNORMA+G), ncol=3) #aircraft-frame
  VL <- matrix(c(D1$VEW, D1$VNS, D1$VSPD), ncol=3) 
  AL <- XformLA (D1, AB)                                    #l-frame
  ALD1 <- AL  # temporary
  ## now corrected for angular effects
  ## See Noureldin et al, 2013, Eq. (5.55)
  AL <- AL - RotationCorrection (D1, VL)
  ALD1c <- AL  # temporary
  
  ## the resulting l-frame accelerations
  D1$LACCX <- AL[, 1]
  D1$LACCY <- AL[, 2]
  D1$LACCZ <- AL[, 3] + G
  
  ## smooth to match GPS-velocity derivatives
  D1$LACCX <- signal::sgolayfilt (D1$LACCX, 3, .span, m=0)
  D1$LACCY <- signal::sgolayfilt (D1$LACCY, 3, .span, m=0)
  D1$LACCZ <- signal::sgolayfilt (D1$LACCZ, 3, .span, m=0)
  
  ## magnitude of horizontal acceleration
  D1$A2 <- D1$LACCX^2 + D1$LACCY^2
  D1$A <- sqrt(D1$A2)
  D1$herr <- (-D1$LACCY*(D1$vedot-D1$LACCX)+D1$LACCX*(D1$vndot-D1$LACCY)) / (Cradeg*D1$A2)
  
  ## a working data.frame, to avoid changing D1
  DT <- D1
  v <- (DT$A > 1) & (abs (DT$herr) < 0.3)
  if (!is.null (.Valid)) {
    v <- v & .Valid
  }
  DT[!v,] <- NA
  DT$TestR <- (!is.na(DT$Time) & (DT$ROLL > 10))     ## right-turn cases
  DT$TestL <- (!is.na(DT$Time) & (DT$ROLL < -10))    ## left-turn cases
  ## create a new data.frame to hold measurements from a sequence of measurements
  ## in turns
  DSET <- data.frame ()
  hmeanR <- vector("numeric")
  hsdR <- vector("numeric")
  tbarR <- vector("numeric")
  hmeanL <- vector("numeric")
  hsdL <- vector("numeric")
  tbarL <- vector("numeric")
  
  setStart <- 0
  NRA <- 40    ## effective averaging seconds for error-bar std. dev.
  NSL <- 25    ## required seconds turn in each direction
  TGAP <- 300  ## seconds constituting break in sets of turn values
  for (k in 1:nrow(DT)) {
    if (DT$TestR[k] || DT$TestL[k]) {
      if (setStart == 0) {
        setStart <- k
        setEnd <- k
        DSET <- rbind(DSET, DT[k,])
      } else if (as.numeric(difftime(DT$Time[k], DT$Time[setEnd], units='sec')) > TGAP) {
        if (length(DSET[DSET$TestR == TRUE, "herr"]) > NSL &&
            length(DSET[DSET$TestL == TRUE, "herr"]) > NSL) {      
          hmnR <- mean (DSET$herr[DSET$TestR], na.rm=TRUE)
          hmnL <- mean (DSET$herr[DSET$TestL], na.rm=TRUE)
          hsdevR <- sd (DSET$herr[DSET$TestR], na.rm=TRUE) / 
            sqrt (length (DSET$TestR)/NRA)
          hsdevL <- sd (DSET$herr[DSET$TestL], na.rm=TRUE) / 
            sqrt (length (DSET$TestL)/NRA)
          tbrR <- mean(DSET$Time[DSET$TestR], na.rm=TRUE)
          tbrL <- mean(DSET$Time[DSET$TestL], na.rm=TRUE)
          # print (sprintf ("R time %s count %d", as.POSIXct (tbrR, origin="2014-07-04", tz="GMT"), length(DSET$TestR)))
          hmeanR <- c(hmeanR, hmnR)
          hsdR   <- c(hsdR, hsdevR)
          tbarR  <- c(tbarR, tbrR)
          # print (sprintf ("L time %s count %d", as.POSIXct (tbrL, origin="2014-07-04", tz="GMT"), length(DSET$TestL)))
          hmeanL <- c(hmeanL, hmnL)
          hsdL   <- c(hsdL, hsdevL)
          tbarL  <- c(tbarL, tbrL)
        }
        setStart <- k
        setEnd <- k
        DSET <- DT[k,]
      } else {
        setEnd <- k
        DSET <- rbind(DSET, DT[k,])
      }
    }
  }
  if (length(DSET[DSET$TestR == TRUE, "herr"]) > NSL &&
      length(DSET[DSET$TestL == TRUE, "herr"]) > NSL) { 
    hmnR <- mean (DSET$herr[DSET$TestR], na.rm=TRUE)
    hmnL <- mean (DSET$herr[DSET$TestL], na.rm=TRUE)
    hsdevR <- sd (DSET$herr[DSET$TestR], na.rm=TRUE) / sqrt (length (DSET$TestR)/NRA)
    hsdevL <- sd (DSET$herr[DSET$TestL], na.rm=TRUE) / sqrt (length (DSET$TestL)/NRA)
    tbrR <- mean(DSET$Time[DSET$TestR], na.rm=TRUE)
    tbrL <- mean(DSET$Time[DSET$TestL], na.rm=TRUE)
    hmeanR <- c(hmeanR, hmnR)
    hsdR   <- c(hsdR, hsdevR)
    tbarR  <- c(tbarR, tbrR)
    hmeanL <- c(hmeanL, hmnL)
    hsdL   <- c(hsdL, hsdevL)
    tbarL  <- c(tbarL, tbrL)
  }
  if (length (tbarL) < 3) {
    print (sprintf ("CorrectHeading found too few qualifying turns (%d); returning default (%.2f)", length (tbarL), .default))
    return (c(rep(.default, LD)))
  }

  ## construct data.frame holding results from each qualifying turn
  EH <- data.frame(tbar=c(tbarL, tbarR), hmeanR=c(rep(NA, length(tbarL)), hmeanR),
                   hmeanL=c(hmeanL, rep(NA, length(tbarR))), 
                   hsdR=c(rep(NA, length(tbarL)), hsdR), 
                   hsdL=c(hsdL, rep(NA, length(tbarR))))
  EH$tbar <- as.POSIXct (EH$tbar, origin="1970-01-01", tz="GMT")
  ## whmean and whsd are not used; left here for reference
  whmean <- weighted.mean (c(EH$hmeanR, EH$hmeanL), 
                           c(1/EH$hsdR^2, 1/EH$hsdL^2), na.rm=TRUE)
  whsd <- sqrt (1/sum(c(1/EH$hsdR^2, 1/EH$hsdL^2), na.rm=TRUE))
  if (GeneratePlot) {
    p <- with (EH, ggplot(data=EH, aes(x=tbar), na.rm=TRUE))
    p <- p + geom_errorbar(aes(ymin=hmeanR-hsdR, ymax=hmeanR+hsdR, colour="right"),
                           width=600, size=1.5, na.rm=TRUE) + ylim (-0.5, 0.5)
    p <- p + geom_point (aes(y = hmeanR, colour="right"),size=3.5, na.rm=TRUE)
    p <- p + geom_errorbar(aes(ymin=hmeanL-hsdL, ymax=hmeanL+hsdL, colour="left"),
                           width=600, size=1.5, na.rm=TRUE)
    p <- p + geom_point (aes(y = hmeanL, colour="left"), size=3.5, na.rm=TRUE)
    p <- p + ylab(expression(paste(delta,psi,' [',degree,']')))
    p <- p + xlab ("Time [UTC]")
  }
  yss <- c(EH$hmeanL[!is.na(EH$hmeanL)], EH$hmeanR[!is.na(EH$hmeanR)])
  ywts <- 1/c(EH$hsdL[!is.na(EH$hsdL)], EH$hsdR[!is.na(EH$hsdR)])^2
  SS <- smooth.spline(EH$tbar, yss, w=ywts, df=length(yss)-1, spar=0.7)
  if (GeneratePlot) {
    D1$HC <- predict(SS, as.numeric(D1$Time))$y
    p <- p + with(D1, geom_line (data=D1, 
                                 aes (x=Time, y=HC, colour="spline"), 
                                 lwd=2, na.rm=TRUE))
    # SS2 <- smooth.spline(EH$tbar, yss, df=8, spar=0.4)
    # xss2 <- as.POSIXct (SS2$x, origin="1970-01-01", tz="GMT")
    # p <- p + geom_line(aes(x=xss2, y=SS2$y, colour="spline"), lwd=2, lty=2, na.rm=TRUE)
    cols <- c("right"="blue", "left"="darkgreen", "spline"="red")
    p <- p + scale_colour_manual("turn direction:", values=cols)
    p <- p + guides(color=guide_legend(override.aes=list(shape=c(16,16,NA), 
                                                         linetype=c(0,0,1))))
    p <- p + ggtitle (sprintf ("%d turns, wtd mean and std %.2f +/- %.3f", length (tbarL), whmean, whsd))
    p <- p + theme_WAC()
    if (.plotfile == 'inLine') {
      print (p)
    } else {
      suppressMessages(ggsave (.plotfile, p))
    }
  }
  
  ## construct the input-rate heading correction HC
  HC <- predict(SS, as.numeric(D$Time))$y
  return (HC)
}
NCAR/Ranadu documentation built on Jan. 27, 2023, 1:09 a.m.