kaskr / RTMB

R bindings to TMB
Other
45 stars 6 forks source link

What to expect if coercing a parameter with as.numeric? #20

Closed williamaeberhard closed 6 months ago

williamaeberhard commented 7 months ago

Dear Kasper,

Not really an issue, but something I experienced while playing around with RTMB. I didn't find answers in the doc, I thought posting here might be helpful for others too.

In an R function I compile with RTMB::MakeADFun I have a line where I coerce a parameter to an ordinary numeric type. This parameter is an element of the parameter list which is the only argument to the function. Upon running MakeADFun I get the warning about the imaginary part of an object being discarded. I understand this relates to how you used the complex number class to store both a parameter's value and... something else (its numerical derivative? the chain of nested operations required by an AD pass? something like that, no?). Now, the interesting thing is this is only a warning, I can actually use the compiled obj. With it I can REPORT this coerced numeric parameter and recover the correct value, but I don't recover the correct value when this parameter is the main output of my R function (in return()). I suppose the cause of this difference is the numeric coercion, but I cannot find any particular pattern in the difference.

It's clear to me we should not mess with your usage of the complex number class, but if we have to coerce some parameter, is there a way to predict what happens to its value if it contributes to the main output of the R function? Thanks!

Best regards,

William

kaskr commented 6 months ago

Just to confirm what you are saying here's an example:

library(RTMB)
f <- function(x) as.numeric(x)
F <- MakeTape(f, 1:3)

And it gives

Warning message: In f(x) : imaginary parts discarded in coercion

This is definitely not the intention... Given that f is a perfectly differentiable function, the above should just work. Fortunately, looking at ?as.numeric, one can write methods for it via as.double. So, the fix is to add something like

as.double.advector <- function (x, ...) x

It may look weird that advector to double conversion should return an advector. The rationale for this is that, from the point of the user, the advector is equivalent to numeric, regardless of the internal storage. Conversion from advector to an actual double should be made difficult on purpose because such conversion drops derivative tracing.

williamaeberhard commented 6 months ago

Thanks for your reply. Ok, we indeed lose the derivative tracing when discarding the imaginary part. For the reference, here is a MWE which illustrates the unexpected differences between return and REPORT:

library(RTMB)

x <- 1:3

func1 <- function(par){
    sumpar <- sum(par) # 
    REPORT(sumpar)
    return(sumpar)
}
obj <- MakeADFun(func=func1,parameters=x)
c(obj$report(x)$sumpar, obj$fn(x))
# ^ identical, as expected

func2 <- function(par){
    sumpar <- sum(as.numeric(par)) # added as.numeric() around par
    REPORT(sumpar)
    return(sumpar)
}
obj <- MakeADFun(func=func2,parameters=x)
c(obj$report(x)$sumpar, obj$fn(x))
# ^ REPORTed sumpar still correct, but $fn incorrect now

Again, this is not a real issue since I doubt many people would ever need to call as.numeric within the function. I'll experiment with an as.double.advector method if this need arises again.