Shinylive Apps for Statistical Computation
    • All Apps

    • The Inverse CDF Method
    • Newton-Raphson on the Cauchy Likelihood
    • Safeguarded Newton-Raphson for Logistic Regression
    • Comparing Gradient Descent and Newton-Raphson Optimization Algorithms
    • Stochastic Gradient Descent
    • Nelder–Mead
    • The EM Algorithm for a Normal Mixture
    • Conjugate Bayesian Updating
    • A Quadrature Grid Missing the Posterior Mode
    • Adaptive Gauss–Hermite Quadrature
    • Envelope Tightness in Rejection Sampling
    • Rejection Sampling from a Gamma Density with a Cauchy Envelope
    • Adaptive Rejection Sampling
    • The Failure of Importance Sampling
    • Importance Sampling of a Student-t Interval
    • Sorted Importance Weights
    • Hamiltonian Monte Carlo Tuning

Nelder–Mead

Author

Longhai Li

Published

October 6, 2026

#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| viewerHeight: 880
library(shiny)

## ---------------------------------------------------------------- objectives
mk <- function(label, fv, xlim, ylim, start, xstar) {
  force(fv)
  list(label = label, fv = fv, f = function(p) fv(p[1], p[2]),
       xlim = xlim, ylim = ylim, start = start,
       xstar = matrix(xstar, ncol = 2))
}

FNS <- list(
  quad = mk(
    "(a) Well-conditioned quadratic",
    function(x, y) 0.5 * (2 * (x - 1)^2 + 1.6 * (x - 1) * (y - 1) +
                          2 * (y - 1)^2),
    c(-3, 4), c(-3, 4), c(-2, 3), c(1, 1)
  ),
  banana = mk(
    "(b) Rosenbrock banana (narrow curved ridge)",
    function(x, y) (1 - x)^2 + 100 * (y - x^2)^2,
    c(-2, 2), c(-1, 3), c(-1.2, 1), c(1, 1)
  ),
  mix = mk(
    "(c) Highly correlated, two modes",
    function(x, y) {
      rho <- 0.9; den <- 1 - rho^2
      q <- function(u, v) (u^2 - 2 * rho * u * v + v^2) / den
      -log(exp(-0.5 * q(x - 1.2, y - 1.2)) +
           0.6 * exp(-0.5 * q(x + 1.5, y + 1.5) / 1.5) + 1e-12)
    },
    c(-4, 4), c(-4, 4), c(-3, -0.5), c(1.2, 1.2)
  ),
  himmel = mk(
    "(d) Himmelblau (four global minima)",
    function(x, y) (x^2 + y - 11)^2 + (x + y^2 - 7)^2,
    c(-5, 5), c(-5, 5), c(-4, 4),
    rbind(c(3, 2), c(-2.805118, 3.131312),
          c(-3.779310, -3.283186), c(3.584428, -1.848126))
  ),
  beale = mk(
    "(e) Beale (flat plateau, sharp valley)",
    function(x, y) (1.5 - x + x * y)^2 + (2.25 - x + x * y^2)^2 +
                   (2.625 - x + x * y^3)^2,
    c(-4.5, 4.5), c(-4.5, 4.5), c(-1, 1), c(3, 0.5)
  ),
  rastrigin = mk(
    "(f) Rastrigin (many local minima)",
    function(x, y) 20 + x^2 - 10 * cos(2 * pi * x) +
                        y^2 - 10 * cos(2 * pi * y),
    c(-5.12, 5.12), c(-5.12, 5.12), c(-3.1, 4.2), c(0, 0)
  )
)

