R/256_zzz_R_specific_to_latex.R

Defines functions .format_numeric .paren_if .is_zero_constant .is_effectively_scalar .peel_promote .build_latex_names .name_to_latex

#####
## DO NOT EDIT THIS FILE!! EDIT THE SOURCE INSTEAD: rsrc_tree/zzz_R_specific/to_latex.R
#####

## R-SPECIFIC: LaTeX export for CVXR expressions, constraints, and problems
##
## Renders Problem/Expression/Constraint objects as publication-quality LaTeX.
## Output uses macros from dcp.sty (shipped in inst/sty/dcp.sty).
##
## Architecture:
##   to_latex(Problem)     -> optidef mini*/maxi* environment
##   to_latex(Expression)  -> math-mode LaTeX string
##   to_latex(Constraint)  -> constraint relation string
##   to_latex(Objective)   -> objective expression
##
## Internal workhorse: .to_latex_prec(x, names_map) returns list(latex, prec)
## for parenthesization control.
##
## Design: ZERO changes to core files. Generic defined here (like smith_annotation
## in visualize_annotations.R). All method() registrations are in this file.


# ==========================================================================
# S7 generic
# ==========================================================================

#' Convert CVXR Object to LaTeX
#'
#' Renders a CVXR \code{Problem}, \code{Expression}, or \code{Constraint}
#' as a LaTeX string.  Problem-level output uses the \code{optidef} package
#' (\code{mini*}/\code{maxi*} environments) and atom macros from
#' \code{dcp.sty} (shipped as \code{system.file("sty", "dcp.sty", package = "CVXR")}).
#'
#' @param x A \code{Problem}, \code{Expression}, \code{Constraint}, or
#'   \code{Objective}.
#' @param ... Reserved for future options.
#' @returns A character string containing LaTeX code.
#'
#' @examples
#' x <- Variable(3, name = "x")
#' cat(to_latex(p_norm(x, 2)))
#' # \cvxnorm{x}_2
#'
#' @export
to_latex <- new_generic("to_latex", "x",
  function(x, ...) S7_dispatch()
)


# ==========================================================================
# Precedence constants
# ==========================================================================

.LATEX_PREC <- list(
  COMPARE = 0L,
  ADD     = 10L,
  MUL     = 20L,
  UNARY   = 30L,
  POW     = 40L,
  FUNC    = 50L,
  ATOM    = 60L
)


# ==========================================================================
# Greek letter mapping
# ==========================================================================

.GREEK_LETTERS <- c(
  "alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta",

"iota", "kappa", "lambda", "mu", "nu", "xi", "pi", "rho",
  "sigma", "tau", "upsilon", "phi", "chi", "psi", "omega",
  ## Uppercase
  "Gamma", "Delta", "Theta", "Lambda", "Xi", "Pi", "Sigma",
  "Upsilon", "Phi", "Psi", "Omega"
)


# ==========================================================================
# Name conversion helpers
# ==========================================================================

#' Convert a variable/parameter name to LaTeX
#' @param nm Character string name
#' @returns LaTeX representation
#' @noRd
.name_to_latex <- function(nm) {
  if (nchar(nm) == 0L) return("")

  ## Single letter: pass through

  if (nchar(nm) == 1L) return(nm)

  ## Greek letter with trailing digits -> \greek_{digits}
  for (gl in .GREEK_LETTERS) {
    if (startsWith(nm, gl)) {
      suffix <- substring(nm, nchar(gl) + 1L)
      if (nchar(suffix) == 0L) {
        return(paste0("\\", gl))
      }
      if (grepl("^[0-9]+$", suffix)) {
        return(paste0("\\", gl, "_{", suffix, "}"))
      }
    }
  }

  ## Recognized suffix patterns: x_hat -> \hat{x}, x_bar -> \bar{x}, x_tilde -> \tilde{x}
  hat_match <- regmatches(nm, regexec("^(.)_hat$", nm))[[1L]]
  if (length(hat_match) == 2L) {
    return(paste0("\\hat{", hat_match[2L], "}"))
  }
  bar_match <- regmatches(nm, regexec("^(.)_bar$", nm))[[1L]]
  if (length(bar_match) == 2L) {
    return(paste0("\\bar{", bar_match[2L], "}"))
  }
  tilde_match <- regmatches(nm, regexec("^(.)_tilde$", nm))[[1L]]
  if (length(tilde_match) == 2L) {
    return(paste0("\\tilde{", tilde_match[2L], "}"))
  }

  ## Multi-character: wrap in \mathit
  paste0("\\mathit{", nm, "}")
}


# ==========================================================================
# Problem-level name resolution (collision-safe)
# ==========================================================================

#' Build collision-safe name table for a Problem
#' @param problem A Problem object
#' @returns An environment mapping id (as character) -> LaTeX name
#' @noRd
.build_latex_names <- function(problem) {
  vars <- variables(problem)
  params <- parameters(problem)
  all_leaves <- c(vars, params)

  if (length(all_leaves) == 0L) {
    return(new.env(hash = TRUE, parent = emptyenv()))
  }

  ## Map: id -> latex_name
  names_map <- new.env(hash = TRUE, parent = emptyenv())

  ## Pass 1: assign base names
  for (leaf in all_leaves) {
    id_key <- as.character(leaf@id)
    if (nzchar(leaf@.latex_name)) {
      ## User-provided .latex_name takes absolute priority
      assign(id_key, leaf@.latex_name, envir = names_map)
    } else {
      nm <- expr_name(leaf)
      assign(id_key, .name_to_latex(nm), envir = names_map)
    }
  }

  ## Pass 2: detect collisions, disambiguate with subscript ID
  latex_vals <- as.list(names_map)
  seen <- new.env(hash = TRUE, parent = emptyenv())
  colliders <- character(0)
  for (id_key in names(latex_vals)) {
    lname <- latex_vals[[id_key]]
    if (exists(lname, envir = seen, inherits = FALSE)) {
      colliders <- c(colliders, lname)
    }
    assign(lname, TRUE, envir = seen)
  }

  if (length(colliders) > 0L) {
    colliders <- unique(colliders)
    for (id_key in names(latex_vals)) {
      if (latex_vals[[id_key]] %in% colliders) {
        old <- latex_vals[[id_key]]
        latex_vals[[id_key]] <- paste0(old, "_{", id_key, "}")
        cli_warn("Multiple variables/parameters map to LaTeX name {.val {old}}; disambiguating with ID subscripts.")
      }
    }
    ## Rebuild env
    names_map <- list2env(latex_vals, hash = TRUE, parent = emptyenv())
  }

  names_map
}


