From 8a2e018b27f2e78e19bcf8d6d5b0a935d771728c Mon Sep 17 00:00:00 2001 From: R script Date: Fri, 19 Jun 2026 17:57:31 +0100 Subject: [PATCH 1/5] TipInstability perf improvement --- DESCRIPTION | 3 +- NAMESPACE | 8 +- NEWS.md | 6 ++ R/stability.R | 72 +++++++-------- benchmark/ab_bench_final.R | 56 ++++++++++++ benchmark/vtune-driver.R | 19 ++++ man/RogueTaxa.Rd | 4 +- man/TipInstability.Rd | 4 +- src/Rogue_init.c | 2 + src/geodesic.h | 28 ++++++ src/graph_geodesic.c | 1 + src/tip_instability.cpp | 180 +++++++++++++++++++++++++++++++++++++ 12 files changed, 335 insertions(+), 48 deletions(-) create mode 100644 benchmark/ab_bench_final.R create mode 100644 benchmark/vtune-driver.R create mode 100644 src/geodesic.h create mode 100644 src/tip_instability.cpp diff --git a/DESCRIPTION b/DESCRIPTION index 82b0122..98bf269 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -38,7 +38,6 @@ Imports: grDevices, matrixStats, Rdpack (>= 0.7), - Rfast, stats, TreeDist (> 2.2.0), TreeTools (>= 1.9.1.9003), @@ -58,7 +57,7 @@ Config/Needs/memcheck: devtools Config/Needs/metadata: codemetar Config/Needs/website: pkgdown RdMacros: Rdpack -SystemRequirements: C99 +SystemRequirements: C++17 ByteCompile: true Encoding: UTF-8 Language: en-GB diff --git a/NAMESPACE b/NAMESPACE index 9a7b06e..71db520 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -9,10 +9,6 @@ export(RogueTaxa) export(TipInstability) export(TipVolatility) importFrom(Rdpack,reprompt) -importFrom(Rfast,rowMads) -importFrom(Rfast,rowMedians) -importFrom(Rfast,rowVars) -importFrom(Rfast,rowmeans) importFrom(TreeDist,ClusteringInfo) importFrom(TreeDist,ConsensusInfo) importFrom(TreeDist,PhylogeneticInfoDistance) @@ -35,6 +31,10 @@ importFrom(cli,cli_progress_update) importFrom(fastmatch,"%fin%") importFrom(fastmatch,fmatch) importFrom(grDevices,hcl.colors) +importFrom(matrixStats,rowMads) +importFrom(matrixStats,rowMeans2) +importFrom(matrixStats,rowMedians) +importFrom(matrixStats,rowSds) importFrom(stats,cmdscale) importFrom(stats,setNames) importFrom(utils,capture.output) diff --git a/NEWS.md b/NEWS.md index 235821d..233493a 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,9 @@ +# Rogue v2.2.0.9000 (development) + +- Improve performance of `TipInstability()`. +- Remove `Rfast` dependency. + + # Rogue v2.2.0 (2026-03-20) ## Performance diff --git a/R/stability.R b/R/stability.R index 2fa6629..d834057 100644 --- a/R/stability.R +++ b/R/stability.R @@ -79,8 +79,8 @@ Cophenetic <- function(x, nTip = length(x$tip.label), log = FALSE, #' calculate leaf stability. #' @param checkTips Logical specifying whether to check that tips are numbered #' consistently. -#' @param parallel Logical specifying whether parallel execution should take -#' place in C++. +#' @param parallel Logical, retained for backwards compatibility; the fused +#' C implementation is single-threaded, so this argument is now ignored. #' @references #' \insertAllCited{} #' @examples @@ -106,7 +106,7 @@ Cophenetic <- function(x, nTip = length(x$tip.label), log = FALSE, #' plot(ConsensusWithout(trees, names(instab[instab > 0.2]))) #' @template MRS #' @family tip instability functions -#' @importFrom Rfast rowmeans rowMads rowMedians rowVars +#' @importFrom matrixStats rowSds rowMads rowMeans2 rowMedians #' @export TipInstability <- function(trees, log = TRUE, average = "mean", deviation = "sd", @@ -131,17 +131,25 @@ TipInstability <- function(trees, log = TRUE, average = "mean", nTip <- NTip(trees[[1]]) } - lt_idx <- which(lower.tri(matrix(TRUE, nTip, nTip))) + whichDev <- pmatch(tolower(deviation), c("sd", "mad")) + if (is.na(whichDev)) { + stop("`deviation` must be 'sd' or 'mad'") + } + whichAve <- pmatch(tolower(average), c("mean", "median")) + if (is.na(whichAve)) { + stop("`average` must be 'mean' or 'median'") + } nEdge <- nrow(trees[[1]]$edge) nNode <- trees[[1]]$Nnode - # Batch C path requires uniform tree dimensions and log = TRUE + # Fused C path requires uniform tree dimensions and log = TRUE useBatch <- isTRUE(log) && all(vapply(trees, function(tr) nrow(tr$edge) == nEdge, logical(1))) if (useBatch) { - # Batch C call: returns lower-triangle entries directly (nPairs × nTree) - # Ensure preorder (the per-tree GraphGeodesic path does this internally) + # A single C call computes the geodesics and reduces them to a per-leaf + # instability score, never materializing the nPairs × nTree distance + # matrix (nor the symmetric deviation matrix) as an R object. trees <- lapply(trees, Preorder) parent_all <- integer(nEdge * length(trees)) child_all <- integer(nEdge * length(trees)) @@ -150,39 +158,30 @@ TipInstability <- function(trees, log = TRUE, average = "mean", parent_all[rng] <- trees[[k]]$edge[, 1] - 1L child_all[rng] <- trees[[k]]$edge[, 2] - 1L } - dists_lt <- matrix( - .Call(`LOG_GRAPH_GEODESIC_MULTI`, - n_tip = as.integer(nTip), - n_node = as.integer(nNode), - parent = as.integer(parent_all), - child = as.integer(child_all), - n_edge = as.integer(nEdge), - n_tree = as.integer(length(trees))), - ncol = length(trees) - ) - } else { - dists <- vapply(trees, GraphGeodesic, double(nTip * nTip), - nTip = nTip, log = log, asMatrix = FALSE) - dists_lt <- dists[lt_idx, , drop = FALSE] + score <- .Call(`TIP_INSTABILITY`, + n_tip = as.integer(nTip), + n_node = as.integer(nNode), + parent = as.integer(parent_all), + child = as.integer(child_all), + n_edge = as.integer(nEdge), + n_tree = as.integer(length(trees)), + which_ave = as.integer(whichAve), + which_dev = as.integer(whichDev)) + return(setNames(score, TipLabels(trees[[1]]))) } - # Auto-enable OpenMP parallelism for Rfast operations on large matrices - use_parallel <- parallel || nrow(dists_lt) > 1000L + # Fallback (log = FALSE, or trees of differing resolution): reduce in R. + lt_idx <- which(lower.tri(matrix(TRUE, nTip, nTip))) + dists <- vapply(trees, GraphGeodesic, double(nTip * nTip), + nTip = nTip, log = log, asMatrix = FALSE) + dists_lt <- dists[lt_idx, , drop = FALSE] - whichDev <- pmatch(tolower(deviation), c("sd", "mad")) - if (is.na(whichDev)) { - stop("`deviation` must be 'sd' or 'mad'") - } devs_lt <- switch(whichDev, - rowVars(dists_lt, std = TRUE, parallel = use_parallel), - rowMads(dists_lt, parallel = use_parallel)) + rowSds(dists_lt), + rowMads(dists_lt)) devs_lt[is.nan(devs_lt)] <- 0 - whichAve <- pmatch(tolower(average), c("mean", "median")) - if (is.na(whichAve)) { - stop("`average` must be 'mean' or 'median'") - } - aves_lt <- switch(whichAve, rowmeans, Rfast::rowMedians)(dists_lt) + aves_lt <- switch(whichAve, rowMeans2(dists_lt), rowMedians(dists_lt)) meanAve <- mean(aves_lt) # Reconstruct symmetric deviation matrix from lower triangle @@ -190,10 +189,7 @@ TipInstability <- function(trees, log = TRUE, average = "mean", devs[lt_idx] <- devs_lt devs <- devs + t(devs) - setNames( - Rfast::rowmeans(devs) / meanAve, # faster than Rfast::colmeans - TipLabels(trees[[1]]) - ) + setNames(rowMeans2(devs) / meanAve, TipLabels(trees[[1]])) } #' `ColByStability()` returns a colour reflecting the instability of each leaf. diff --git a/benchmark/ab_bench_final.R b/benchmark/ab_bench_final.R new file mode 100644 index 0000000..512b0d2 --- /dev/null +++ b/benchmark/ab_bench_final.R @@ -0,0 +1,56 @@ +.libPaths(c("C:/Users/pjjg18/GitHub/Rogue/.ab_dev", + "C:/Users/pjjg18/GitHub/Rogue/.ab_ref", + .libPaths())) +library(RogueRef) +library(RogueDev) +library(TreeTools) +library(bench) + +trees150 <- ape::read.tree(system.file("example/150.bs", package="RogueRef", + lib.loc="C:/Users/pjjg18/GitHub/Rogue/.ab_ref")) +trees_med <- trees150[1:100] +trees_small <- AddTipEverywhere(BalancedTree(8), "Rogue") + +# Warmup (includes OpenMP thread pool init) +invisible(RogueRef::TipInstability(trees_small)) +invisible(RogueDev::TipInstability(trees_small)) +invisible(RogueRef::QuickRogue(trees_small, info="phylogenetic")) +invisible(RogueDev::QuickRogue(trees_small, info="phylogenetic")) + +cat("=== Canary: GraphGeodesic (single tree, should be neutral) ===\n") +b <- bench::mark( + ref = RogueRef::GraphGeodesic(trees_small[[1]], log=TRUE), + dev = RogueDev::GraphGeodesic(trees_small[[1]], log=TRUE), + min_iterations=50, check=FALSE) +print(b[, c("expression","min","median","n_itr")]) + +cat("\n=== TipInstability (100 trees x 150 tips) ===\n") +b <- bench::mark( + ref = RogueRef::TipInstability(trees_med, log=TRUE, average="median", deviation="mad"), + dev = RogueDev::TipInstability(trees_med, log=TRUE, average="median", deviation="mad"), + min_iterations=5, check=FALSE) +print(b[, c("expression","min","median","n_itr","mem_alloc")]) + +cat("\n=== QuickRogue (100 trees x 150 tips) ===\n") +b <- bench::mark( + ref = RogueRef::QuickRogue(trees_med, info="phylogenetic"), + dev = RogueDev::QuickRogue(trees_med, info="phylogenetic"), + min_iterations=3, check=FALSE) +print(b[, c("expression","min","median","n_itr","mem_alloc")]) + +cat("\n=== RogueTaxa fspic (100 trees x 150 tips) ===\n") +b <- bench::mark( + ref = RogueRef::RogueTaxa(trees_med, info="fspic"), + dev = RogueDev::RogueTaxa(trees_med, info="fspic"), + min_iterations=3, check=FALSE) +print(b[, c("expression","min","median","n_itr","mem_alloc")]) + +# Verify correctness +cat("\n=== Correctness check ===\n") +r_ref <- RogueRef::QuickRogue(trees_med, info="phylogenetic") +r_dev <- RogueDev::QuickRogue(trees_med, info="phylogenetic") +cat("Results match:", identical(r_ref, r_dev), "\n") + +ti_ref <- RogueRef::TipInstability(trees_med, log=TRUE, average="median", deviation="mad") +ti_dev <- RogueDev::TipInstability(trees_med, log=TRUE, average="median", deviation="mad") +cat("TipInstability match:", all.equal(ti_ref, ti_dev), "\n") diff --git a/benchmark/vtune-driver.R b/benchmark/vtune-driver.R new file mode 100644 index 0000000..4692609 --- /dev/null +++ b/benchmark/vtune-driver.R @@ -0,0 +1,19 @@ +# VTune driver: exercise the C hot paths in TipInstability +library(Rogue, lib.loc = ".vtune-lib") +library(TreeTools) + +trees150 <- ape::read.tree(system.file("example/150.bs", package = "Rogue", + lib.loc = ".vtune-lib")) +trees_med <- trees150[1:100] + +# Warmup +invisible(TipInstability(trees_med, log = TRUE, average = "median", + deviation = "mad")) + +# Repeat enough times for meaningful VTune sampling (~30s target) +cat("Starting VTune workload...\n") +for (i in seq_len(50)) { + TipInstability(trees_med, log = TRUE, average = "median", + deviation = "mad", checkTips = FALSE) +} +cat("Done.\n") diff --git a/man/RogueTaxa.Rd b/man/RogueTaxa.Rd index 8190601..159ec67 100644 --- a/man/RogueTaxa.Rd +++ b/man/RogueTaxa.Rd @@ -99,8 +99,8 @@ calculate leaf stability.} \item{fullSeq}{Logical specifying whether to list all taxa (\code{TRUE}), or only those that improve information content when all are dropped (\code{FALSE}).} -\item{parallel}{Logical specifying whether parallel execution should take -place in C++.} +\item{parallel}{Logical, retained for backwards compatibility; the fused +C implementation is single-threaded, so this argument is now ignored.} \item{.prepared}{Logical; if \code{TRUE}, skip internal tree preparation. For internal use.} diff --git a/man/TipInstability.Rd b/man/TipInstability.Rd index 8bdf39f..491a0c4 100644 --- a/man/TipInstability.Rd +++ b/man/TipInstability.Rd @@ -37,8 +37,8 @@ calculate leaf stability.} \item{checkTips}{Logical specifying whether to check that tips are numbered consistently.} -\item{parallel}{Logical specifying whether parallel execution should take -place in C++.} +\item{parallel}{Logical, retained for backwards compatibility; the fused +C implementation is single-threaded, so this argument is now ignored.} \item{pal}{A vector listing a sequence of colours to be used for plotting. The earliest entries will be assigned to the most stable tips.} diff --git a/src/Rogue_init.c b/src/Rogue_init.c index 06e1c61..f043da2 100644 --- a/src/Rogue_init.c +++ b/src/Rogue_init.c @@ -10,11 +10,13 @@ extern SEXP RogueNaRok(SEXP, SEXP, SEXP, SEXP, SEXP, extern SEXP GRAPH_GEODESIC(SEXP, SEXP, SEXP, SEXP, SEXP); extern SEXP LOG_GRAPH_GEODESIC(SEXP, SEXP, SEXP, SEXP, SEXP); extern SEXP LOG_GRAPH_GEODESIC_MULTI(SEXP, SEXP, SEXP, SEXP, SEXP, SEXP); +extern SEXP TIP_INSTABILITY(SEXP, SEXP, SEXP, SEXP, SEXP, SEXP, SEXP, SEXP); static const R_CallMethodDef callMethods[] = { {"GRAPH_GEODESIC", (DL_FUNC) &GRAPH_GEODESIC, 5}, {"LOG_GRAPH_GEODESIC", (DL_FUNC) &LOG_GRAPH_GEODESIC, 5}, {"LOG_GRAPH_GEODESIC_MULTI", (DL_FUNC) &LOG_GRAPH_GEODESIC_MULTI, 6}, + {"TIP_INSTABILITY", (DL_FUNC) &TIP_INSTABILITY, 8}, {"RogueNaRok", (DL_FUNC) &RogueNaRok, 10}, {NULL, NULL, 0} }; diff --git a/src/geodesic.h b/src/geodesic.h new file mode 100644 index 0000000..fb8e7c4 --- /dev/null +++ b/src/geodesic.h @@ -0,0 +1,28 @@ +#ifndef ROGUE_GEODESIC_H +#define ROGUE_GEODESIC_H + +/* Shared declarations so the C++ translation unit (tip_instability.cpp) can + * reuse the geodesic worker and log lookup table defined in graph_geodesic.c. + * Wrapped in extern "C" when included from C++ so the symbols match the + * (unmangled) C definitions. */ + +#ifdef __cplusplus +extern "C" { +#endif + +/* Lookup table of natural logs, lg[k] == log(k); populated at load time by a + * constructor in graph_geodesic.c. */ +extern double lg[]; + +/* Fill `ret` (an all_nodes * all_nodes int matrix) with pairwise node graph + * geodesics for one tree. See graph_geodesic.c for the algorithm. */ +void graph_geodesic_phylo(const int *n_tip, const int *n_node, + const int *parent, const int *child, + const int *n_edge, const int *all_nodes, + int *ret); + +#ifdef __cplusplus +} +#endif + +#endif /* ROGUE_GEODESIC_H */ diff --git a/src/graph_geodesic.c b/src/graph_geodesic.c index 3f6edda..5d07246 100644 --- a/src/graph_geodesic.c +++ b/src/graph_geodesic.c @@ -1,6 +1,7 @@ #include #include #include +#include "geodesic.h" #define GEOD_MAX 32768 diff --git a/src/tip_instability.cpp b/src/tip_instability.cpp new file mode 100644 index 0000000..1960735 --- /dev/null +++ b/src/tip_instability.cpp @@ -0,0 +1,180 @@ +/* Fused tip-instability kernel. + * + * Replaces, for the batch path of R's TipInstability() (log = TRUE, uniform + * tree dimensions), the chain + * .Call(LOG_GRAPH_GEODESIC_MULTI) -> R matrix() -> Rfast row-stats + * -> symmetric matrix -> Rfast::rowmeans + * with a single call that returns the length-nTip instability vector directly. + * + * The nPairs x nTrees distance matrix is never materialised as an R object: + * - mean + sd : streamed per pair via Welford (O(nPairs) memory); + * - median/mad : held only in a transient C++ buffer, reduced with + * std::nth_element (no Rfast dependency). + * + * Numerics mirror the previous Rfast path exactly: sd uses the (n-1) divisor, + * mad uses the 1.4826 constant, medians use the mean-of-two-middle rule for + * even n, and non-finite deviations collapse to 0. + */ + +#include +#include +#include +#include +#include +#include "geodesic.h" + +/* Median of [first, last) via nth_element; reorders the range in place. + * Even n: mean of the two central order statistics (matches matrixStats / + * the Rfast path this replaces). */ +static double median_inplace(double *first, double *last) { + const std::ptrdiff_t n = last - first; + if (n <= 0) return 0.0; + const std::ptrdiff_t mid = n / 2; + std::nth_element(first, first + mid, last); + const double hi = first[mid]; + if (n & 1) { + return hi; + } + const double lo = *std::max_element(first, first + mid); + return 0.5 * (lo + hi); +} + +extern "C" SEXP TIP_INSTABILITY(SEXP n_tip, SEXP n_node, SEXP parent_all, + SEXP child_all, SEXP n_edge, SEXP n_tree, + SEXP which_ave, SEXP which_dev) { + const int nTip = INTEGER(n_tip)[0]; + const int nNode = INTEGER(n_node)[0]; + const int allNodes = nTip + nNode; + const int nEdge = INTEGER(n_edge)[0]; + const int nTree = INTEGER(n_tree)[0]; + const int whichAve = INTEGER(which_ave)[0]; /* 1 = mean, 2 = median */ + const int whichDev = INTEGER(which_dev)[0]; /* 1 = sd, 2 = mad */ + const R_xlen_t nPairs = (R_xlen_t)nTip * (nTip - 1) / 2; + const int *parAll = INTEGER(parent_all); + const int *chAll = INTEGER(child_all); + + /* Reused across trees by graph_geodesic_phylo(). */ + std::vector interim((size_t)allNodes * allNodes); + + /* Per-pair deviation and average, in column-major lower-triangle order + * (outer column j, inner row i > j) -- the same ordering R uses. */ + std::vector devs(nPairs), aves(nPairs); + + const bool needBuffer = (whichAve == 2 || whichDev == 2); + + if (!needBuffer) { + /* mean + sd: stream Welford accumulators, no nPairs x nTrees buffer. */ + std::vector mean(nPairs, 0.0), m2(nPairs, 0.0); + for (int t = 0; t < nTree; ++t) { + R_CheckUserInterrupt(); + const int *par = parAll + (R_xlen_t)t * nEdge; + const int *ch = chAll + (R_xlen_t)t * nEdge; + graph_geodesic_phylo(&nTip, &nNode, par, ch, &nEdge, &allNodes, + interim.data()); + const double inv = 1.0 / (t + 1); + R_xlen_t p = 0; + for (int j = 0; j < nTip - 1; ++j) { + const int colOffset = allNodes * j; + for (int i = j + 1; i < nTip; ++i, ++p) { + const double x = lg[interim[i + colOffset]]; + const double delta = x - mean[p]; + mean[p] += delta * inv; + m2[p] += delta * (x - mean[p]); + } + } + } + const double denom = nTree > 1 ? nTree - 1 : 1; + for (R_xlen_t p = 0; p < nPairs; ++p) { + aves[p] = mean[p]; + const double var = m2[p] / denom; + const double sd = var > 0 ? std::sqrt(var) : 0.0; + devs[p] = std::isfinite(sd) ? sd : 0.0; + } + } else { + /* median and/or mad: buffer distances (tree-major, contiguous writes), + * then reduce each pair with nth_element. */ + std::vector buf(nPairs * (R_xlen_t)nTree); + for (int t = 0; t < nTree; ++t) { + R_CheckUserInterrupt(); + const int *par = parAll + (R_xlen_t)t * nEdge; + const int *ch = chAll + (R_xlen_t)t * nEdge; + graph_geodesic_phylo(&nTip, &nNode, par, ch, &nEdge, &allNodes, + interim.data()); + double *col = &buf[(R_xlen_t)t * nPairs]; + R_xlen_t p = 0; + for (int j = 0; j < nTip - 1; ++j) { + const int colOffset = allNodes * j; + for (int i = j + 1; i < nTip; ++i, ++p) { + col[p] = lg[interim[i + colOffset]]; + } + } + } + + // Scratch buffers reused across pairs (no per-pair heap churn). + std::vector row(nTree), scratch(nTree), absDev(nTree); + const double denom = nTree > 1 ? nTree - 1 : 1; + const bool needSum = (whichAve == 1 || whichDev == 1); // mean or sd + const bool needMed = (whichAve == 2 || whichDev == 2); // median or mad + for (R_xlen_t p = 0; p < nPairs; ++p) { + for (int t = 0; t < nTree; ++t) { + row[t] = buf[(R_xlen_t)t * nPairs + p]; + } + + double mean = 0.0, med = 0.0; + if (needSum) { + double sum = 0.0; + for (int t = 0; t < nTree; ++t) sum += row[t]; + mean = sum / nTree; + } + if (needMed) { + // The MAD centre is the same row median used by average = "median", + // so compute it once and share it. + std::copy(row.begin(), row.end(), scratch.begin()); + med = median_inplace(scratch.data(), scratch.data() + nTree); + } + + aves[p] = (whichAve == 1) ? mean : med; + + double dev; + if (whichDev == 1) { + double ss = 0.0; + for (int t = 0; t < nTree; ++t) { + const double d = row[t] - mean; + ss += d * d; + } + const double var = ss / denom; + dev = var > 0 ? std::sqrt(var) : 0.0; + } else { + for (int t = 0; t < nTree; ++t) absDev[t] = std::fabs(row[t] - med); + dev = 1.4826 * median_inplace(absDev.data(), absDev.data() + nTree); + } + devs[p] = std::isfinite(dev) ? dev : 0.0; + } + } + + /* meanAve = mean deviation-average over all pairs (normalising constant). */ + double sumAve = 0.0; + for (R_xlen_t p = 0; p < nPairs; ++p) sumAve += aves[p]; + const double meanAve = sumAve / nPairs; + + /* Fold each pair's deviation onto both its leaves -- equivalent to + * rowMeans of the symmetric (zero-diagonal) deviation matrix, without + * building it. */ + SEXP result = PROTECT(allocVector(REALSXP, nTip)); + double *res = REAL(result); + for (int i = 0; i < nTip; ++i) res[i] = 0.0; + { + R_xlen_t p = 0; + for (int j = 0; j < nTip - 1; ++j) { + for (int i = j + 1; i < nTip; ++i, ++p) { + res[i] += devs[p]; + res[j] += devs[p]; + } + } + } + const double norm = 1.0 / ((double)nTip * meanAve); + for (int i = 0; i < nTip; ++i) res[i] *= norm; + + UNPROTECT(1); + return result; +} From 42fc67d65593d99a7385a1d4ed81a973efc9f603 Mon Sep 17 00:00:00 2001 From: R script Date: Fri, 19 Jun 2026 17:58:05 +0100 Subject: [PATCH 2/5] consumers --- NEWS.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 233493a..43f55de 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,7 @@ # Rogue v2.2.0.9000 (development) -- Improve performance of `TipInstability()`. +- Improve performance of `TipInstability()` (and thence `ColByStability()`, + `QuickRogue()`). - Remove `Rfast` dependency. From daa8312c0e69162441accea63999296c9a366183 Mon Sep 17 00:00:00 2001 From: R script Date: Fri, 19 Jun 2026 19:39:32 +0100 Subject: [PATCH 3/5] build: link tip_instability.o (fix macOS load failure) The previous commit shipped src/tip_instability.cpp and registered TIP_INSTABILITY in Rogue_init.c, but Makevars still listed only the C objects, so the new C++ object was compiled but never put on the link line. On macOS (-undefined dynamic_lookup) the .so links cleanly with the symbol unresolved, then dlopen fails at load: symbol not found in flat namespace '_TIP_INSTABILITY' Add tip_instability.o to OBJECTS and set CXX_STD = CXX17 (matches DESCRIPTION SystemRequirements). Local Windows builds passed only because they used the working-tree Makevars. Co-Authored-By: Claude Opus 4.8 --- src/Makevars | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Makevars b/src/Makevars index 38c0eee..6f72746 100644 --- a/src/Makevars +++ b/src/Makevars @@ -1,2 +1,3 @@ +CXX_STD = CXX17 SOURCES = graph_geodesic.c rnr/Array.c rnr/BitVector.c rnr/Dropset.c rnr/HashTable.c rnr/List.c rnr/Node.c rnr/ProfileElem.c rnr/RogueNaRok.c rnr/Tree.c rnr/common.c rnr/legacy.c rnr/newFunctions.c Rogue_init.c -OBJECTS = $(SOURCES:.c=.o) +OBJECTS = $(SOURCES:.c=.o) tip_instability.o From 53d8f9780831fb7cdceb91165eb9c5ebe49d0f29 Mon Sep 17 00:00:00 2001 From: R script Date: Fri, 19 Jun 2026 19:39:32 +0100 Subject: [PATCH 4/5] build: link tip_instability.o (fix macOS load failure) The previous commit shipped src/tip_instability.cpp and registered TIP_INSTABILITY in Rogue_init.c, but Makevars still listed only the C objects, so the new C++ object was compiled but never put on the link line. On macOS (-undefined dynamic_lookup) the .so links cleanly with the symbol unresolved, then dlopen fails at load: symbol not found in flat namespace '_TIP_INSTABILITY' Add tip_instability.o to OBJECTS and set CXX_STD = CXX17 (matches DESCRIPTION SystemRequirements). Local Windows builds passed only because they used the working-tree Makevars. Co-Authored-By: Claude Opus 4.8 --- src/Makevars | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Makevars b/src/Makevars index 38c0eee..49e1328 100644 --- a/src/Makevars +++ b/src/Makevars @@ -1,2 +1,5 @@ -SOURCES = graph_geodesic.c rnr/Array.c rnr/BitVector.c rnr/Dropset.c rnr/HashTable.c rnr/List.c rnr/Node.c rnr/ProfileElem.c rnr/RogueNaRok.c rnr/Tree.c rnr/common.c rnr/legacy.c rnr/newFunctions.c Rogue_init.c -OBJECTS = $(SOURCES:.c=.o) +CXX_STD = CXX17 +C_SOURCES = graph_geodesic.c rnr/Array.c rnr/BitVector.c rnr/Dropset.c rnr/HashTable.c rnr/List.c rnr/Node.c rnr/ProfileElem.c rnr/RogueNaRok.c rnr/Tree.c rnr/common.c rnr/legacy.c rnr/newFunctions.c Rogue_init.c +CXX_SOURCES = tip_instability.cpp +SOURCES = $(C_SOURCES) $(CXX_SOURCES) +OBJECTS = $(C_SOURCES:.c=.o) $(CXX_SOURCES:.cpp=.o) From d390c4b6c8cc56a6c787cce711052ffc73248f0e Mon Sep 17 00:00:00 2001 From: R script Date: Fri, 19 Jun 2026 19:55:28 +0100 Subject: [PATCH 5/5] fix(cpp): include STL headers before R headers (macOS libc++ clash) Rinternals.h defines `#define length(x) Rf_length(x)` globally. When is included after , that macro fires inside libc++'s __locale:792 (`length` method), producing errors: too many arguments provided to function-like macro invocation '__abi_tag__' attribute only applies to structs/variables/... Fix: include all C++ standard headers before any R headers. Co-Authored-By: Claude Sonnet 4.6 --- src/tip_instability.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tip_instability.cpp b/src/tip_instability.cpp index 1960735..4e746c5 100644 --- a/src/tip_instability.cpp +++ b/src/tip_instability.cpp @@ -16,11 +16,11 @@ * even n, and non-finite deviations collapse to 0. */ -#include -#include #include #include #include +#include +#include #include "geodesic.h" /* Median of [first, last) via nth_element; reorders the range in place.