R/auto_layout_helpers.R

Defines functions check_graph_pass_thru is_multigroup_qgraph has_intercept make_straight check_exclude m_from_beta y_from_beta x_from_beta qgraph_to_beta to_integer gcd_2 gcd_k layout_matrix_from_mxy check_pass_thru all_lines fix_mxy c_list_to_layout column_list lower_e fixed_beta

# Input:
# - A beta matrix
# - x: The x-variable(s)
# - y: The y-variable(s)
# - exclude: Variables to be excluded from the plot
# Output:
# - A beta-matrix with only x, y, and mediators,
#   in ("non-reduced") column echelon form
fixed_beta <- function(
                beta_matrix,
                x = NULL,
                y = NULL,
                exclude = NULL) {

  # Always remove exclude first, if any

  if (!is.null(exclude)) {
    check_exclude(beta_matrix = beta_matrix,
                  exclude = exclude)
    i <- match(exclude, colnames(beta_matrix))
    i <- i[!is.na(i)]
    if (length(i) > 0) {
      beta1 <- beta_matrix[-i, -i]
    } else {
      beta1 <- beta_matrix
    }
  } else {
    beta1 <- beta_matrix
  }

  # Drop orphan variable(s)

  i <- colSums(beta1)
  j <- rowSums(beta1)
  tmp <- (i == 0) & (j == 0)
  beta1 <- beta1[!tmp, !tmp]

  if (is.null(x)) {
    # Determine x automatically
    x <- x_from_beta(beta1)
    if (length(x) == 0) {
      stop("The model has no x-variable(s).")
    }
  }

  if (is.null(y)) {
    # Determine y automatically
    y <- y_from_beta(beta1)
    if (length(y) == 0) {
      stop("The model has no y-variable(s).")
    }
  }

  # Sanity checks

  bnames <- colnames(beta1)
  y <- intersect(y, bnames)
  x <- intersect(x, bnames)
  if (length(y) == 0) {
    stop("All y(s) not in the model.")
  }
  if (length(x) == 0) {
    stop("All x(s) not in the model.")
  }

  # Reorder the rows and columns
  i <- c(x, setdiff(bnames, c(x, y)), y)
  beta1 <- beta1[i, i]
  beta1 <- lower_e(beta1)
  beta1
}

# Input:
# - A beta matrix
# Output:
# - A matrix in ("non-reduced") column echelon form
lower_e <- function(m) {

  allnames <- colnames(m)
  m1 <- m
  x0 <- character(0)

  # Reorder the rows and columns
  # y variable(s) last
  a <- colSums(m1)
  y0 <- colnames(m1)[a == 0]
  i <- c(setdiff(allnames, y0), y0)
  m1 <- m1[i, i]

  # up0 <- all((m1[upper.tri(m1)]) == 0)
  while (length(x0) < ncol(m)) {
    j <- setdiff(allnames, c(x0, y0))
    if (length(j) == 0) break
    a <- rowSums(m1[j, j, drop = FALSE])
    x0 <- c(x0, names(a)[(a == 0)])
    i <- c(x0, setdiff(allnames, c(x0, y0)), y0)
    m1 <- m1[i, i, drop = FALSE]
  }
  m1
}

# Input:
# - A beta-matrix with only x, y, and mediators,
#   in ("non-reduced") column echelon form
# Output:
# - A list of character vectors of columns of variables
column_list <- function(beta_matrix) {
  out <- list()
  m <- beta_matrix
  i <- which(colSums(m) == 0)
  if (length(i) > 0) {
    out_last <- list(colnames(m)[i])
    m <- m[-i, -i]
  } else {
    out_last <- list()
  }
  while (nrow(m) > 0) {
    tmp <- rowSums(m)
    i <- which(tmp == 0)
    x <- colnames(m)[i]
    out <- c(out, list(x))
    m <- m[-i, -i, drop = FALSE]
  }
  out <- c(out, out_last)
  out
}

# Input:
# - A list of character vectors,
#   such as the output of column_list().
# Output:
# - A layout x-y matrix
c_list_to_layout <- function(
                      c_list,
                      v_pos = c("middle", "lower", "upper")
                    ) {
  v_pos <- match.arg(v_pos)
  f <- function(i) {
    xx <- c_list[[i]]
    v_i <- switch(v_pos,
                  upper = seq_along(xx) - 1,
                  lower = seq_along(xx) - length(xx),
                  middle = (seq_along(xx) - 1) - (length(xx) - 1) / 2)
    out <- cbind(i, v_i)
    rownames(out) <- xx
    colnames(out) <- c("x", "y")
    out
  }
  out <- sapply(
          seq_along(c_list),
          f,
          simplify = FALSE
        )
  out <- do.call(rbind,
                 out)
  attr(out, "v_pos") <- v_pos
  out
}