# ==========================================================================
# Shape / zero-detection helpers
# ==========================================================================

## Peel through Promote wrappers to get the "real" expression
.peel_promote <- function(expr) {
  while (S7_inherits(expr, Promote)) {
    expr <- expr@args[[1L]]
  }
  expr
}

## Is the expression effectively scalar (possibly wrapped in Promote)?
.is_effectively_scalar <- function(expr) {
  prod(.peel_promote(expr)@shape) == 1L
}

## Is the expression a zero constant (possibly promoted)?
.is_zero_constant <- function(expr) {
  inner <- .peel_promote(expr)
  if (!S7_inherits(inner, Constant)) return(FALSE)
  v <- inner@.value
  isTRUE(all(as.numeric(v) == 0))
}


# ==========================================================================
# Parenthesization helper
# ==========================================================================

#' Wrap LaTeX in parens if inner precedence is lower than outer
#' @noRd
.paren_if <- function(inner_latex, inner_prec, outer_prec) {
  if (inner_prec < outer_prec) {
    paste0("\\left(", inner_latex, "\\right)")
  } else {
    inner_latex
  }
}


# ==========================================================================
# Internal workhorse: .to_latex_prec()
# ==========================================================================
## Returns list(latex = character(1), prec = integer(1))
## names_map: environment (id -> LaTeX name) or NULL for standalone

.to_latex_prec <- new_generic(".to_latex_prec", "x",
  function(x, names_map = NULL, ...) S7_dispatch()
)


# ==========================================================================
# Numeric formatting
# ==========================================================================

.format_numeric <- function(val, digits = 4L) {
  if (length(val) == 1L) {
    if (is.na(val)) return("\\mathrm{NA}")
    if (is.infinite(val)) {
      return(if (val > 0) "\\infty" else "-\\infty")
    }
    ## Integer-valued floats: drop decimals
    if (is.numeric(val) && val == round(val) && abs(val) < 1e12) {
      return(as.character(as.integer(val)))
    }
    return(format(val, digits = digits))
  }
  ## Matrix/vector: shouldn't reach here normally
  as.character(val)
}


# ==========================================================================
# Leaf methods
# ==========================================================================

## -- Variable --
method(.to_latex_prec, Variable) <- function(x, names_map = NULL, ...) {
  if (!is.null(names_map)) {
    id_key <- as.character(x@id)
    if (exists(id_key, envir = names_map, inherits = FALSE)) {
      return(list(latex = get(id_key, envir = names_map), prec = .LATEX_PREC$ATOM))
    }
  }
  ## Standalone: use .latex_name if set, else .name_to_latex(expr_name)
  if (nzchar(x@.latex_name)) {
    return(list(latex = x@.latex_name, prec = .LATEX_PREC$ATOM))
  }
  list(latex = .name_to_latex(expr_name(x)), prec = .LATEX_PREC$ATOM)
}

## -- Parameter --
method(.to_latex_prec, Parameter) <- function(x, names_map = NULL, ...) {
  if (!is.null(names_map)) {
    id_key <- as.character(x@id)
    if (exists(id_key, envir = names_map, inherits = FALSE)) {
      return(list(latex = get(id_key, envir = names_map), prec = .LATEX_PREC$ATOM))
    }
  }
  if (nzchar(x@.latex_name)) {
    return(list(latex = x@.latex_name, prec = .LATEX_PREC$ATOM))
  }
  list(latex = .name_to_latex(expr_name(x)), prec = .LATEX_PREC$ATOM)
}

## -- Constant --
method(.to_latex_prec, Constant) <- function(x, names_map = NULL, ...) {
  v <- x@.value
  shape <- x@shape
  if (prod(shape) == 1L) {
    ## Scalar constant
    val <- as.numeric(v[1L])
    ltx <- .format_numeric(val)
    prec <- if (val < 0) .LATEX_PREC$ADD else .LATEX_PREC$ATOM
    return(list(latex = ltx, prec = prec))
  }
  ## Named constant
  if (nchar(x@.name) > 0L) {
    return(list(latex = .name_to_latex(x@.name), prec = .LATEX_PREC$ATOM))
  }
  ## Matrix/vector: abbreviated
  list(
    latex = sprintf("\\mathit{[%dx%d]}", shape[1L], shape[2L]),
    prec = .LATEX_PREC$ATOM
  )
}


# ==========================================================================
# Atom fallback (default for any atom without a specialized method)
# ==========================================================================

method(.to_latex_prec, Atom) <- function(x, names_map = NULL, ...) {
  cls <- sub("^CVXR::", "", class(x)[[1L]])
  args_latex <- vapply(x@args, function(a) {
    .to_latex_prec(a, names_map)$latex
  }, character(1))
  list(
    latex = sprintf("\\operatorname{%s}\\left(%s\\right)", cls,
                    paste(args_latex, collapse = ", ")),
    prec = .LATEX_PREC$FUNC
  )
}


# ==========================================================================
# Affine operators
# ==========================================================================

## -- AddExpression --
method(.to_latex_prec, AddExpression) <- function(x, names_map = NULL, ...) {
  n <- length(x@args)
  if (n == 0L) return(list(latex = "0", prec = .LATEX_PREC$ATOM))

  parts <- character(n)
  for (i in seq_len(n)) {
    res <- .to_latex_prec(x@args[[i]], names_map)
    if (i == 1L) {
      parts[i] <- res$latex
    } else {
      ## Check if this arg is a negation: render as " - ..." instead of " + -..."
      ## After broadcast_args, NegExpression may be wrapped in Promote
      ai <- x@args[[i]]
      neg_inner <- if (S7_inherits(ai, NegExpression)) ai@args[[1L]]
        else if (S7_inherits(ai, Promote) && S7_inherits(ai@args[[1L]], NegExpression)) ai@args[[1L]]@args[[1L]]
        else NULL
      if (!is.null(neg_inner)) {
        inner <- .to_latex_prec(neg_inner, names_map)
        parts[i] <- paste0(" - ", .paren_if(inner$latex, inner$prec, .LATEX_PREC$ADD))
      } else if (startsWith(res$latex, "-")) {
        parts[i] <- paste0(" - ", substring(res$latex, 2L))
      } else {
        parts[i] <- paste0(" + ", res$latex)
      }
    }
  }
  list(latex = paste0(parts, collapse = ""), prec = .LATEX_PREC$ADD)
}

