tests/testthat/test.complementarySlackness.R

################################################################################
## Tests of functions that test for complementarySlackness
################################################################################

context("Complementary slackness, no subproblems")

make_known_optimal <- function(flipped=FALSE) {

    x <- data.frame(row.names = c("A", "B", "C", "D", "E"),
                    z = c(rep(!flipped, 2), rep(flipped, 3)),
                    y = c(0, 2, -3, 0, 6))
    m <- match_on(z ~ y, data = x, method = "euclidean")
    m  <- as.InfinitySparseMatrix(m)
    nodes <- new("NodeInfo",
                 data.frame(stringsAsFactors = FALSE,
                     name = c("A", "B", "C", "D", "E", '(_End_)'),
                     price = c(3, 4.75, 0, 2.75, 0.25, 0),
                     upstream_not_down = c(TRUE, TRUE, FALSE, FALSE, FALSE, NA),
                     supply = c(1L, 1L, 0L, 0L, 0L, -2L),
                     groups = factor(rep("a", 6))))
    node.labels(nodes)  <- nodes[['name']]

    arcs <- new("ArcInfo",
                matches = data.frame(
                    groups = factor(rep("a", 2)),
                    upstream   = factor(c("A", "B" ), levels=node.labels(nodes)),
                    downstream = factor(c("D", "E"), levels=node.labels(nodes))
                ),
                bookkeeping = data.frame(
                    groups = factor(rep("a", 3)),
                    start = factor(c("C", "D", "E"), levels=node.labels(nodes)),
                    end = factor(c("(_End_)", "(_End_)", "(_End_)"), levels=node.labels(nodes)),
                    flow = c(0L, 1L, 1L),
                    capacity = rep(1L, 3)
                ))
    subprob  <- new("SubProbInfo",
                    data.frame(groups="a", flipped=flipped, hashed_dist=character(1),
                               resolution=NA_real_, lagrangian_value=NA_real_, dual_value=NA_real_,
                               feasible=NA, exceedance=NA_real_, stringsAsFactors=FALSE)
                    )
    mcf_solution  <- new("MCFSolutions", subproblems=subprob, nodes=nodes, arcs=arcs)
    list(x = x, m = m, mcf = mcf_solution)
}

make_caliper_example <- function(flipped=FALSE) {
    opt <- make_known_optimal(flipped=flipped)
    opt$cal <- caliper(opt$m, 3, values = TRUE)
    tmp <- opt$mcf@nodes
    tmp$price <- c(3.25, 5.75, 0.25, 3.25, NA_real_, 0)
    opt$mcf@nodes <- new("NodeInfo", tmp)
    node.labels(opt$mcf)  <- setNames(tmp$name, tmp$name)

    opt$mcf@arcs <- new("ArcInfo",
                    matches = data.frame(
                        groups = factor(rep("a", 2)),
                        upstream   = factor(c("A", "B" ),levels=node.labels(opt$mcf)),
                        downstream = factor(c("C", "D"),levels=node.labels(opt$mcf))
                    ),
                    bookkeeping = data.frame(
                        groups = factor(rep("a", 2)),
                        start = factor(c("C", "D"),levels=node.labels(opt$mcf)),
                        end = factor(c("(_End_)", "(_End_)"),levels=node.labels(opt$mcf)),
                        flow = c(1L, 1L),
                        capacity = rep(1L, 2)))
    return(opt)
}