# Input:
# - A layout x-y matrix, such as the output of
#   c_list_to_layout().
# - A beta matrix. Required to identify
#   paths in the model.
# Output:
# - A modified layout x-y matrix
fix_mxy <- function(
              m,
              beta,
              v_preference = c("upper", "lower")
            ) {
  v_pos <- attr(m, "v_pos")
  v_preference <- switch(
                    v_pos,
                    lower = "lower",
                    upper = "upper",
                    middle = match.arg(v_preference))
  m_new <- m
  mnames <- rownames(m)
  for (i in unique(m_new[, "x", drop = TRUE])[-1]) {
    m_i <- mnames[m[, "x"] == i]
    x_i <- mnames[m[, "x"] < i]
    y_i <- mnames[m[, "x"] > i]
    tmp <- which(colnames(beta) %in% m_i)
    beta_tmp <- beta[-tmp, -tmp]
    x_beta <- beta_tmp[, x_i, drop = FALSE]
    y_beta <- beta_tmp[y_i, , drop = FALSE]
    x_i <- colnames(x_beta)[colSums(x_beta) > 0]
    y_i <- rownames(y_beta)[rowSums(y_beta) > 0]
    if ((length(x_i) == 0) ||
        (length(y_i) == 0)) {
      # No paths through m_i
      next
    }
    lines_i <- all_lines(
                  m = m_new,
                  from = x_i,
                  to = y_i,
                  beta = beta
                )
    # lines_i_to <- all_lines(
    #               m = m_new,
    #               from = x_i,
    #               to = m_i
    #             )
    # lines_i_from <- all_lines(
    #               m = m_new,
    #               from = m_i,
    #               to = y_i
    #             )
    k0 <- length(m_i)
    # m_i_lower and m_i_upper used
    # when v_pos = "middle"
    if (k0 > 1) {
      if (((k0 %% 2) == 0)) {
        # Even
        m_i_lower <- m_i[seq(1, k0 / 2)]
        m_i_upper <- m_i[seq(k0 / 2 + 1, length(m_i))]
      } else {
        # Odd
        if (v_preference == "upper") {
          m_i_lower <- m_i[seq(1, floor(k0 / 2))]
          m_i_upper <- m_i[seq(floor(k0 / 2) + 1, length(m_i))]
        } else {
          m_i_lower <- m_i[seq(1, ceiling(k0 / 2))]
          m_i_upper <- m_i[seq(ceiling(k0 / 2) + 1, length(m_i))]
        }
      }
    } else {
      if (v_preference == "upper") {
        m_i_lower <- character(0)
        m_i_upper <- m_i
      } else {
        m_i_lower <- m_i
        m_i_upper <- character(0)
      }
    }
    # delta_lower and delta_upper used only
    # if v_pos is "middle"
    delta_lower <- switch(
                  v_pos,
                  upper = 0.5,
                  lower = -0.5,
                  middle = -0.5
                )
    delta_upper <- switch(
                  v_pos,
                  upper = 0.5,
                  lower = -0.5,
                  middle = 0.5
                )
    delta <- switch(
                v_pos,
                upper = 0.5,
                lower = -0.5,
                middle = NA)
    if (v_pos == "middle") {
      for (m_ii in c("lower", "upper")) {
        m_ij <- switch(m_ii,
                       lower = m_i_lower,
                       upper = m_i_upper)
        if (length(m_ij) == 0) next
        chk <- check_pass_thru(
                        m_i = m_ij,
                        m = m_new,
                        lines_i = lines_i
                      )
        ok <- all(unlist(chk) != 0)
        delta_ij <- switch(m_ii,
                           lower = delta_lower,
                           upper = delta_upper)
        while (!ok) {
          m_new[m_ij, "y"] <- m_new[m_ij, "y"] + delta_ij
          chk <- check_pass_thru(
                          m_i = m_ij,
                          m = m_new,
                          lines_i = lines_i
                        )
          lines_i_to <- all_lines(
                        m = m_new,
                        from = x_i,
                        to = m_ij,
                        beta = beta
                      )
          chk_to <- lapply(
                          x_i,
                          check_pass_thru,
                          m = m_new,
                          lines_i = lines_i_to)
          # chk_from <- lapply(
          #                 y_i,
          #                 check_pass_thru,
          #                 m = m_new,
          #                 lines_i = lines_i_from)
          tmp <- unlist(c(chk, chk_to))
          tmp <- tmp[!is.na(tmp)]
          ok <- all(tmp != 0)
        }
      }
    } else {
      # v_pos is "lower" or "upper"
      chk <- check_pass_thru(
                      m_i = m_i,
                      m = m_new,
                      lines_i = lines_i
                    )
      ok <- all(unlist(chk) != 0)
      while (!ok) {
        m_new[m_i, "y"] <- m_new[m_i, "y"] + delta
        chk <- check_pass_thru(
                        m_i = m_i,
                        m = m_new,
                        lines_i = lines_i
                      )
        lines_i_to <- all_lines(
                      m = m_new,
                      from = x_i,
                      to = m_i,
                      beta = beta
                    )
        chk_to <- lapply(
                        x_i,
                        check_pass_thru,
                        m = m_new,
                        lines_i = lines_i_to)
        # chk_from <- lapply(
        #                 y_i,
        #                 check_pass_thru,
        #                 m = m_new,
        #                 lines_i = lines_i_from)
        tmp <- unlist(c(chk, chk_to))
        tmp <- tmp[!is.na(tmp)]
        ok <- all(tmp != 0)
      }
    }
  }
  m_new
}