## -- NegExpression --
method(.to_latex_prec, NegExpression) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  list(
    latex = paste0("-", .paren_if(inner$latex, inner$prec, .LATEX_PREC$UNARY)),
    prec = .LATEX_PREC$UNARY
  )
}

## -- MulExpression (matrix multiply %*%) --
method(.to_latex_prec, MulExpression) <- function(x, names_map = NULL, ...) {
  lhs <- .to_latex_prec(x@args[[1L]], names_map)
  rhs <- .to_latex_prec(x@args[[2L]], names_map)
  ## For matrix multiply, juxtaposition (no operator) is standard;
  ## use explicit space for readability
  l <- .paren_if(lhs$latex, lhs$prec, .LATEX_PREC$MUL)
  r <- .paren_if(rhs$latex, rhs$prec, .LATEX_PREC$MUL)
  list(latex = paste0(l, " ", r), prec = .LATEX_PREC$MUL)
}

## -- Multiply (elementwise *) --
method(.to_latex_prec, Multiply) <- function(x, names_map = NULL, ...) {
  lhs <- .to_latex_prec(x@args[[1L]], names_map)
  rhs <- .to_latex_prec(x@args[[2L]], names_map)
  l <- .paren_if(lhs$latex, lhs$prec, .LATEX_PREC$MUL)
  r <- .paren_if(rhs$latex, rhs$prec, .LATEX_PREC$MUL)
  ## Use \circ for Hadamard product only when BOTH sides are genuinely

  ## non-scalar. Promote wraps a scalar to match the other side's shape,
  ## so peel through it before checking.
  if (!.is_effectively_scalar(x@args[[1L]]) && !.is_effectively_scalar(x@args[[2L]])) {
    list(latex = paste0(l, " \\circ ", r), prec = .LATEX_PREC$MUL)
  } else {
    list(latex = paste0(l, " ", r), prec = .LATEX_PREC$MUL)
  }
}

## -- DivExpression --
method(.to_latex_prec, DivExpression) <- function(x, names_map = NULL, ...) {
  num <- .to_latex_prec(x@args[[1L]], names_map)
  den <- .to_latex_prec(x@args[[2L]], names_map)
  list(
    latex = paste0("\\frac{", num$latex, "}{", den$latex, "}"),
    prec = .LATEX_PREC$MUL
  )
}

## -- Transpose --
method(.to_latex_prec, Transpose) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  ## Use braces to scope the ^T
  ltx <- if (inner$prec >= .LATEX_PREC$ATOM) {
    paste0(inner$latex, "\\T")
  } else {
    paste0("\\left(", inner$latex, "\\right)\\T")
  }
  list(latex = ltx, prec = .LATEX_PREC$ATOM)
}

## -- Index --
method(.to_latex_prec, Index) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  orig_shape <- x@args[[1L]]@shape

  ## Guard against double subscripts and ambiguous subscript scope:
  ## If the inner expression is compound (not ATOM-level), wrapping _{...}
  ## would only subscript the last token. Wrap in parens first.
  base <- if (inner$prec < .LATEX_PREC$ATOM) {
    paste0("\\left(", inner$latex, "\\right)")
  } else {
    inner$latex
  }

  ## Format row index
  row_idx <- x@key[[1L]]
  col_idx <- x@key[[2L]]
  row_all <- identical(row_idx, seq_len(orig_shape[1L]))
  col_all <- identical(col_idx, seq_len(orig_shape[2L]))

  .fmt_idx <- function(idx, dim_len) {
    if (identical(idx, seq_len(dim_len))) return(":")
    if (length(idx) == 1L) return(as.character(idx))
    ## Contiguous range
    if (length(idx) > 1L && all(diff(idx) == 1L)) {
      return(paste0(idx[1L], ":", idx[length(idx)]))
    }
    paste(idx, collapse = ",")
  }

  row_str <- .fmt_idx(row_idx, orig_shape[1L])
  col_str <- .fmt_idx(col_idx, orig_shape[2L])

  ## Single element: x_{ij}
  if (length(row_idx) == 1L && length(col_idx) == 1L) {
    sub <- paste0(row_str, col_str)
    if (nchar(sub) <= 2L) {
      ltx <- paste0(base, "_{", sub, "}")
    } else {
      ltx <- paste0(base, "_{", row_str, ",", col_str, "}")
    }
    return(list(latex = ltx, prec = .LATEX_PREC$ATOM))
  }

  ## Column vector: x_{i:j} or x_{:}
  if (orig_shape[2L] == 1L) {
    ltx <- paste0(base, "_{", row_str, "}")
    return(list(latex = ltx, prec = .LATEX_PREC$ATOM))
  }

  ## General slice
  ltx <- paste0(base, "_{", row_str, ",", col_str, "}")
  list(latex = ltx, prec = .LATEX_PREC$ATOM)
}

## -- SumEntries --
method(.to_latex_prec, SumEntries) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  axis <- x@axis
  if (is.null(axis)) {
    ## Sum of all entries — adapt to argument shape
    arg_shape <- x@args[[1L]]@shape
    i <- .paren_if(inner$latex, inner$prec, .LATEX_PREC$MUL)
    if (arg_shape[2L] == 1L) {
      ## Column vector: 1^T x suffices
      ltx <- paste0("\\ones\\T ", i)
    } else if (arg_shape[1L] == 1L) {
      ## Row vector: x 1 suffices
      ltx <- paste0(i, " \\ones")
    } else {
      ## General matrix: 1^T X 1
      ltx <- paste0("\\ones\\T ", i, " \\ones")
    }
    list(latex = ltx, prec = .LATEX_PREC$MUL)
  } else {
    ## Axis-wise sum: use \sum notation
    list(
      latex = paste0("\\operatorname{sum}\\left(", inner$latex,
                     ", \\text{axis}=", axis, "\\right)"),
      prec = .LATEX_PREC$FUNC
    )
  }
}