## ------------------------------------------------------------- Nelder-Mead
## coefficients: reflection a, expansion g, contraction r, shrink s
run_nm <- function(FN, x0, h, a = 1, g = 2, r = 0.5, s = 0.5,
                   maxit = 80, tol = 1e-7) {
  f <- FN$f
  S <- rbind(x0, x0 + c(h, 0), x0 + c(0, h))
  fS <- apply(S, 1, f)
  steps <- list()
  note <- "maximum number of iterations reached"

  for (k in seq_len(maxit)) {
    o <- order(fS); S <- S[o, , drop = FALSE]; fS <- fS[o]
    dia <- max(c(sqrt(sum((S[1, ] - S[2, ])^2)),
                 sqrt(sum((S[1, ] - S[3, ])^2)),
                 sqrt(sum((S[2, ] - S[3, ])^2))))

    if (dia < tol || diff(range(fS)) < 1e-12) {
      steps[[k]] <- list(S = S, fS = fS, dia = dia, op = "converged",
                         cen = colMeans(S[1:2, , drop = FALSE]),
                         xr = NULL, xe = NULL, xc = NULL, Sn = S)
      note <- "converged (simplex collapsed)"
      break
    }

    cen <- colMeans(S[1:2, , drop = FALSE])          # centroid of the best two
    xr <- cen + a * (cen - S[3, ]); fr <- f(xr)
    xe <- NULL; xc <- NULL
    Sn <- S; fn <- fS

    if (fr < fS[1]) {                                 # better than the best
      xe <- cen + g * (xr - cen); fe <- f(xe)
      if (fe < fr) { Sn[3, ] <- xe; fn[3] <- fe; op <- "expansion" }
      else         { Sn[3, ] <- xr; fn[3] <- fr; op <- "reflection" }
    } else if (fr < fS[2]) {                          # middling: accept
      Sn[3, ] <- xr; fn[3] <- fr; op <- "reflection"
    } else {
      if (fr < fS[3]) {                               # outside contraction
        xc <- cen + r * (xr - cen); fc <- f(xc)
        if (fc <= fr) { Sn[3, ] <- xc; fn[3] <- fc; op <- "outside contraction" }
        else op <- "shrink"
      } else {                                        # inside contraction
        xc <- cen + r * (S[3, ] - cen); fc <- f(xc)
        if (fc < fS[3]) { Sn[3, ] <- xc; fn[3] <- fc; op <- "inside contraction" }
        else op <- "shrink"
      }
      if (op == "shrink") {
        Sn[2, ] <- S[1, ] + s * (S[2, ] - S[1, ])
        Sn[3, ] <- S[1, ] + s * (S[3, ] - S[1, ])
        fn[2] <- f(Sn[2, ]); fn[3] <- f(Sn[3, ])
      }
    }

    steps[[k]] <- list(S = S, fS = fS, dia = dia, op = op, cen = cen,
                       xr = xr, xe = xe, xc = xc, Sn = Sn)
    S <- Sn; fS <- fn
  }

  list(steps = steps, n = length(steps), note = note)
}

## ---------------------------- widen the data box to the device aspect ratio
expand_box <- function(xlim, ylim, pin) {
  if (length(pin) != 2 || any(!is.finite(pin)) || any(pin <= 0))
    return(list(xlim = xlim, ylim = ylim))
  rr <- pin[1] / pin[2]
  wd <- diff(xlim); ht <- diff(ylim)
  if (wd / ht < rr) {
    w <- ht * rr; m <- mean(xlim); xlim <- m + c(-0.5, 0.5) * w
  } else {
    hh <- wd / rr; m <- mean(ylim); ylim <- m + c(-0.5, 0.5) * hh
  }
  list(xlim = xlim, ylim = ylim)
}

COL <- c(refl = "#E8820C", exp = "#7B3FBF", con = "#1F6FB4",
         new = "#2E8B57", best = "#2E8B57", worst = "#C81E1E")

## --------------------------------------------------------------------- UI
ui <- fluidPage(
  titlePanel("Shinylive App for the Nelder–Mead Algorithm"),
  fluidRow(
    column(
      3,
      selectInput("fn", "Objective function",
                  setNames(names(FNS), sapply(FNS, `[[`, "label"))),
      sliderInput("k", "Iteration k", min = 0, max = 1, value = 0, step = 1),
      sliderInput("h", "Initial simplex size (% of range)",
                  min = 2, max = 40, value = 12, step = 1),
      checkboxInput("showpath", "Keep past simplices", TRUE),
      htmlOutput("info")
    ),
    column(
      2,
      div(
        style = "margin-top:26px;",
        actionButton("play", "Play", class = "btn-primary btn-lg",
                     style = "width:100%; margin-bottom:10px;"),
        actionButton("step", "Next", class = "btn-lg",
                     style = "width:100%; margin-bottom:10px;"),
        actionButton("back", "Prev", class = "btn-lg",
                     style = "width:100%;"),
        hr(),
        checkboxInput("autoplay", "Autoplay on change", FALSE),
        actionButton("reset", "Reset initial",
                     style = "width:100%; margin-bottom:8px;"),
        helpText("Click inside the contour plot to choose a new initial value.")
      )
    ),
    column(
      7,
      plotOutput("contour", click = "click", height = "540px"),
      plotOutput("conv", height = "190px")
    )
  )
)