# Input:
# - m: Layout x-y matrix
# - from: Lines from
# - to: Lines to
# - The beta matrix
# Output:
# - A list of equations
all_lines <- function(m,
                      from,
                      to,
                      beta) {
  out <- vector("list", length(from) * length(to))
  i <- 0
  if (missing(beta)) {
    beta <- matrix(1,
                   ncol = length(from),
                   nrow = length(to))
    colnames(beta) <- from
    rownames(beta) <- to
  }
  for (p1 in from) {
    for (p2 in to) {
      if ((p1 != p2) &&
          (beta[p2, p1] > 0)) {
        i <- i + 1
        a <- m[p1, "y"] - m[p2, "y"]
        b <- m[p2, "x"] - m[p1, "x"]
        c <- m[p1, "x"] * m[p2, "y"] -
            m[p2, "x"] * m[p1, "y"]
        tmp <- c(a = a, b = b, c = c)
        attr(tmp, "from") <- p1
        attr(tmp, "to") <- p2
        out[[i]] <- tmp
      }
    }
  }
  out <- out[seq_len(i)]
  out
}

# Input:
# - m_i: The names of the mediators
# - m: The layout matrix in x-y form
# - lines_i: The a, b, c for the lines that may pass through m_i
# Output:
#- A list of vectors. If 0, a mediator is on a line.
check_pass_thru <- function(
                      m_i,
                      m,
                      lines_i) {
  out <- vector("list", length(m_i))
  names(out) <- m_i
  for (mm in m_i) {
    chk <- sapply(
              lines_i,
              function(xx) {
                if (all(xx == 0)) {
                  return(-1)
                } else {
                  m_x <- m[mm, "x"]
                  m_y <- m[mm, "y"]
                  tmp <- xx["a"] * m_x +
                         xx["b"] * m_y +
                         xx["c"]
                  tmp <- round(tmp, 4)
                  if (tmp == 0) {
                    to <- attr(xx, "to")
                    from <- attr(xx, "from")
                    x_range <- range(m[c(from, to), "x"])
                    y_range <- range(m[c(from, to), "y"])
                    if (m_x <= max(x_range) && m_x >= min(x_range) &&
                        m_y <= max(y_range) && m_y >= min(y_range)) {
                      return(0)
                    } else {
                      return(1)
                    }
                  }
                  tmp
                }
              })
    in_to <- (mm == sapply(lines_i, attr, which = "to"))
    in_from <- (mm == sapply(lines_i, attr, which = "from"))
    chk[in_to] <- -1
    chk[in_from] <- -1
    out[[mm]] <- chk
  }
  out
}


# Convert a x-y matrix to a
# semPlot layout matrix
layout_matrix_from_mxy <- function(
                            m
                          ) {
  out0 <- m
  y <- out0[, "y", drop = TRUE]
  y <- to_integer(y)
  y <- y - max(y)
  y <- y * -1 + 1
  y <- tryCatch(y / gcd_k(y),
                error = function(e) y)
  out0[, "y"] <- y

  x <- out0[, "x", drop = TRUE]
  x <- to_integer(x)
  x <- x - min(x) + 1
  x <- tryCatch(x / gcd_k(x),
                error = function(e) x)
  out0[, "x"] <- x

  out0 <- out0[, c("y", "x")]
  out0 <- split(out0, rownames(out0))
  do.call(layout_matrix,
          out0)
}