## -- Trace --
method(.to_latex_prec, Trace) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  list(
    latex = paste0("\\tr\\left(", inner$latex, "\\right)"),
    prec = .LATEX_PREC$FUNC
  )
}

## -- Kron (Kronecker product) --
method(.to_latex_prec, Kron) <- function(x, names_map = NULL, ...) {
  lhs <- .to_latex_prec(x@args[[1L]], names_map)
  rhs <- .to_latex_prec(x@args[[2L]], names_map)
  list(
    latex = paste0(lhs$latex, " \\otimes ", rhs$latex),
    prec = .LATEX_PREC$MUL
  )
}

## -- Reshape --
method(.to_latex_prec, Reshape) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  list(
    latex = paste0("\\operatorname{reshape}\\left(", inner$latex,
                   ", ", x@shape[1L], ", ", x@shape[2L], "\\right)"),
    prec = .LATEX_PREC$FUNC
  )
}

## -- Promote (transparent: just render the inner expression) --
method(.to_latex_prec, Promote) <- function(x, names_map = NULL, ...) {
  .to_latex_prec(x@args[[1L]], names_map)
}

## -- Conj_ (complex conjugate) --
method(.to_latex_prec, Conj_) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  list(
    latex = paste0("\\overline{", inner$latex, "}"),
    prec = .LATEX_PREC$ATOM
  )
}

## -- Real_ --
method(.to_latex_prec, Real_) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  list(
    latex = paste0("\\operatorname{Re}\\left(", inner$latex, "\\right)"),
    prec = .LATEX_PREC$FUNC
  )
}

## -- Imag_ --
method(.to_latex_prec, Imag_) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  list(
    latex = paste0("\\operatorname{Im}\\left(", inner$latex, "\\right)"),
    prec = .LATEX_PREC$FUNC
  )
}

## -- HStack --
method(.to_latex_prec, HStack) <- function(x, names_map = NULL, ...) {
  parts <- vapply(x@args, function(a) {
    .to_latex_prec(a, names_map)$latex
  }, character(1))
  list(
    latex = paste0("\\begin{bmatrix} ", paste(parts, collapse = " & "), " \\end{bmatrix}"),
    prec = .LATEX_PREC$ATOM
  )
}

## -- VStack --
method(.to_latex_prec, VStack) <- function(x, names_map = NULL, ...) {
  parts <- vapply(x@args, function(a) {
    .to_latex_prec(a, names_map)$latex
  }, character(1))
  list(
    latex = paste0("\\begin{bmatrix} ", paste(parts, collapse = " \\\\ "), " \\end{bmatrix}"),
    prec = .LATEX_PREC$ATOM
  )
}

## -- DiagVec --
method(.to_latex_prec, DiagVec) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  list(
    latex = paste0("\\diag\\left(", inner$latex, "\\right)"),
    prec = .LATEX_PREC$FUNC
  )
}

## -- DiagMat --
method(.to_latex_prec, DiagMat) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  list(
    latex = paste0("\\diag\\left(", inner$latex, "\\right)"),
    prec = .LATEX_PREC$FUNC
  )
}

## -- UpperTri --
method(.to_latex_prec, UpperTri) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  list(
    latex = paste0("\\operatorname{upper\\_tri}\\left(", inner$latex, "\\right)"),
    prec = .LATEX_PREC$FUNC
  )
}

## -- Convolve --
method(.to_latex_prec, Convolve) <- function(x, names_map = NULL, ...) {
  lhs <- .to_latex_prec(x@args[[1L]], names_map)
  rhs <- .to_latex_prec(x@args[[2L]], names_map)
  list(
    latex = paste0(lhs$latex, " * ", rhs$latex),
    prec = .LATEX_PREC$MUL
  )
}

## -- Cumsum --
method(.to_latex_prec, Cumsum) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  list(
    latex = paste0("\\cumsum\\left(", inner$latex, "\\right)"),
    prec = .LATEX_PREC$FUNC
  )
}

## -- Wrap (transparent) --
method(.to_latex_prec, Wrap) <- function(x, names_map = NULL, ...) {
  .to_latex_prec(x@args[[1L]], names_map)
}


# ==========================================================================
# Elementwise atoms
# ==========================================================================

## -- Abs --
method(.to_latex_prec, Abs) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  list(
    latex = paste0("\\cvxabs{", inner$latex, "}"),
    prec = .LATEX_PREC$ATOM
  )
}

## -- Power --
method(.to_latex_prec, Power) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  p <- x@p_used

  ## Special cases
  if (!is.null(p)) {
    if (p == 0.5) {
      return(list(
        latex = paste0("\\sqrt{", inner$latex, "}"),
        prec = .LATEX_PREC$ATOM
      ))
    }
    if (p == -1) {
      return(list(
        latex = paste0("\\frac{1}{", inner$latex, "}"),
        prec = .LATEX_PREC$MUL
      ))
    }
    ## General power
    base <- if (inner$prec < .LATEX_PREC$POW) {
      paste0("\\left(", inner$latex, "\\right)")
    } else {
      inner$latex
    }
    p_str <- .format_numeric(p)
    return(list(
      latex = paste0("{", base, "}^{", p_str, "}"),
      prec = .LATEX_PREC$POW
    ))
  }
  ## p is not constant: use general notation
  p_latex <- .to_latex_prec(x@p, names_map)$latex
  base <- if (inner$prec < .LATEX_PREC$POW) {
    paste0("\\left(", inner$latex, "\\right)")
  } else {
    inner$latex
  }
  list(
    latex = paste0("{", base, "}^{", p_latex, "}"),
    prec = .LATEX_PREC$POW
  )
}

## -- Exp --
method(.to_latex_prec, Exp) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  list(
    latex = paste0("e^{", inner$latex, "}"),
    prec = .LATEX_PREC$ATOM
  )
}