## ----------------------------------------------------------------- server
server <- function(input, output, session) {

  FN <- reactive(FNS[[input$fn]])
  start <- reactiveVal(FNS[[1]]$start)
  playing <- reactiveVal(FALSE)

  observeEvent(input$fn, start(FNS[[input$fn]]$start))
  observeEvent(input$reset, start(FN()$start))
  observeEvent(input$click, {
    p <- c(input$click$x, input$click$y)
    if (all(is.finite(p))) start(p)
  })

  path <- reactive({
    req(start())
    FNc <- FN()
    run_nm(FNc, start(), h = input$h / 100 * diff(FNc$xlim))
  })

  observeEvent(path(), {
    updateSliderInput(session, "k", max = max(1, path()$n - 1), value = 0)
    playing(isTRUE(input$autoplay) && path()$n > 1)
  })

  observeEvent(input$play, {
    if (isTRUE(playing())) {
      playing(FALSE)
    } else {
      if (as.integer(input$k) >= path()$n - 1)
        updateSliderInput(session, "k", value = 0)
      playing(TRUE)
    }
  })

  observeEvent(playing(), {
    updateActionButton(session, "play",
                       label = if (isTRUE(playing())) "Pause" else "Play")
  })

  observe({
    if (!isTRUE(playing())) return()
    kmax <- isolate(path()$n) - 1
    k <- isolate(as.integer(input$k))
    if (k >= kmax) { playing(FALSE); return() }
    invalidateLater(900, session)
    updateSliderInput(session, "k", value = k + 1)
  })

  observeEvent(input$step, {
    playing(FALSE)
    updateSliderInput(session, "k",
                      value = min(as.integer(input$k) + 1, max(0, path()$n - 1)))
  })

  observeEvent(input$back, {
    playing(FALSE)
    updateSliderInput(session, "k", value = max(as.integer(input$k) - 1, 0))
  })

  output$contour <- renderPlot({
    FNc <- FN(); P <- path()
    k <- min(as.integer(input$k), P$n - 1) + 1L
    st <- P$steps[[k]]

    par(mar = c(4, 4, 3, 1))
    plot.new()
    bx <- expand_box(FNc$xlim, FNc$ylim, par("pin"))
    plot.window(xlim = bx$xlim, ylim = bx$ylim, xaxs = "i", yaxs = "i")

    xs <- seq(bx$xlim[1], bx$xlim[2], length.out = 180)
    ys <- seq(bx$ylim[1], bx$ylim[2], length.out = 180)
    z  <- outer(xs, ys, FNc$fv)
    zf <- z[is.finite(z)]
    lv <- unique(quantile(zf, probs = seq(0, 1, length.out = 30)^1.7))
    zr <- matrix(rank(z, na.last = "keep", ties.method = "average"),
                 nrow = length(xs))

    image(xs, ys, zr, add = TRUE,
          col = colorRampPalette(c("#f7fbff", "#9ec4e3"))(128))
    contour(xs, ys, z, levels = lv, add = TRUE,
            col = "grey45", drawlabels = FALSE)
    axis(1); axis(2); box()
    title(main = paste0("Nelder-Mead  |  ", FNc$label),
          xlab = expression(x[1]), ylab = expression(x[2]), cex.main = 1.0)
    mtext(paste0("iteration ", k - 1, ":  ", st$op), side = 3, line = 0.1,
          cex = 1.1, font = 2)

    points(FNc$xstar[, 1], FNc$xstar[, 2], pch = 8, col = "red",
           cex = 1.3, lwd = 2)

    ## past simplices
    if (isTRUE(input$showpath) && k > 1)
      for (j in seq_len(k - 1))
        polygon(P$steps[[j]]$S[, 1], P$steps[[j]]$S[, 2],
                border = "grey55", lty = 3)

    ## current simplex
    S <- st$S
    polygon(S[, 1], S[, 2], border = "black", lwd = 2,
            col = adjustcolor("white", alpha.f = 0.35))
    points(st$cen[1], st$cen[2], pch = 3, col = "black", lwd = 2, cex = 1.1)
    points(S[, 1], S[, 2], pch = 21, cex = 1.6, lwd = 2, bg = "white",
           col = c(COL["best"], "grey30", COL["worst"]))
    text(S[, 1], S[, 2], labels = c("B", "G", "W"), pos = 3, offset = 0.6,
         font = 2, col = c(COL["best"], "grey30", COL["worst"]))

    ## candidate points generated this iteration
    if (!is.null(st$xr)) {
      segments(S[3, 1], S[3, 2], st$xr[1], st$xr[2],
               col = COL["refl"], lwd = 2, lty = 2)
      points(st$xr[1], st$xr[2], pch = 19, col = COL["refl"], cex = 1.5)
    }
    if (!is.null(st$xe)) {
      segments(st$xr[1], st$xr[2], st$xe[1], st$xe[2],
               col = COL["exp"], lwd = 2, lty = 2)
      points(st$xe[1], st$xe[2], pch = 19, col = COL["exp"], cex = 1.5)
    }
    if (!is.null(st$xc))
      points(st$xc[1], st$xc[2], pch = 19, col = COL["con"], cex = 1.5)

    ## the simplex that results
    if (st$op != "converged") {
      polygon(st$Sn[, 1], st$Sn[, 2], border = COL["new"], lwd = 3, lty = 2)
      if (st$op == "shrink")
        arrows(S[2:3, 1], S[2:3, 2], st$Sn[2:3, 1], st$Sn[2:3, 2],
               col = COL["con"], lwd = 2, length = 0.10)
    }

    legend("topleft", cex = 0.85, bg = "#ffffffcc", box.col = NA,
           pch = c(21, 21, 3, 19, 19, 19, NA),
           lty = c(NA, NA, NA, NA, NA, NA, 2),
           lwd = c(2, 2, 2, NA, NA, NA, 3),
           col = c(COL["best"], COL["worst"], "black", COL["refl"],
                   COL["exp"], COL["con"], COL["new"]),
           legend = c("best vertex B", "worst vertex W",
                      "centroid of B and G", "reflection", "expansion",
                      "contraction / shrink", "next simplex"))
  })

  output$conv <- renderPlot({
    P <- path(); k <- min(as.integer(input$k), P$n - 1) + 1L
    fb <- sapply(P$steps, function(s) s$fS[1])
    fw <- sapply(P$steps, function(s) s$fS[3])
    par(mar = c(4, 4.5, 1.5, 1))
    matplot(seq_len(P$n) - 1, cbind(fb, fw), type = "b", pch = 20, lty = 1,
            col = c(COL["best"], COL["worst"]),
            xlab = "iteration k", ylab = "f at simplex vertices")
    abline(v = k - 1, col = "grey60", lty = 2)
    legend("topright", bty = "n", cex = 0.9, lty = 1, pch = 20,
           col = c(COL["best"], COL["worst"]), legend = c("best", "worst"))
  })

  output$info <- renderUI({
    P <- path(); k <- min(as.integer(input$k), P$n - 1) + 1L
    st <- P$steps[[k]]
    fmt <- function(v, d = 4) formatC(v, format = "g", digits = d)
    HTML(paste0(
      "<hr><b>iteration</b> ", k - 1, " of ", P$n - 1, "<br>",
      "<b>operation</b>: ", st$op, "<br><br>",
      "<b>B</b> = (", fmt(st$S[1, 1]), ", ", fmt(st$S[1, 2]), "),  f = ",
      fmt(st$fS[1], 6), "<br>",
      "<b>G</b> = (", fmt(st$S[2, 1]), ", ", fmt(st$S[2, 2]), "),  f = ",
      fmt(st$fS[2], 6), "<br>",
      "<b>W</b> = (", fmt(st$S[3, 1]), ", ", fmt(st$S[3, 2]), "),  f = ",
      fmt(st$fS[3], 6), "<br><br>",
      "<b>simplex diameter</b> = ", fmt(st$dia), "<br>",
      "<b>f range</b> = ", fmt(diff(range(st$fS))),
      "<br><br><i>", P$note, "</i>"
    ))
  })
}

