rsolvernonlinear-optimizationnonlinear-functions

Solve non-linear optimization problem with ROI package in R


I want solve a non-linear objective function with two non-linear constraints in R. I have succesfully solved the model in GAMS but because of my general data workflow, I would like to use R instead. The idea behind the model is that I would like to calibrate a rational function (four parameters) so that it is consistent with a given dataset (resulting in two constraints), while minimizing the difference between the slope of the function and a target value (objective function). Hence the outcome of the optimization problem are the four parameters that determine the shape of the rational function. I am using the R Optimization Infrastructure (ROI) to solve the model. As I want to solve the model for several different cases, I created a series of functions that accept variables so the objective function and constraints can easily be updated.

My model:

library(ROI)

# I installed several non-linear solvers that were suggested after running ROI_applicable_solvers() on the model
#install.packages("ROI.plugin.alabama")
#install.packages("ROI.plugin.nlminb")
#install.packages("ROI.plugin.nloptr")

# Empirical data for the example
elas_sl_t <- -0.006648
elas_by_t <- 0.2831
gdp_cap_rel_t <- 51.69
cal_cap_t <- 27.75

# Rational function for which parameters x[] need to be solved
elas_f <- function(x, gdp_cap) {
  (x[1]*gdp_cap + x[2])/((gdp_cap+x[3])*(gdp_cap+x[4]))
}

# Integral of elas_f
int_elas_f <- function(x, gdp_cap) {
  -(x[1] * x[4] - x[2]) * log(gdp_cap + x[4])/(x[4]^2 - x[3] * x[4]) - 
    (x[2] - x[1] * x[3]) * log(gdp_cap + x[3])/(x[3] * x[4] - x[3]^2) + x[2] * log(gdp_cap)/(x[3] * x[4])
}

# Derivative of elas_f
der_elas_f <- function(x, gdp_cap){
  - (x[1] * gdp_cap + x[2]) / ((gdp_cap + x[3])^2 * (gdp_cap + x[4])) -
    (x[1] * gdp_cap + x[2]) / ((gdp_cap + x[3]) * (gdp_cap + x[4])^2) + x[1] / ((gdp_cap + x[3]) * (gdp_cap + x[4]))
}

# Objective function (should be minimized)
obj_f <- function(x, gdp_cap = 1, elas_sl_by = elas_sl_t){
  (der_elas_f(x, gdp_cap) - elas_sl_by)^2
}

# 1st constraint (should be equal to zero)
con1_f <- function(x, gdp_cap = 1, elas_by = elas_by_t){
  elas_f(x, gdp_cap) - elas_by
}

# 2nd constraint (should be equal to zero)
con2_f <- function(x, gdp_cap_ty = gdp_cap_rel_t, gdp_cap_by = 1, cal_ty = cal_cap_t) {
  int_elas_f(x, gdp_cap = gdp_cap_ty) - int_elas_f(x, gdp_cap = gdp_cap_by) - log(cal_ty)
}

# The solution of the model is
param_t <- c(1245.685,  30.987, 883.645,  4.097)

# check the solution
con1_f(param_t) # close to zero, ok
con2_f(param_t) # close to zero, ok
obj_f(param_t) # 0.05155

# Solving the model with ROI, using the actual solution as starting values does not work.
nlp <- OP(F_objective(F = obj_f, n = 4), 
          F_constraint(F = list(con1_f, con2_f), dir = rep("==", 2), rhs = c(0,0)),
          maximum = FALSE)
nlp
sol <- ROI_solve(nlp, start = param_t, solver = "auto")
sol
solution(sol)