## -- Log --
method(.to_latex_prec, Log) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  list(
    latex = paste0("\\log\\left(", inner$latex, "\\right)"),
    prec = .LATEX_PREC$FUNC
  )
}

## -- Log1p --
method(.to_latex_prec, Log1p) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  list(
    latex = paste0("\\log\\left(1 + ", inner$latex, "\\right)"),
    prec = .LATEX_PREC$FUNC
  )
}

## -- Ceil --
method(.to_latex_prec, Ceil) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  list(
    latex = paste0("\\cvxceil{", inner$latex, "}"),
    prec = .LATEX_PREC$ATOM
  )
}

## -- Floor --
method(.to_latex_prec, Floor) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  list(
    latex = paste0("\\cvxfloor{", inner$latex, "}"),
    prec = .LATEX_PREC$ATOM
  )
}

## -- Maximum (elementwise max of 2+ args) --
method(.to_latex_prec, Maximum) <- function(x, names_map = NULL, ...) {
  args_latex <- vapply(x@args, function(a) {
    .to_latex_prec(a, names_map)$latex
  }, character(1))
  list(
    latex = paste0("\\max\\left(", paste(args_latex, collapse = ", "), "\\right)"),
    prec = .LATEX_PREC$FUNC
  )
}

## -- Minimum (elementwise min of 2+ args) --
method(.to_latex_prec, Minimum) <- function(x, names_map = NULL, ...) {
  args_latex <- vapply(x@args, function(a) {
    .to_latex_prec(a, names_map)$latex
  }, character(1))
  list(
    latex = paste0("\\min\\left(", paste(args_latex, collapse = ", "), "\\right)"),
    prec = .LATEX_PREC$FUNC
  )
}

## -- Entr (entropy: -x log x) --
method(.to_latex_prec, Entr) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  i <- .paren_if(inner$latex, inner$prec, .LATEX_PREC$MUL)
  list(
    latex = paste0("-", i, " \\log\\left(", inner$latex, "\\right)"),
    prec = .LATEX_PREC$ADD
  )
}

## -- RelEntr (relative entropy: x log(x/y)) --
method(.to_latex_prec, RelEntr) <- function(x, names_map = NULL, ...) {
  x_ltx <- .to_latex_prec(x@args[[1L]], names_map)
  y_ltx <- .to_latex_prec(x@args[[2L]], names_map)
  x_s <- .paren_if(x_ltx$latex, x_ltx$prec, .LATEX_PREC$MUL)
  list(
    latex = paste0(x_s, " \\log\\left(\\frac{", x_ltx$latex, "}{",
                   y_ltx$latex, "}\\right)"),
    prec = .LATEX_PREC$MUL
  )
}

## -- KlDiv (KL divergence: x log(x/y) - x + y) --
method(.to_latex_prec, KlDiv) <- function(x, names_map = NULL, ...) {
  x_ltx <- .to_latex_prec(x@args[[1L]], names_map)
  y_ltx <- .to_latex_prec(x@args[[2L]], names_map)
  x_s <- .paren_if(x_ltx$latex, x_ltx$prec, .LATEX_PREC$MUL)
  list(
    latex = paste0(x_s, " \\log\\left(\\frac{", x_ltx$latex, "}{",
                   y_ltx$latex, "}\\right) - ", x_ltx$latex, " + ", y_ltx$latex),
    prec = .LATEX_PREC$ADD
  )
}

## -- Logistic --
method(.to_latex_prec, Logistic) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  list(
    latex = paste0("\\log\\left(1 + e^{", inner$latex, "}\\right)"),
    prec = .LATEX_PREC$FUNC
  )
}

## -- Huber --
method(.to_latex_prec, Huber) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  M_val <- if (S7_inherits(x@M, Expression)) {
    .to_latex_prec(x@M, names_map)$latex
  } else {
    .format_numeric(as.numeric(x@M))
  }
  list(
    latex = paste0("\\huber_{", M_val, "}\\left(", inner$latex, "\\right)"),
    prec = .LATEX_PREC$FUNC
  )
}

## -- Xexp (x * exp(x)) --
method(.to_latex_prec, Xexp) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  i <- .paren_if(inner$latex, inner$prec, .LATEX_PREC$MUL)
  list(
    latex = paste0(i, " e^{", inner$latex, "}"),
    prec = .LATEX_PREC$MUL
  )
}


# ==========================================================================
# Logic atoms
# ==========================================================================

method(.to_latex_prec, Not) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  list(
    latex = paste0("\\neg ", .paren_if(inner$latex, inner$prec, .LATEX_PREC$UNARY)),
    prec = .LATEX_PREC$UNARY
  )
}

method(.to_latex_prec, And) <- function(x, names_map = NULL, ...) {
  parts <- vapply(x@args, function(a) {
    r <- .to_latex_prec(a, names_map)
    .paren_if(r$latex, r$prec, .LATEX_PREC$MUL)
  }, character(1))
  list(latex = paste(parts, collapse = " \\land "), prec = .LATEX_PREC$MUL)
}

method(.to_latex_prec, Or) <- function(x, names_map = NULL, ...) {
  parts <- vapply(x@args, function(a) {
    r <- .to_latex_prec(a, names_map)
    .paren_if(r$latex, r$prec, .LATEX_PREC$ADD)
  }, character(1))
  list(latex = paste(parts, collapse = " \\lor "), prec = .LATEX_PREC$ADD)
}

method(.to_latex_prec, Xor) <- function(x, names_map = NULL, ...) {
  parts <- vapply(x@args, function(a) {
    r <- .to_latex_prec(a, names_map)
    .paren_if(r$latex, r$prec, .LATEX_PREC$ADD)
  }, character(1))
  list(latex = paste(parts, collapse = " \\oplus "), prec = .LATEX_PREC$ADD)
}


# ==========================================================================
# Norm atoms
# ==========================================================================