make_known_optimal_fullm <- function(flipped=FALSE)
{
    x <- data.frame(row.names = c("a", "b", "c", "d", "e"),
                    z = c(rep(!flipped, 3), rep(flipped, 2)),
                    y = c(0, 2, 4, 0, 6))
    m <- match_on(z ~ y, data = x, method = "euclidean")
    m  <- as.InfinitySparseMatrix(m)
    nodes <- new("NodeInfo",
                 data.frame(stringsAsFactors = FALSE,
                     name = c("a", "b", "c", "d", "e", '(_Sink_)', '(_End_)'),
                     price = c(6, 4, 4, 0, 0, 0, 0),
                     upstream_not_down = c(TRUE, TRUE, TRUE, FALSE, FALSE, NA, NA),
                     supply = c(2L, 2L, 2L, 0L, 0L, -2L, -4L),
                     groups = factor(rep("b", 7))))
    arcs <- new("ArcInfo",
                matches=data.frame(
                    groups=factor("b"),
                    upstream=factor(c(1, 2, 3),levels=node.labels(nodes)),
                    downstream=factor(c(4, 4, 5),levels=node.labels(nodes))
                ),
                bookkeeping=data.frame(
                    groups=factor("b"),
                    start=factor(c(1, 2, 3, 4, 5, 4, 5),levels=node.labels(nodes)),
                    end=factor(c(7, 7, 7, 7, 7, 6, 6),levels=node.labels(nodes)),
                    flow=as.integer(c(1, 1, 1, 1, 0, 1, 1)),
                    capacity=as.integer(c(1, 1, 1, 2, 2, 1, 1))
                )
                )
subprob  <- new("SubProbInfo",
                    data.frame(groups='b', flipped=flipped, hashed_dist=character(1),
                               resolution=NA_real_, lagrangian_value=NA_real_, dual_value=NA_real_,
                               feasible=NA, exceedance=NA_real_, stringsAsFactors=FALSE)
                    )
    mcf_solution  <- new("MCFSolutions", subproblems=subprob, nodes=nodes, arcs=arcs)
    node.labels(mcf_solution)  <- names(node.labels(mcf_solution)) # for alignment w/ m
    list(x = x, m = m, mcf = mcf_solution)
}
test_that("Compute primal", {
    opt <- make_known_optimal()
    expect_equal(evaluate_primal(opt$m, opt$mcf), 4)
  ## repeat with a dense density matrix
    expect_equal(evaluate_primal(new("DenseMatrix", as.matrix(opt$m)), opt$mcf), 4)

    ## now do it with a calipered version of the problem.
    cal <- make_caliper_example()
    expect_equal(evaluate_primal(cal$cal, cal$mcf), 5)

    ## simple f.m. problem (with both End and Sink bookkeeping nodes)
    optfm  <- make_known_optimal_fullm()
    expect_equal(evaluate_primal(optfm$m, optfm$mcf), 4)
    ## 'flipped' variant
    opt.f  <- make_known_optimal(flipped=TRUE)
    expect_equal(evaluate_primal(opt.f$m, opt.f$mcf), 4)

})
test_that("Compute Lagrangian", {
    opt <- make_known_optimal()
    ## since the above arcs represents the optimal, the lagrangian at this point should be equal to the
    ## primal objective function (ie., the sum of matched distances).
    expect_equal(evaluate_lagrangian(opt$m, opt$mcf), 4)

    ## repeat with a dense density matrix
    expect_equal(evaluate_lagrangian(new("DenseMatrix", as.matrix(opt$m)), opt$mcf), 4)

    ## now do it with a calipered version of the problem.
    cal <- make_caliper_example()
    expect_equal(evaluate_lagrangian(cal$cal, cal$mcf), 5)

    ## this shouldn't change when we use the full version, since none of those edges contribute flow or capacity
    expect_equal(evaluate_lagrangian(cal$m, cal$mcf), 5)

    ## simple f.m. problem (with both End and Sink bookkeeping nodes)
    optfm  <- make_known_optimal_fullm()
    expect_equal(evaluate_lagrangian(optfm$m, optfm$mcf), 4)
    ## 'flipped' variant
    opt.f  <- make_known_optimal(flipped=TRUE)
    expect_equal(evaluate_lagrangian(opt.f$m, opt.f$mcf), 4)
})


test_that("Compute dual functional", {
    opt <- make_known_optimal()

    ## since the above arcs represent the optimum, the dual functional at this point should be equal to the
    ## primal objective function (ie., the sum of matched distances).
    expect_equal(evaluate_dual(opt$m, opt$mcf), 4)

    ## repeat with a dense density matrix
    expect_equal(evaluate_dual(new("DenseMatrix", as.matrix(opt$m)), opt$mcf), 4)

    ## now do it with a calipered version of the problem.
    cal <- make_caliper_example()
    expect_equal(evaluate_dual(cal$cal, cal$mcf), 5)

    ## the caliper isn't optimal for the full problem, so the dual decreases using the full distance matrix
    expect_equal(evaluate_dual(cal$m, cal$mcf), 2.75)

    ## simple f.m. problem (with both End and Sink bookkeeping nodes)
    optfm  <- make_known_optimal_fullm()
    expect_equal(evaluate_dual(optfm$m, optfm$mcf), 4)
    ## 'flipped' variant
    opt.f  <- make_known_optimal(flipped=TRUE)
    expect_equal(evaluate_dual(opt.f$m, opt.f$mcf), 4)
})