It seems that the model cannot be solved or perhaps I am doing something wrong. Is it possible to solve the above problem using R (with ROI or perhaps an other package)? Perhaps the available solvers are not good enough? I used the GAMS CONOPT solver to solve my problem, which is not available for ROI/R. I also managed the solve the problems with the GAMS IPOPT solver, which is available for R (https://github.com/coin-or/Ipopt) but unfortunately not as a plugin for ROI (only IPOP, which is a quadratic solver). I also tried the NEOS solver in ROI but this results in an error message as the server cannot be contacted. Any help is much appreciated.


Solution

  • I'm not sure about the "solution" you got from GAMS. Also, what is considered "close to zero"?

    # Modified Integral of elas_f
    int_elas_f <- function(x, gdp_cap) {
      if (any(gdp_cap + c(0, x[3:4]) < 0)) return(NA_real_)
      -(x[1] * x[4] - x[2]) * log(gdp_cap + x[4])/(x[4]^2 - x[3] * x[4]) - 
        (x[2] - x[1] * x[3]) * log(gdp_cap + x[3])/(x[3] * x[4] - x[3]^2) + x[2] * log(gdp_cap)/(x[3] * x[4])
    }
    
    with(
      optim(
        c(1e3, 100, 1e3, 10),
        function(x) {
          y <- obj_f(x)
          y + 1e3*(abs(y) + 1)*(abs(con1_f(x)) + abs(con2_f(x)))
        }
      ), list(par = par, constraints = c(con1_f(par), con2_f(par)), obj = obj_f(par), value = value)
    )
    #> $par
    #> [1] 1361.293833  400.099856  880.053420    6.061782
    #> 
    #> $constraints
    #> [1]  1.676616e-08 -2.059692e-08
    #> 
    #> $obj
    #> [1] 0.0342367
    #> 
    #> $value
    #> [1] 0.03427534
    

    The results are quite sensitive to the initial guess. A more comprehensive exploration indicates the solution may be divergent:

    library(data.table)
    
    f <- function(x) {
        with(
          optim(
            x,
            function(x) {
              y <- obj_f(x)
              y + 1e3*(abs(y) + 1)*(abs(con1_f(x)) + abs(con2_f(x)))
            }, method = "Nelder-Mead"
          ),
          c(con1_f(par), con2_f(par), obj_f(par))
        )
      }
    
    m <- RcppAlgos::permuteGeneral(2^(8:20), 4)
    n <- nrow(m)
    df <- data.frame(con1 = numeric(n), con2 = numeric(n), obj = numeric(n))
    
    for (i in 1:n) df[i,] <- f(m[i,])
    
    dt <- setorder(setDT(df)[,r := .I][abs(con1) < 1e-6 & abs(con2) < 1e-6], obj)
    m[dt$r[1],]
    #> [1] 1048576    8192  131072    1024
    
    sol <- with(
      optim(
        m[dt$r[1],],
        function(x) {
          y <- obj_f(x)
          y + 1e3*(abs(y) + 1)*(abs(con1_f(x)) + abs(con2_f(x)))
        }
      ),
      list(
        par = par,
        constraints = c(con1_f(par), con2_f(par)),
        obj = obj_f(par),
        value = value
      )
    )
    
    with(
      optim(
        sol$par,
        function(x) {
          y <- obj_f(x)
          y + 1e5*(abs(y) + 1)*(abs(con1_f(x)) + abs(con2_f(x)))
        }
      ),
      list(
        par = par,
        constraints = c(con1_f(par), con2_f(par)),
        obj = obj_f(par),
        value = value
      )
    )
    #> $par
    #> [1]  4868827.420 24021111.619    30753.161     3317.202
    #> 
    #> $constraints
    #> [1] -3.325118e-14  2.353673e-14
    #> 
    #> $obj
    #> [1] 0.002944623
    #> 
    #> $value
    #> [1] 0.002944629
    

    As I continue to increase the initial values in the search space (e.g., m <- RcppAlgos::permuteGeneral(2^(12:24), 4)), the parameter values get increasingly larger while the value of the objective function continues to shrink.

    The suspicion of non-convergence is supported by ROI, which has the sense to stop by the time it gets to 3.795066e-03:

     library(ROI)
     
     param_t <- c(1245.685,  30.987, 883.645,  4.097)
    
     nlp <- OP(F_objective(F = obj_f, n = 4), 
               F_constraint(F = list(con1_f, con2_f), dir = rep("==", 2), rhs = c(0,0)),
               maximum = FALSE)
     sol <- ROI_solve(nlp, start = param_t, solver = "auto")
    #> Warning in nlminb2(start = start, objective = objective(x), eqFun = eqfun, :
    #> gradient not applicable for *constrained* NLPs for solver 'nlminb'.
     sol
    #> No optimal solution found.
    #> The solver message was: No solution.
    #> The objective value is: 3.795066e-03
     sol$solution
    #> [1] 3.262698e+06 1.350195e+07 1.333212e+02 4.250796e+05
     sol$status$msg$symbol
    #> [1] "NON_CONVERGENCE"
     sol$message$message
    #> [1] "false convergence (8)"