## -- Pnorm (general p-norm) --
method(.to_latex_prec, Pnorm) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  p <- x@p  # numeric (class_numeric)
  p_str <- if (is.infinite(p) && p > 0) {
    "\\infty"
  } else if (is.infinite(p) && p < 0) {
    "-\\infty"
  } else if (p == 2) {
    "2"
  } else {
    .format_numeric(p)
  }
  list(
    latex = paste0("\\cvxnorm{", inner$latex, "}_{", p_str, "}"),
    prec = .LATEX_PREC$ATOM
  )
}

## -- Norm1 --
method(.to_latex_prec, Norm1) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  list(
    latex = paste0("\\cvxnorm{", inner$latex, "}_1"),
    prec = .LATEX_PREC$ATOM
  )
}

## -- NormInf --
method(.to_latex_prec, NormInf) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  list(
    latex = paste0("\\cvxnorm{", inner$latex, "}_\\infty"),
    prec = .LATEX_PREC$ATOM
  )
}

## -- NormNuc (nuclear norm) --
method(.to_latex_prec, NormNuc) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  list(
    latex = paste0("\\cvxnorm{", inner$latex, "}_*"),
    prec = .LATEX_PREC$ATOM
  )
}

## -- SigmaMax (spectral norm / max singular value) --
method(.to_latex_prec, SigmaMax) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  list(
    latex = paste0("\\sigmamax\\left(", inner$latex, "\\right)"),
    prec = .LATEX_PREC$FUNC
  )
}


# ==========================================================================
# Reduction atoms (MaxEntries, MinEntries, Prod, SumLargest, etc.)
# ==========================================================================

## -- MaxEntries --
method(.to_latex_prec, MaxEntries) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  list(
    latex = paste0("\\max\\left(", inner$latex, "\\right)"),
    prec = .LATEX_PREC$FUNC
  )
}

## -- MinEntries --
method(.to_latex_prec, MinEntries) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  list(
    latex = paste0("\\min\\left(", inner$latex, "\\right)"),
    prec = .LATEX_PREC$FUNC
  )
}

## -- Prod --
method(.to_latex_prec, Prod) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  list(
    latex = paste0("\\prod\\left(", inner$latex, "\\right)"),
    prec = .LATEX_PREC$FUNC
  )
}

## -- SumLargest --
method(.to_latex_prec, SumLargest) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  k <- x@k
  list(
    latex = paste0("\\operatorname{sum\\_largest}\\left(", inner$latex, ", ", k, "\\right)"),
    prec = .LATEX_PREC$FUNC
  )
}

## -- LogSumExp --
method(.to_latex_prec, LogSumExp) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  list(
    latex = paste0("\\logsumexp\\left(", inner$latex, "\\right)"),
    prec = .LATEX_PREC$FUNC
  )
}

## -- GeoMean --
method(.to_latex_prec, GeoMean) <- function(x, names_map = NULL, ...) {
  args_latex <- vapply(x@args, function(a) {
    .to_latex_prec(a, names_map)$latex
  }, character(1))
  list(
    latex = paste0("\\operatorname{geo\\_mean}\\left(", paste(args_latex, collapse = ", "), "\\right)"),
    prec = .LATEX_PREC$FUNC
  )
}

## -- Cummax --
method(.to_latex_prec, Cummax) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  list(
    latex = paste0("\\operatorname{cummax}\\left(", inner$latex, "\\right)"),
    prec = .LATEX_PREC$FUNC
  )
}

## -- Cumprod --
method(.to_latex_prec, Cumprod) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  list(
    latex = paste0("\\operatorname{cumprod}\\left(", inner$latex, "\\right)"),
    prec = .LATEX_PREC$FUNC
  )
}


# ==========================================================================
# Matrix / spectral atoms
# ==========================================================================

## -- QuadForm --
method(.to_latex_prec, QuadForm) <- function(x, names_map = NULL, ...) {
  x_ltx <- .to_latex_prec(x@args[[1L]], names_map)
  P_ltx <- .to_latex_prec(x@args[[2L]], names_map)
  list(
    latex = paste0(x_ltx$latex, "\\T ", P_ltx$latex, " ", x_ltx$latex),
    prec = .LATEX_PREC$MUL
  )
}

## -- QuadOverLin --
method(.to_latex_prec, QuadOverLin) <- function(x, names_map = NULL, ...) {
  x_ltx <- .to_latex_prec(x@args[[1L]], names_map)
  y_ltx <- .to_latex_prec(x@args[[2L]], names_map)
  list(
    latex = paste0("\\frac{\\cvxnorm{", x_ltx$latex, "}_2^2}{", y_ltx$latex, "}"),
    prec = .LATEX_PREC$MUL
  )
}

## -- LogDet --
method(.to_latex_prec, LogDet) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  list(
    latex = paste0("\\logdet\\left(", inner$latex, "\\right)"),
    prec = .LATEX_PREC$FUNC
  )
}

## -- LambdaMax --
method(.to_latex_prec, LambdaMax) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  list(
    latex = paste0("\\lambdamax\\left(", inner$latex, "\\right)"),
    prec = .LATEX_PREC$FUNC
  )
}

## -- LambdaSumLargest --
method(.to_latex_prec, LambdaSumLargest) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  k <- x@k
  list(
    latex = paste0("\\sum_{i=1}^{", k, "} \\lambda_i\\left(", inner$latex, "\\right)"),
    prec = .LATEX_PREC$FUNC
  )
}

## -- MatrixFrac --
method(.to_latex_prec, MatrixFrac) <- function(x, names_map = NULL, ...) {
  x_ltx <- .to_latex_prec(x@args[[1L]], names_map)
  P_ltx <- .to_latex_prec(x@args[[2L]], names_map)
  list(
    latex = paste0(x_ltx$latex, "\\T ", P_ltx$latex, "^{-1} ", x_ltx$latex),
    prec = .LATEX_PREC$MUL
  )
}

## -- TrInv --
method(.to_latex_prec, TrInv) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  list(
    latex = paste0("\\tr\\left(", inner$latex, "^{-1}\\right)"),
    prec = .LATEX_PREC$FUNC
  )
}

## -- EyeMinusInv --
method(.to_latex_prec, EyeMinusInv) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  list(
    latex = paste0("\\tr\\left(\\left(I - ", inner$latex, "\\right)^{-1}\\right)"),
    prec = .LATEX_PREC$FUNC
  )
}