context("Complementary slackness, multiple subproblems")

make_known_2subprobs  <- function(flipped=c(FALSE, FALSE))
    {
        o1  <- make_known_optimal(flipped=flipped[1])
        o2  <- make_known_optimal_fullm(flipped=flipped[2])
        stopifnot(length(intersect(rownames(o1$m), rownames(o2$m)))==0,
                  length(intersect(colnames(o1$m), colnames(o2$m)))==0)
        newx  <- rbind(o1$x, o2$x)
        newm  <- matrix(Inf,
                        nrow=(nrow(o1$m)+nrow(o2$m)),
                        ncol=(ncol(o1$m)+ncol(o2$m)),
                        dimnames=list(c(rownames(o1$m), rownames(o2$m)),
                                      c(colnames(o1$m), colnames(o2$m))
                                      )
                        )
        newm[rownames(o1$m), colnames(o1$m)]  <- o1$m
        newm[rownames(o2$m), colnames(o2$m)]  <- o2$m
        newm  <- as.InfinitySparseMatrix(newm)
        newmcf  <- c(o1$mcf, o2$mcf)
        list(x=newx, m=newm, mcf=newmcf)
        }

test_that("Compute primal", {
    opt <- make_known_2subprobs()
    expect_equal(evaluate_primal(opt$m, opt$mcf), 8)
    ## partly 'flipped' variant
    opt.f  <- make_known_2subprobs(flipped=c(FALSE, TRUE))
    expect_equal(evaluate_primal(opt.f$m, opt.f$mcf), 8)
})

test_that("Compute Lagrangian", {
    opt <- make_known_2subprobs()
    ## since the above arcs represents the optimal, the lagrangian
    ## at this point should be equal to the primal
    ## objective function (ie., the sum of matched distances).
    expect_equal(evaluate_lagrangian(opt$m, opt$mcf), 8)
    ## 'flipped' variant
    opt.f  <- make_known_2subprobs(flipped=c(FALSE, TRUE))
    expect_equal(evaluate_lagrangian(opt.f$m, opt.f$mcf), 8)
})

test_that("Compute dual functional", {
    opt <- make_known_2subprobs()
    ## since the above arcs represents the optimal, the dual
    ## functional at this point should be equal to the primal
    ## objective function (ie., the sum of matched distances).
    expect_equal(evaluate_dual(opt$m, opt$mcf), 8)
    ## 'flipped' variant
    opt.f  <- make_known_2subprobs(flipped=c(FALSE,TRUE))
    expect_equal(evaluate_dual(opt.f$m, opt.f$mcf), 8)
})

context("Solvers give back optimal solutions")