shinyApp(ui, server)

About the app

Shows how the Nelder-Mead method finds a minimum without derivatives, by repeatedly reflecting, expanding, contracting, and shrinking a simplex. Reflection, expansion, contraction and shrink steps are colour-coded on the contour plot as the simplex moves.

The app above animates every reflection, expansion, contraction, and shrink on six test objectives; click inside the panel to place a new starting simplex.

NoteR source for this app
library(shiny)

## ---------------------------------------------------------------- objectives
mk <- function(label, fv, xlim, ylim, start, xstar) {
  force(fv)
  list(label = label, fv = fv, f = function(p) fv(p[1], p[2]),
       xlim = xlim, ylim = ylim, start = start,
       xstar = matrix(xstar, ncol = 2))
}

FNS <- list(
  quad = mk(
    "(a) Well-conditioned quadratic",
    function(x, y) 0.5 * (2 * (x - 1)^2 + 1.6 * (x - 1) * (y - 1) +
                          2 * (y - 1)^2),
    c(-3, 4), c(-3, 4), c(-2, 3), c(1, 1)
  ),
  banana = mk(
    "(b) Rosenbrock banana (narrow curved ridge)",
    function(x, y) (1 - x)^2 + 100 * (y - x^2)^2,
    c(-2, 2), c(-1, 3), c(-1.2, 1), c(1, 1)
  ),
  mix = mk(
    "(c) Highly correlated, two modes",
    function(x, y) {
      rho <- 0.9; den <- 1 - rho^2
      q <- function(u, v) (u^2 - 2 * rho * u * v + v^2) / den
      -log(exp(-0.5 * q(x - 1.2, y - 1.2)) +
           0.6 * exp(-0.5 * q(x + 1.5, y + 1.5) / 1.5) + 1e-12)
    },
    c(-4, 4), c(-4, 4), c(-3, -0.5), c(1.2, 1.2)
  ),
  himmel = mk(
    "(d) Himmelblau (four global minima)",
    function(x, y) (x^2 + y - 11)^2 + (x + y^2 - 7)^2,
    c(-5, 5), c(-5, 5), c(-4, 4),
    rbind(c(3, 2), c(-2.805118, 3.131312),
          c(-3.779310, -3.283186), c(3.584428, -1.848126))
  ),
  beale = mk(
    "(e) Beale (flat plateau, sharp valley)",
    function(x, y) (1.5 - x + x * y)^2 + (2.25 - x + x * y^2)^2 +
                   (2.625 - x + x * y^3)^2,
    c(-4.5, 4.5), c(-4.5, 4.5), c(-1, 1), c(3, 0.5)
  ),
  rastrigin = mk(
    "(f) Rastrigin (many local minima)",
    function(x, y) 20 + x^2 - 10 * cos(2 * pi * x) +
                        y^2 - 10 * cos(2 * pi * y),
    c(-5.12, 5.12), c(-5.12, 5.12), c(-3.1, 4.2), c(0, 0)
  )
)