## -- ConditionNumber --
method(.to_latex_prec, ConditionNumber) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  list(
    latex = paste0("\\condnum\\left(", inner$latex, "\\right)"),
    prec = .LATEX_PREC$FUNC
  )
}

## -- Perspective --
method(.to_latex_prec, Perspective) <- function(x, names_map = NULL, ...) {
  f_ltx <- .to_latex_prec(x@args[[1L]], names_map)
  s_ltx <- .to_latex_prec(x@args[[2L]], names_map)
  list(
    latex = paste0(s_ltx$latex, " \\, \\operatorname{f}\\left(\\frac{",
                   f_ltx$latex, "}{", s_ltx$latex, "}\\right)"),
    prec = .LATEX_PREC$MUL
  )
}

## -- Dotsort --
method(.to_latex_prec, Dotsort) <- function(x, names_map = NULL, ...) {
  args_latex <- vapply(x@args, function(a) {
    .to_latex_prec(a, names_map)$latex
  }, character(1))
  list(
    latex = paste0("\\operatorname{dotsort}\\left(", paste(args_latex, collapse = ", "), "\\right)"),
    prec = .LATEX_PREC$FUNC
  )
}

## -- SymbolicQuadForm --
method(.to_latex_prec, SymbolicQuadForm) <- function(x, names_map = NULL, ...) {
  x_ltx <- .to_latex_prec(x@args[[1L]], names_map)
  P_ltx <- .to_latex_prec(x@args[[2L]], names_map)
  list(
    latex = paste0(x_ltx$latex, "\\T ", P_ltx$latex, " ", x_ltx$latex),
    prec = .LATEX_PREC$MUL
  )
}


# ==========================================================================
# DQCP atoms
# ==========================================================================

## -- GenLambdaMax --
method(.to_latex_prec, GenLambdaMax) <- function(x, names_map = NULL, ...) {
  args_latex <- vapply(x@args, function(a) {
    .to_latex_prec(a, names_map)$latex
  }, character(1))
  list(
    latex = paste0("\\lambda_{\\max}\\left(", paste(args_latex, collapse = ", "), "\\right)"),
    prec = .LATEX_PREC$FUNC
  )
}

## -- DistRatio --
method(.to_latex_prec, DistRatio) <- function(x, names_map = NULL, ...) {
  args_latex <- vapply(x@args, function(a) {
    .to_latex_prec(a, names_map)$latex
  }, character(1))
  list(
    latex = paste0("\\frac{\\cvxnorm{", args_latex[1L], " - ", args_latex[2L], "}_2}{",
                   "\\cvxnorm{", args_latex[1L], " - ", args_latex[3L], "}_2}"),
    prec = .LATEX_PREC$MUL
  )
}


# ==========================================================================
# Constraint methods
# ==========================================================================

## -- Zero (x == 0) --
method(.to_latex_prec, Zero) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  list(
    latex = paste0(inner$latex, " = 0"),
    prec = .LATEX_PREC$COMPARE
  )
}

## -- Equality (lhs == rhs) --
method(.to_latex_prec, Equality) <- function(x, names_map = NULL, ...) {
  lhs <- .to_latex_prec(x@args[[1L]], names_map)
  rhs <- .to_latex_prec(x@args[[2L]], names_map)
  list(
    latex = paste0(lhs$latex, " = ", rhs$latex),
    prec = .LATEX_PREC$COMPARE
  )
}

## -- NonPos (x <= 0) --
method(.to_latex_prec, NonPos) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  list(
    latex = paste0(inner$latex, " \\leq 0"),
    prec = .LATEX_PREC$COMPARE
  )
}

## -- NonNeg (x >= 0) --
method(.to_latex_prec, NonNeg) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  list(
    latex = paste0(inner$latex, " \\geq 0"),
    prec = .LATEX_PREC$COMPARE
  )
}

## -- Inequality (lhs <= rhs) --
method(.to_latex_prec, Inequality) <- function(x, names_map = NULL, ...) {
  ## When LHS is a zero constant (e.g. from x >= 0 which stores
  ## Inequality(0, x)), flip to "RHS >= 0" for readability
  if (.is_zero_constant(x@args[[1L]])) {
    rhs <- .to_latex_prec(x@args[[2L]], names_map)
    return(list(
      latex = paste0(rhs$latex, " \\geq 0"),
      prec = .LATEX_PREC$COMPARE
    ))
  }
  ## When RHS is a zero constant, render as "LHS <= 0"
  if (.is_zero_constant(x@args[[2L]])) {
    lhs <- .to_latex_prec(x@args[[1L]], names_map)
    return(list(
      latex = paste0(lhs$latex, " \\leq 0"),
      prec = .LATEX_PREC$COMPARE
    ))
  }
  lhs <- .to_latex_prec(x@args[[1L]], names_map)
  rhs <- .to_latex_prec(x@args[[2L]], names_map)
  list(
    latex = paste0(lhs$latex, " \\leq ", rhs$latex),
    prec = .LATEX_PREC$COMPARE
  )
}

## -- PSD (X >> 0) --
## PSD stores args = list(expr) where expr = LHS - RHS (from %>>%).
## Try to decompose the subtraction for cleaner rendering.
method(.to_latex_prec, PSD) <- function(x, names_map = NULL, ...) {
  arg <- x@args[[1L]]

  ## Detect A + (-B) pattern from %>>%: PSD(A - B)
  ## After broadcast_args, the NegExpression may be wrapped in Promote
  .unwrap_neg <- function(e) {
    if (S7_inherits(e, NegExpression)) return(e)
    if (S7_inherits(e, Promote) && S7_inherits(e@args[[1L]], NegExpression)) return(e@args[[1L]])
    NULL
  }
  neg_node <- if (S7_inherits(arg, AddExpression) && length(arg@args) == 2L)
    .unwrap_neg(arg@args[[2L]]) else NULL
  if (!is.null(neg_node)) {
    lhs_expr <- arg@args[[1L]]
    ## Unwrap Promote on LHS too (scalar LHS promoted by broadcast_args)
    if (S7_inherits(lhs_expr, Promote)) lhs_expr <- lhs_expr@args[[1L]]
    rhs_expr <- neg_node@args[[1L]]  # unwrap NegExpression
    lhs <- .to_latex_prec(lhs_expr, names_map)

    if (.is_zero_constant(rhs_expr)) {
      ## S %>>% 0  ->  S \psd 0
      return(list(latex = paste0(lhs$latex, " \\psd 0"), prec = .LATEX_PREC$COMPARE))
    }
    ## S %>>% T  ->  S \psd T
    rhs <- .to_latex_prec(rhs_expr, names_map)
    return(list(latex = paste0(lhs$latex, " \\psd ", rhs$latex), prec = .LATEX_PREC$COMPARE))
  }

  ## Fallback: just expr \psd 0
  inner <- .to_latex_prec(arg, names_map)
  list(
    latex = paste0(inner$latex, " \\psd 0"),
    prec = .LATEX_PREC$COMPARE
  )
}