test_that("Verifying solvers get correct node prices", {
  mytol <- .Machine$double.eps^(1/4)
  x <- 1:5
  z <- c(1, 1, 0, 0, 1)
  units <- paste0("u", 1:5)
  names(x) <- names(z) <- units
  df <- data.frame(x, z)
  mm <- match_on(z ~ x, data = df)

  mmm <- as.matrix(mm)
  min_dist <- sum(mmm[1:2, 1]) + mm[3, 2]

  match_lemon <- fullmatch(mm, data = df, solver = "LEMON")
  mcfs_lemon <- attr(match_lemon, "MCFSolutions")

  expect_equal(evaluate_primal(mm, mcfs_lemon), min_dist)

  expect_equal(evaluate_dual(mm, mcfs_lemon), min_dist)

  if (requireNamespace("rrelaxiv", quietly = TRUE)) {
    match_relax <- fullmatch(mm, data = df, solver = "RELAX-IV", tol=mytol/10)
    mcfs_relax <- attr(match_relax, "MCFSolutions")
    expect_equal(evaluate_primal(mm, mcfs_relax), min_dist)
    expect_equal(evaluate_dual(mm, mcfs_relax), min_dist, tolerance = mytol)
  }


  ## some examples from the nuclearplants data set

  data(nuclearplants)
  npm <- match_on(pt ~ . - pt, data = nuclearplants)
  np_lemon <- fullmatch(npm, data = nuclearplants, solver = "LEMON",
                        tol = mytol/10)
  primal_lemon <- evaluate_primal(npm, attr(np_lemon, "MCFSolutions"))
  dual_lemon <- evaluate_dual(npm, attr(np_lemon, "MCFSolutions"))

  expect_equal(primal_lemon, dual_lemon, tol = mytol)

  if (requireNamespace("rrelaxiv", quietly = TRUE)) {
    np_relax <- fullmatch(npm, data = nuclearplants, solver = "RELAX-IV",
                        tol = mytol/10)
    primal_relax <- evaluate_primal(npm, attr(np_relax, "MCFSolutions"))
    dual_relax <- evaluate_dual(npm, attr(np_relax, "MCFSolutions"))

    expect_equal(primal_relax, primal_lemon, tol = mytol)
    expect_equal(primal_relax, dual_relax, tol = mytol)
  }

  npm2 <- match_on(pr ~ cost + t1, data = nuclearplants)
  np2_lemon <- fullmatch(npm2, min.controls = 1, max.controls = 3,
                         data = nuclearplants, solver = "LEMON",
                        tol = mytol/10)
  primal2_lemon <- evaluate_primal(npm2, attr(np2_lemon, "MCFSolutions"))
  dual2_lemon <- evaluate_dual(npm2, attr(np2_lemon, "MCFSolutions"))

  expect_equal(primal2_lemon, dual2_lemon, tol = mytol)


  if (requireNamespace("rrelaxiv", quietly = TRUE)) {
    np2_relax <- fullmatch(npm2, min.controls = 1, max.controls = 3,
                           data = nuclearplants, solver = "RELAX-IV",
                        tol = mytol/10)
    primal2_relax <- evaluate_primal(npm2, attr(np2_relax, "MCFSolutions"))
    dual2_relax <- evaluate_dual(npm2, attr(np2_relax, "MCFSolutions"))

    expect_equal(primal2_relax, primal2_lemon, tol = mytol)
    expect_equal(primal2_relax, dual2_relax, tol = mytol)
  }

  lemons <-  c("CycleCancelling", "CapacityScaling",
                               "CostScaling", "NetworkSimplex")

  for (solver in lemons) {
    np2_solver <- fullmatch(npm2, min.controls = 1, max.controls = 3, data = nuclearplants, solver = LEMON(solver),
                        tol = mytol/10)
    mcf2_solver <- attr(np2_solver, "MCFSolutions")
    expect_equal(evaluate_dual(npm2, mcf2_solver), dual2_lemon, label = solver, tol = mytol)
  }

  # this data frme is just to silence some warnings from fullmatch
  d <- data.frame(rep(1, 6))
  rownames(d) <- LETTERS[1:6]

  # this little example was leading to different prices right out of fmatch
  v <- c(1, Inf, 2,
         2, 1, Inf,
         3, 2, 1)
  m <- matrix(v, nrow = 3, ncol = 3)
  colnames(m) <- c("A", "B", "C")
  rownames(m) <- c("D", "E", "F")
  mm <- as.InfinitySparseMatrix(m)
  mm_lemon <- fullmatch(mm, data = d,
                        tol = mytol/10)
  mm_mcfs  <- attr(mm_lemon, "MCFSolutions")
  mm_dual <- evaluate_dual(mm, mm_mcfs)

  for (solver in lemons) {
    np2_solver <- fullmatch(mm, solver = LEMON(solver), data = d,
                        tol = mytol/10)
    mcf2_solver <- attr(np2_solver, "MCFSolutions")
    expect_equal(evaluate_dual(mm, mcf2_solver), mm_dual, label = solver, tol = mytol)
  }

})

Try the optmatch package in your browser

Any scripts or data that you put into this service are public.

optmatch documentation built on Nov. 16, 2023, 5:06 p.m.