## ------------------------------------------------------------- Nelder-Mead
## coefficients: reflection a, expansion g, contraction r, shrink s
run_nm <- function(FN, x0, h, a = 1, g = 2, r = 0.5, s = 0.5,
                   maxit = 80, tol = 1e-7) {
  f <- FN$f
  S <- rbind(x0, x0 + c(h, 0), x0 + c(0, h))
  fS <- apply(S, 1, f)
  steps <- list()
  note <- "maximum number of iterations reached"

  for (k in seq_len(maxit)) {
    o <- order(fS); S <- S[o, , drop = FALSE]; fS <- fS[o]
    dia <- max(c(sqrt(sum((S[1, ] - S[2, ])^2)),
                 sqrt(sum((S[1, ] - S[3, ])^2)),
                 sqrt(sum((S[2, ] - S[3, ])^2))))

    if (dia < tol || diff(range(fS)) < 1e-12) {
      steps[[k]] <- list(S = S, fS = fS, dia = dia, op = "converged",
                         cen = colMeans(S[1:2, , drop = FALSE]),
                         xr = NULL, xe = NULL, xc = NULL, Sn = S)
      note <- "converged (simplex collapsed)"
      break
    }

    cen <- colMeans(S[1:2, , drop = FALSE])          # centroid of the best two
    xr <- cen + a * (cen - S[3, ]); fr <- f(xr)
    xe <- NULL; xc <- NULL
    Sn <- S; fn <- fS

    if (fr < fS[1]) {                                 # better than the best
      xe <- cen + g * (xr - cen); fe <- f(xe)
      if (fe < fr) { Sn[3, ] <- xe; fn[3] <- fe; op <- "expansion" }
      else         { Sn[3, ] <- xr; fn[3] <- fr; op <- "reflection" }
    } else if (fr < fS[2]) {                          # middling: accept
      Sn[3, ] <- xr; fn[3] <- fr; op <- "reflection"
    } else {
      if (fr < fS[3]) {                               # outside contraction
        xc <- cen + r * (xr - cen); fc <- f(xc)
        if (fc <= fr) { Sn[3, ] <- xc; fn[3] <- fc; op <- "outside contraction" }
        else op <- "shrink"
      } else {                                        # inside contraction
        xc <- cen + r * (S[3, ] - cen); fc <- f(xc)
        if (fc < fS[3]) { Sn[3, ] <- xc; fn[3] <- fc; op <- "inside contraction" }
        else op <- "shrink"
      }
      if (op == "shrink") {
        Sn[2, ] <- S[1, ] + s * (S[2, ] - S[1, ])
        Sn[3, ] <- S[1, ] + s * (S[3, ] - S[1, ])
        fn[2] <- f(Sn[2, ]); fn[3] <- f(Sn[3, ])
      }
    }

    steps[[k]] <- list(S = S, fS = fS, dia = dia, op = op, cen = cen,
                       xr = xr, xe = xe, xc = xc, Sn = Sn)
    S <- Sn; fS <- fn
  }

  list(steps = steps, n = length(steps), note = note)
}