## -- SOC (||x||_2 <= t) --
method(.to_latex_prec, SOC) <- function(x, names_map = NULL, ...) {
  ## args: [X, t]
  X_ltx <- .to_latex_prec(x@args[[1L]], names_map)
  t_ltx <- .to_latex_prec(x@args[[2L]], names_map)
  list(
    latex = paste0("\\cvxnorm{", X_ltx$latex, "}_2 \\leq ", t_ltx$latex),
    prec = .LATEX_PREC$COMPARE
  )
}

## -- ExpCone --
method(.to_latex_prec, ExpCone) <- function(x, names_map = NULL, ...) {
  x_ltx <- .to_latex_prec(x@args[[1L]], names_map)
  y_ltx <- .to_latex_prec(x@args[[2L]], names_map)
  z_ltx <- .to_latex_prec(x@args[[3L]], names_map)
  list(
    latex = paste0("\\left(", x_ltx$latex, ", ", y_ltx$latex, ", ",
                   z_ltx$latex, "\\right) \\in \\Kexp"),
    prec = .LATEX_PREC$COMPARE
  )
}

## -- PowCone3D --
method(.to_latex_prec, PowCone3D) <- function(x, names_map = NULL, ...) {
  x_ltx <- .to_latex_prec(x@args[[1L]], names_map)
  y_ltx <- .to_latex_prec(x@args[[2L]], names_map)
  z_ltx <- .to_latex_prec(x@args[[3L]], names_map)
  alpha <- if (length(x@alpha) == 1L) .format_numeric(x@alpha) else "\\alpha"
  list(
    latex = paste0("\\left(", x_ltx$latex, ", ", y_ltx$latex, ", ",
                   z_ltx$latex, "\\right) \\in \\Kpow{", alpha, "}"),
    prec = .LATEX_PREC$COMPARE
  )
}

## -- FiniteSet --
method(.to_latex_prec, FiniteSet) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  ## x@vec is a Constant (Reshape); extract its numeric value
  vec_val <- value(x@vec)
  vals_str <- paste(vapply(as.numeric(vec_val), .format_numeric, character(1)), collapse = ", ")
  list(
    latex = paste0(inner$latex, " \\in \\left\\{", vals_str, "\\right\\}"),
    prec = .LATEX_PREC$COMPARE
  )
}


# ==========================================================================
# Objective methods
# ==========================================================================

method(.to_latex_prec, Minimize) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  list(latex = inner$latex, prec = inner$prec)
}

method(.to_latex_prec, Maximize) <- function(x, names_map = NULL, ...) {
  inner <- .to_latex_prec(x@args[[1L]], names_map)
  list(latex = inner$latex, prec = inner$prec)
}


# ==========================================================================
# Top-level to_latex() methods (user-facing)
# ==========================================================================

## -- Expression (generic fallback) --
method(to_latex, Expression) <- function(x, ...) {
  .to_latex_prec(x, names_map = NULL)$latex
}

## -- Constraint --
method(to_latex, Constraint) <- function(x, ...) {
  .to_latex_prec(x, names_map = NULL)$latex
}

## -- Objective --
method(to_latex, Objective) <- function(x, ...) {
  sense <- if (S7_inherits(x, Minimize)) "\\text{minimize}" else "\\text{maximize}"
  inner <- .to_latex_prec(x@args[[1L]], names_map = NULL)$latex
  paste0(sense, " \\quad ", inner)
}

## -- Problem (optidef environment) --
method(to_latex, Problem) <- function(x, ...) {
  ## Build collision-safe name table
  names_map <- .build_latex_names(x)

  ## Objective
  obj <- x@objective
  sense <- if (S7_inherits(obj, Minimize)) "mini" else "maxi"
  obj_latex <- .to_latex_prec(obj@args[[1L]], names_map)$latex

  ## Variables for underset
  vars <- variables(x)
  var_names <- vapply(vars, function(v) {
    id_key <- as.character(v@id)
    if (exists(id_key, envir = names_map, inherits = FALSE)) {
      get(id_key, envir = names_map)
    } else {
      .name_to_latex(expr_name(v))
    }
  }, character(1))
  var_str <- paste(var_names, collapse = ", ")

  ## Constraints
  constrs <- x@constraints
  if (length(constrs) == 0L) {
    ## Unconstrained
    lines <- c(
      sprintf("\\begin{%s*}{%s}{%s}{}{}", sense, var_str, obj_latex),
      sprintf("\\end{%s*}", sense)
    )
    return(paste(lines, collapse = "\n"))
  }

  constr_lines <- vapply(constrs, function(c) {
    .to_latex_prec(c, names_map)$latex
  }, character(1))

  ## Build optidef environment
  ## optidef format: \addConstraint{LHS}{relation RHS}
  ## We split on the relation symbol
  lines <- character(0)
  lines <- c(lines, sprintf("\\begin{%s*}{%s}{%s}{}{}", sense, var_str, obj_latex))
  for (cl in constr_lines) {
    lines <- c(lines, sprintf("  \\addConstraint{%s}{}", cl))
  }
  lines <- c(lines, sprintf("\\end{%s*}", sense))

  paste(lines, collapse = "\n")
}

Try the CVXR package in your browser

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

CVXR documentation built on March 6, 2026, 9:10 a.m.