gcd_k <- function(x) {
  for (i in seq_along(x[-1]) + 1) {
    if (i == 2) {
      out <- gcd_2(x[i], x[i])
    } else {
      out <- gcd_2(out, x[i])
    }
    out <- unname(out)
  }
  out
}

gcd_2 <- function(x, y) {
  # Based on https://stackoverflow.com/a/21504113/4085819
  r <- x %% y
  return(ifelse(r,
                Recall(y, r),
                y))
}

to_integer <- function(x) {
  ok <- isTRUE(all.equal(round(x), x))
  k <- 1
  x0 <- x * k
  while (!ok || k > 100) {
    k <- k + 1
    x0 <- x * k
    ok <- isTRUE(all.equal(round(x0), x0))
  }
  round(x0)
}

# Input:
# - A qgraph object
# Output:
# - A beta matrix
qgraph_to_beta <- function(object) {
  # TODO:
  # - What to do with loadings?
  # - What to do with intercepts?
  e <- object$Edgelist
  i_directed <- (e$directed & !e$bidirectional)
  i_from <- e$from[i_directed]
  i_to <- e$to[i_directed]
  ge <- object$graphAttributes$Edges
  gn <- object$graphAttributes$Nodes
  in_beta <- sort(unique(c(i_from, i_to)))
  node_names <- unlist(gn$names)[in_beta]
  p <- length(node_names)
  out <- matrix(0, p, p)
  colnames(out) <- rownames(out) <- node_names
  i_from <- node_names[i_from]
  i_to <- node_names[i_to]
  for (i in seq_along(i_from)) {
    out[i_to[i], i_from[i]] <- 1
  }
  out
}

# Input:
# - A beta matrix
# Output:
# - A character vector of "pure" x
x_from_beta <- function(
                beta_matrix
              ) {
  i <- rowSums(beta_matrix)
  x <- colnames(beta_matrix)[i == 0]
  x
}

# Input:
# - A beta matrix
# Output:
# - A character vector of "pure" y
y_from_beta <- function(
                  beta_matrix
                ) {
  i <- rowSums(beta_matrix)
  j <- colSums(beta_matrix)
  y <- colnames(beta_matrix)[(j == 0) & (i > 0)]
  y
}

# Input:
# - A beta matrix
# Output:
# - A character vector of mediators
m_from_beta <- function(
                  beta_matrix
                ) {
  i <- rowSums(beta_matrix)
  j <- colSums(beta_matrix)
  m <- colnames(beta_matrix)[(j > 0) & (i > 0)]
  m
}

# Input:
# - A beta matrix
# - A vector of variables to be dropped
check_exclude <- function(
                beta_matrix,
                exclude
              ) {
  m0 <- m_from_beta(beta_matrix)
  if (length(intersect(exclude, m0)) > 0) {
    stop("One or more variables in 'exclude' is/are mediators and should not be excluded.")
  }
}

# Input:
# - A qgraph object
# Output:
# - A qgraph object with all directed paths no curvature
make_straight <- function(object) {
  e2 <- object$Edgelist
  i1 <- !e2$bidirectional
  i2 <- (e2$from != e2$to)
  i <- i1 & i2
  if (any(i)) {
    object$graphAttributes$Edges$curve[i] <- 0
  }
  object
}

# Input:
# - A qgraph object
# Output:
# - Logical
has_intercept <- function(object) {
  "triangle" %in% object$graphAttributes$Nodes$shape
}

# Input:
# - A qgraph object
# Output:
# - Logical
is_multigroup_qgraph <- function(object) {
  all(sapply(object, \(x) inherits(x, "qgraph")))
}

# Input:
# - A qgraph object
# Output:
# - Whether any path passes through any node
check_graph_pass_thru <- function(object) {

  m <- qgraph_to_layoutxy(object)
  beta <- qgraph_to_beta(object)
  vnames <- colnames(beta)

  lines_i <- all_lines(
                m,
                from = vnames,
                to = vnames,
                beta = beta
              )
  chk <- lapply(
          vnames,
          check_pass_thru,
          m = m,
          lines_i = lines_i)
  chk <- unlist(chk)
  chk <- chk[!is.na(chk)]
  all(chk != 0)
}

Try the semptools package in your browser

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

semptools documentation built on Aug. 8, 2025, 6:22 p.m.