Skip to content

Matrix part 2 - #79

Merged
t-kalinowski merged 13 commits into
t-kalinowski:mainfrom
mns-nordicals:matrix-part-2
Jan 13, 2026
Merged

Matrix part 2#79
t-kalinowski merged 13 commits into
t-kalinowski:mainfrom
mns-nordicals:matrix-part-2

Conversation

@mns-nordicals

@mns-nordicals mns-nordicals commented Jan 9, 2026

Copy link
Copy Markdown
Contributor

Second batch of functions. These functions enable us to do linear regression with the example in the original issue.

See the doc/solve... file to see some ekstra context on changes to r2f.R (in the bottom of the document.)


Matrix LAPACK Handlers: Implementation Walkthrough

This document explains the changes made to support solve(), chol(),
chol2inv(), and diag() along with the linear-model example.

1) Add dimension expression support

  • R/manifest.R: added min() and max() to the dimension expression
    evaluator so Fortran declarations can emit min(a, b) and max(a, b)
    instead of failing on those calls.

2) New BLAS/LAPACK emitters

  • R/r2f-matrix-blas.R: introduced LAPACK helpers and diag utilities:

    • lapack_solve() uses dgesv to solve A %*% x = B and respects
      destination inference while avoiding destructive overwrite of inputs.
    • lapack_inverse() uses dgetrf + dgetri for solve(A).
    • lapack_chol() uses dpotrf and zeroes the lower triangle to match
      base R chol() output.
    • lapack_chol2inv() uses dpotri and mirrors the upper triangle to
      produce a full symmetric inverse.
  • diag_extract() and diag_matrix() cover extraction and construction
    forms for diag().

    • diag_length_expr() computes min(nrow, ncol) while keeping the
      expression symbolic when needed.
    • zero_lower_triangle() mirrors base R output by clearing the lower
      triangle after dpotrf.
  • Updated blas_int() to safely deparse language inputs so expressions like
    int(min(a, b), kind=c_int) render correctly.

  • Note: LAPACK status (info) is not propagated yet, so singular or
    non-PD inputs will currently produce undefined numeric results instead of
    R errors. This will be revisited once we have a reliable mechanism to
    bubble Fortran failures back to R.

3) Destination inference for new handlers

  • R/r2f-matrix-infer.R: added inference helpers for:
    • solve() (vector, matrix, and inverse cases)
    • chol() and chol2inv()

This allows the assignment handler to pass a destination and avoid extra
allocations when possible.

4) Register new matrix handlers

  • R/r2f-matrix.R:
    • Added handlers for solve(), chol(), chol2inv(), and diag().
    • Implemented base-R-like argument rules and error behavior.
    • Ensured incompatible options (e.g., pivot=TRUE for chol) are rejected
      with clear messages.

5) Vector and scalar reshape behavior

  • R/r2f.R:
    • Added maybe_reshape_vector_matrix() and scalarize_matrix() so
      elementwise operations can handle vector vs 1x1 matrix and vector vs
      matrix with singleton dimensions.
      • 1x1 matrices are scalarized via scalarize_matrix() (they become a
        scalar designator like x(1, 1)), which keeps Fortran ranks consistent.
      • Vectors can be reshaped into 1xN or Nx1 matrices to match singleton
        dimensions when needed.
    • This removes rank mismatches in cases like diag(XtX_inv) * s2 when s2
      is a 1x1 matrix from crossprod(res) and diag() returns a vector.
    • Also passed hoist through [ lowering so temporary arrays created by
      BLAS/LAPACK helpers (e.g., crossprod(res) emitting a temp in syrk())
      are declared and in scope when immediately subscripted like
      crossprod(res)[1].
      This matters because Fortran cannot subscript arbitrary expressions, so
      hoisting ensures the temp has a name and valid storage before [...].

6) Tests added

  • tests/testthat/test-matrix-lapack.R:
    • solve() vector, matrix, and inverse.
    • chol() and chol2inv().
    • diag() for vector input, matrix extraction, and size-based construction.
    • Full linear-model example based on the provided code.

7) Commands executed

  • air format .
  • R -q -e 'devtools::test()'
  • R -q -e 'rcmdcheck::rcmdcheck(error_on = "warning")'

All tests and R CMD check completed without errors or warnings.

@mns-nordicals

Copy link
Copy Markdown
Contributor Author

@codex please review. Do not flag issues with not handling info from LAPACK calls - this is well known and left for later.

@codecov

codecov Bot commented Jan 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.73663% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.35%. Comparing base (cdf43db) to head (5cbad69).
⚠️ Report is 14 commits behind head on main.

Files with missing lines Patch % Lines
R/r2f-matrix-infer.R 92.03% 9 Missing ⚠️
R/r2f-matrix-blas.R 99.68% 1 Missing ⚠️
R/r2f.R 98.21% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #79      +/-   ##
==========================================
+ Coverage   88.11%   89.35%   +1.23%     
==========================================
  Files          22       22              
  Lines        3989     4442     +453     
==========================================
+ Hits         3515     3969     +454     
+ Misses        474      473       -1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ae93bbe263

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread R/r2f-matrix.R Outdated
  Add dest_infer support for diag() to eliminate unnecessary temporary
  matrix allocations. Previously, `out <- diag(x, nrow, ncol)` would
  allocate a temporary matrix, fill it, then copy to out. Now it writes
  directly to the destination.

  Also fix argument extraction to support both named and positional args:

  - diag(): was ignoring positional nrow/ncol args (e.g., `diag(x, 5, 7)`)
  - crossprod/tcrossprod: add support for named x/y args
  - forwardsolve/backsolve: add support for named l/r/x args
  - chol/chol2inv: add support for named x arg

  This ensures handlers and inference functions match R's function
  signatures and work correctly regardless of calling convention.
  Consolidate duplicate conformability checking code and add tests for
  inference NULL paths and LAPACK error handling.