## ---------------------------- widen the data box to the device aspect ratio
expand_box <- function(xlim, ylim, pin) {
  if (length(pin) != 2 || any(!is.finite(pin)) || any(pin <= 0))
    return(list(xlim = xlim, ylim = ylim))
  rr <- pin[1] / pin[2]
  wd <- diff(xlim); ht <- diff(ylim)
  if (wd / ht < rr) {
    w <- ht * rr; m <- mean(xlim); xlim <- m + c(-0.5, 0.5) * w
  } else {
    hh <- wd / rr; m <- mean(ylim); ylim <- m + c(-0.5, 0.5) * hh
  }
  list(xlim = xlim, ylim = ylim)
}

COL <- c(refl = "#E8820C", exp = "#7B3FBF", con = "#1F6FB4",
         new = "#2E8B57", best = "#2E8B57", worst = "#C81E1E")

## --------------------------------------------------------------------- UI
ui <- fluidPage(
  titlePanel("Shinylive App for the Nelder–Mead Algorithm"),
  fluidRow(
    column(
      3,
      selectInput("fn", "Objective function",
                  setNames(names(FNS), sapply(FNS, `[[`, "label"))),
      sliderInput("k", "Iteration k", min = 0, max = 1, value = 0, step = 1),
      sliderInput("h", "Initial simplex size (% of range)",
                  min = 2, max = 40, value = 12, step = 1),
      checkboxInput("showpath", "Keep past simplices", TRUE),
      htmlOutput("info")
    ),
    column(
      2,
      div(
        style = "margin-top:26px;",
        actionButton("play", "Play", class = "btn-primary btn-lg",
                     style = "width:100%; margin-bottom:10px;"),
        actionButton("step", "Next", class = "btn-lg",
                     style = "width:100%; margin-bottom:10px;"),
        actionButton("back", "Prev", class = "btn-lg",
                     style = "width:100%;"),
        hr(),
        checkboxInput("autoplay", "Autoplay on change", FALSE),
        actionButton("reset", "Reset initial",
                     style = "width:100%; margin-bottom:8px;"),
        helpText("Click inside the contour plot to choose a new initial value.")
      )
    ),
    column(
      7,
      plotOutput("contour", click = "click", height = "540px"),
      plotOutput("conv", height = "190px")
    )
  )
)

## ----------------------------------------------------------------- server
server <- function(input, output, session) {

  FN <- reactive(FNS[[input$fn]])
  start <- reactiveVal(FNS[[1]]$start)
  playing <- reactiveVal(FALSE)

  observeEvent(input$fn, start(FNS[[input$fn]]$start))
  observeEvent(input$reset, start(FN()$start))
  observeEvent(input$click, {
    p <- c(input$click$x, input$click$y)
    if (all(is.finite(p))) start(p)
  })

  path <- reactive({
    req(start())
    FNc <- FN()
    run_nm(FNc, start(), h = input$h / 100 * diff(FNc$xlim))
  })

  observeEvent(path(), {
    updateSliderInput(session, "k", max = max(1, path()$n - 1), value = 0)
    playing(isTRUE(input$autoplay) && path()$n > 1)
  })

  observeEvent(input$play, {
    if (isTRUE(playing())) {
      playing(FALSE)
    } else {
      if (as.integer(input$k) >= path()$n - 1)
        updateSliderInput(session, "k", value = 0)
      playing(TRUE)
    }
  })

  observeEvent(playing(), {
    updateActionButton(session, "play",
                       label = if (isTRUE(playing())) "Pause" else "Play")
  })

  observe({
    if (!isTRUE(playing())) return()
    kmax <- isolate(path()$n) - 1
    k <- isolate(as.integer(input$k))
    if (k >= kmax) { playing(FALSE); return() }
    invalidateLater(900, session)
    updateSliderInput(session, "k", value = k + 1)
  })

  observeEvent(input$step, {
    playing(FALSE)
    updateSliderInput(session, "k",
                      value = min(as.integer(input$k) + 1, max(0, path()$n - 1)))
  })

  observeEvent(input$back, {
    playing(FALSE)
    updateSliderInput(session, "k", value = max(as.integer(input$k) - 1, 0))
  })

  output$contour <- renderPlot({
    FNc <- FN(); P <- path()
    k <- min(as.integer(input$k), P$n - 1) + 1L
    st <- P$steps[[k]]

    par(mar = c(4, 4, 3, 1))
    plot.new()
    bx <- expand_box(FNc$xlim, FNc$ylim, par("pin"))
    plot.window(xlim = bx$xlim, ylim = bx$ylim, xaxs = "i", yaxs = "i")

    xs <- seq(bx$xlim[1], bx$xlim[2], length.out = 180)
    ys <- seq(bx$ylim[1], bx$ylim[2], length.out = 180)
    z  <- outer(xs, ys, FNc$fv)
    zf <- z[is.finite(z)]
    lv <- unique(quantile(zf, probs = seq(0, 1, length.out = 30)^1.7))
    zr <- matrix(rank(z, na.last = "keep", ties.method = "average"),
                 nrow = length(xs))

    image(xs, ys, zr, add = TRUE,
          col = colorRampPalette(c("#f7fbff", "#9ec4e3"))(128))
    contour(xs, ys, z, levels = lv, add = TRUE,
            col = "grey45", drawlabels = FALSE)
    axis(1); axis(2); box()
    title(main = paste0("Nelder-Mead  |  ", FNc$label),
          xlab = expression(x[1]), ylab = expression(x[2]), cex.main = 1.0)
    mtext(paste0("iteration ", k - 1, ":  ", st$op), side = 3, line = 0.1,
          cex = 1.1, font = 2)

    points(FNc$xstar[, 1], FNc$xstar[, 2], pch = 8, col = "red",
           cex = 1.3, lwd = 2)

    ## past simplices
    if (isTRUE(input$showpath) && k > 1)
      for (j in seq_len(k - 1))
        polygon(P$steps[[j]]$S[, 1], P$steps[[j]]$S[, 2],
                border = "grey55", lty = 3)

    ## current simplex
    S <- st$S
    polygon(S[, 1], S[, 2], border = "black", lwd = 2,
            col = adjustcolor("white", alpha.f = 0.35))
    points(st$cen[1], st$cen[2], pch = 3, col = "black", lwd = 2, cex = 1.1)
    points(S[, 1], S[, 2], pch = 21, cex = 1.6, lwd = 2, bg = "white",
           col = c(COL["best"], "grey30", COL["worst"]))
    text(S[, 1], S[, 2], labels = c("B", "G", "W"), pos = 3, offset = 0.6,
         font = 2, col = c(COL["best"], "grey30", COL["worst"]))

    ## candidate points generated this iteration
    if (!is.null(st$xr)) {
      segments(S[3, 1], S[3, 2], st$xr[1], st$xr[2],
               col = COL["refl"], lwd = 2, lty = 2)
      points(st$xr[1], st$xr[2], pch = 19, col = COL["refl"], cex = 1.5)
    }
    if (!is.null(st$xe)) {
      segments(st$xr[1], st$xr[2], st$xe[1], st$xe[2],
               col = COL["exp"], lwd = 2, lty = 2)
      points(st$xe[1], st$xe[2], pch = 19, col = COL["exp"], cex = 1.5)
    }
    if (!is.null(st$xc))
      points(st$xc[1], st$xc[2], pch = 19, col = COL["con"], cex = 1.5)

    ## the simplex that results
    if (st$op != "converged") {
      polygon(st$Sn[, 1], st$Sn[, 2], border = COL["new"], lwd = 3, lty = 2)
      if (st$op == "shrink")
        arrows(S[2:3, 1], S[2:3, 2], st$Sn[2:3, 1], st$Sn[2:3, 2],
               col = COL["con"], lwd = 2, length = 0.10)
    }

    legend("topleft", cex = 0.85, bg = "#ffffffcc", box.col = NA,
           pch = c(21, 21, 3, 19, 19, 19, NA),
           lty = c(NA, NA, NA, NA, NA, NA, 2),
           lwd = c(2, 2, 2, NA, NA, NA, 3),
           col = c(COL["best"], COL["worst"], "black", COL["refl"],
                   COL["exp"], COL["con"], COL["new"]),
           legend = c("best vertex B", "worst vertex W",
                      "centroid of B and G", "reflection", "expansion",
                      "contraction / shrink", "next simplex"))
  })

  output$conv <- renderPlot({
    P <- path(); k <- min(as.integer(input$k), P$n - 1) + 1L
    fb <- sapply(P$steps, function(s) s$fS[1])
    fw <- sapply(P$steps, function(s) s$fS[3])
    par(mar = c(4, 4.5, 1.5, 1))
    matplot(seq_len(P$n) - 1, cbind(fb, fw), type = "b", pch = 20, lty = 1,
            col = c(COL["best"], COL["worst"]),
            xlab = "iteration k", ylab = "f at simplex vertices")
    abline(v = k - 1, col = "grey60", lty = 2)
    legend("topright", bty = "n", cex = 0.9, lty = 1, pch = 20,
           col = c(COL["best"], COL["worst"]), legend = c("best", "worst"))
  })

  output$info <- renderUI({
    P <- path(); k <- min(as.integer(input$k), P$n - 1) + 1L
    st <- P$steps[[k]]
    fmt <- function(v, d = 4) formatC(v, format = "g", digits = d)
    HTML(paste0(
      "<hr><b>iteration</b> ", k - 1, " of ", P$n - 1, "<br>",
      "<b>operation</b>: ", st$op, "<br><br>",
      "<b>B</b> = (", fmt(st$S[1, 1]), ", ", fmt(st$S[1, 2]), "),  f = ",
      fmt(st$fS[1], 6), "<br>",
      "<b>G</b> = (", fmt(st$S[2, 1]), ", ", fmt(st$S[2, 2]), "),  f = ",
      fmt(st$fS[2], 6), "<br>",
      "<b>W</b> = (", fmt(st$S[3, 1]), ", ", fmt(st$S[3, 2]), "),  f = ",
      fmt(st$fS[3], 6), "<br><br>",
      "<b>simplex diameter</b> = ", fmt(st$dia), "<br>",
      "<b>f range</b> = ", fmt(diff(range(st$fS))),
      "<br><br><i>", P$note, "</i>"
    ))
  })
}

shinyApp(ui, server)

This app accompanies Optimization for Maximum Likelihood Estimation in the book.