@mns-nordicals

Copy link
Copy Markdown
Contributor Author

I created this script to help me get the test coverage up. Maybe you have a way to get the data from codecov, but I couldn't so I created this script and thought it might be useful to you too

library(httr2)
library(jsonlite)

get_codecov_report <- function(token = NULL, pull_id = NULL) {
  if(any(is.null(token) || is.null(pull_id))) stop("token and pull_id must be set")
  
  service <- "github"
  owner <- "t-kalinowski"
  repo <- "quickr"

  compare_url <- glue::glue("https://api.codecov.io/api/v2/{service}/{owner}/repos/{repo}/compare/")
  
  request(compare_url) |> 
    req_url_query(pullid = pull_id) |> 
    req_headers(Authorization = paste("Bearer", token)) |>
    req_perform() |>
    resp_body_json()
}

parse_report <- function(diff_report) {
  out <- list()

  out$commit_ids <- list(
    base_commit = diff_report$base_commit,
    head_commit = diff_report$head_commit
  )

  out$pr_stats <- list(
    base = list(
      lines_covered = diff_report$totals$base$hits,
      lines_not_covered = diff_report$totals$base$misses,
      pr_coverage = diff_report$totals$base$coverage
    ), 
    head = list(
      lines_covered = diff_report$totals$head$hits,
      lines_not_covered = diff_report$totals$head$misses,
      pr_coverage = diff_report$totals$head$coverage
    ),
    delta = list(
      lines_covered = diff_report$totals$head$hits - diff_report$totals$base$hits,
      lines_not_covered = diff_report$totals$head$misses - diff_report$totals$base$misses,
      pr_coverage = diff_report$totals$head$coverage - diff_report$totals$base$coverage
    )
  )


  files <- diff_report$files
  changed_files <- list()

  for(file in files) {
    if(!file$has_diff) next

    changed_file <- list()

    changed_file$base_and_head_file_names <- list(
      base_file_name = file$name$base,
      head_file_name = file$name$head
    )

    changed_file$file_stats <- list(
      base = list(
        lines_covered = file$totals$base$hits,
        lines_not_covered = file$totals$base$misses,
        pr_coverage = file$totals$base$coverage
      ), 
      head = list(
        lines_covered = file$totals$head$hits,
        lines_not_covered = file$totals$head$misses,
        pr_coverage = file$totals$head$coverage
      ),
      delta = list(
        lines_covered = file$totals$head$hits - file$totals$base$hits,
        lines_not_covered = file$totals$head$misses - file$totals$base$misses,
        pr_coverage = file$totals$head$coverage - file$totals$base$coverage
      )
    )

    lines <- file$lines
    changed_lines <- list()
    idx_line <- 0

    for(line in lines) {
      line_coverage <- line$coverage$head %||% 0

      if(line_coverage != 1) next

      idx_line <- idx_line + 1

      line_info <- list(
        uncoverd_code = line$value,
        line_number = line$number$head,
        is_new_code = line$added,
        diff_from_base = line$is_diff
      )

      changed_lines[[idx_line]] <- line_info
    }

    file_name <- file$name$head
    changed_files[[file_name]] <- changed_file
    changed_files[[file_name]]$changed_lines <- changed_lines

  }

  out$changed_files <- changed_files
  out
}

token <- Sys.getenv("CODECOV_QUICKR")
pull_id <- 79
output_file <- glue::glue("scratch/codecov/PR/{pull_id}/codecov_report.json")
dir.create(dirname(output_file), recursive = TRUE, showWarnings = FALSE)

get_codecov_report(token, pull_id) |> 
  parse_report() |> 
  toJSON(pretty = T, auto_unbox = T) |> 
  writeLines(output_file)

@mns-nordicals

mns-nordicals commented Jan 10, 2026

Copy link
Copy Markdown
Contributor Author

I kind of started it in the last commit, but I am thinking about making a rather big refactor off the matrix files. There are a lot of small if() branches that maybe can made more readable by instead implementing some assert_some_condition() functions.

I think the current PR is reasonable complete with a high code coverage, so maybe you want to review as is or wait for a refactor?

@mns-nordicals

Copy link
Copy Markdown
Contributor Author

@codex do a final review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 32b44c700c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread R/r2f.R
@mns-nordicals

Copy link
Copy Markdown
Contributor Author

Added to news.md and also an example to readme.md. I created a new example for RcppArmadillo that mimics the R code better and they are now about the same in performance.

I might be a good idea for you to render the readme so it is more consistent?

@t-kalinowski

Copy link
Copy Markdown
Owner

Thank you for the excellent PR! I'm will merge this as is, and then handle the tiny handful of nits/style-preferences I have in a subsequent PR. This is great!

@t-kalinowski
t-kalinowski merged commit 4377b23 into t-kalinowski:main Jan 13, 2026
8 checks passed
@mns-nordicals

mns-nordicals commented Jan 14, 2026

Copy link
Copy Markdown
Contributor Author

@t-kalinowski thanks.

Just for your information:
I just noticed that the README.md is out of sync with README.rmd - as you will see my lm example is now producing a lot of warnigs. I added s2 <- crossprod(resid)[1], which will not produce a warning in R, to the .rmd file but I guess i forgot to rebuild it. Also, I made some small last minute edits to a test, so air format . changes one line in a test.

Also, for some reason it seems that benchmarks (for me at leat) runs slower in a rmarkdown/knitr context compared to console/terminal - but maybe that is expected.

@mns-nordicals
mns-nordicals deleted the matrix-part-2 branch January 14, 2026 15:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants