From 0a24c0a04df37e676d52726f9df64e75a553d52b Mon Sep 17 00:00:00 2001 From: Jason Cory Brunson Date: Sat, 28 Jun 2025 18:50:11 -0400 Subject: [PATCH 1/8] add param to control output class w/ persistence option - begins #38 --- DESCRIPTION | 5 +++-- NAMESPACE | 1 + NEWS.md | 3 +++ R/PHom.R | 12 ++++++++++++ R/cubical.R | 19 ++++++++++++++++--- R/vietoris_rips.R | 28 ++++++++++++++++++++++++---- man/cubical.Rd | 14 ++++++++++++-- man/ripserr.Rd | 4 ++-- man/vietoris_rips.Rd | 26 +++++++++++++++++++++++--- 9 files changed, 96 insertions(+), 16 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index e2e306d..960983e 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: ripserr Title: Calculate Persistent Homology with Ripser-Based Engines -Version: 1.0.0 +Version: 1.0.0.0001 Authors@R: c(person(given = "Raoul R.", family = "Wadhwa", @@ -79,7 +79,8 @@ Depends: R (>= 3.5.0) Imports: Rcpp (>= 1.0), stats (>= 3.0), - utils (>= 3.0) + utils (>= 3.0), + phutil Suggests: covr (>= 3.5), knitr (>= 1.29), diff --git a/NAMESPACE b/NAMESPACE index 65af230..2234648 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -25,6 +25,7 @@ export(vietoris_rips.matrix) export(vietoris_rips.numeric) export(vietoris_rips.ts) importFrom(Rcpp,sourceCpp) +importFrom(phutil,as_persistence) importFrom(utils,head) importFrom(utils,tail) useDynLib(ripserr) diff --git a/NEWS.md b/NEWS.md index c13005f..fb2be3e 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,6 @@ +The new `return_class` parameter allows the user to specify whether to output persistence data in the legacy `'PHom'` class or the `'persistence'` class from {phutil}. +It defaults to `'PHom'` but will switch to `'persistence'` when the `'PHom'` class is deprecated in a future version. + # ripserr 1.0.0 This major version replaces an outdated version of the Ripser C++ library with its current version. diff --git a/R/PHom.R b/R/PHom.R index f5c9d95..cf3cf73 100644 --- a/R/PHom.R +++ b/R/PHom.R @@ -128,6 +128,8 @@ valid_colval <- function(df, val, val_name) { #' # print feature details to confirm accuracy #' print.data.frame(df_phom) PHom <- function(x, dim_col = 1, birth_col = 2, death_col = 3) { + # deprecate_PHom() + ## basic parameter checks (column nums/names are valid, etc.) if (!is.data.frame(x)) { x <- as.data.frame(x) @@ -201,6 +203,8 @@ as.PHom <- function(x, dim_col = 1, birth_col = 2, death_col = 3) { #' # confirm that persistence data is NOT valid #' is.PHom(df) is.PHom <- function(x) { + # deprecate_PHom() + # use validate to implement checks return( validate_PHom(x = x, error = FALSE) @@ -307,3 +311,11 @@ tail.PHom <- function(x, ...) { x <- as.data.frame(x) tail(x, ...) } + +# deprecate_PHom <- function() { +# lifecycle::deprecate_soft( +# "1.0.0", +# I("'PHom' class"), +# with = I("'persistence' from the {phutil} package") +# ) +# } diff --git a/R/cubical.R b/R/cubical.R index 8c19373..41fa9c7 100644 --- a/R/cubical.R +++ b/R/cubical.R @@ -18,7 +18,7 @@ #' @param ... other relevant parameters #' @rdname cubical #' @export cubical -#' @return `PHom` object +#' @return `"PHom"` or `"persistence"` object #' @examples #' #' # 2-dim example @@ -47,9 +47,18 @@ cubical <- function(dataset, ...) { #' @param threshold maximum simplicial complex diameter to explore #' @param method either `"lj"` (for Link Join) or `"cp"` (for Compute Pairs); #' see Kaji et al. (2020) for details +#' @param return_class class of output object; either `"PHom"` (default; legacy) +#' or `"persistence"` (from the +#' **[phutil](https://cran.r-project.org/package=phutil)** package) #' @export cubical.array #' @export -cubical.array <- function(dataset, threshold = 9999, method = "lj", ...) { +cubical.array <- function( + dataset, + threshold = 9999, + method = "lj", + return_class = c("PHom", "persistence"), + ... +) { # ensure valid arguments passed validate_params_cub(threshold = threshold, method = method) @@ -103,7 +112,11 @@ cubical.array <- function(dataset, threshold = 9999, method = "lj", ...) { } # convert data frame to a PHom object - ans <- new_PHom(ans) + ans <- switch( + match.arg(return_class), + PHom = new_PHom(ans), + persistence = as_persistence(ans) + ) # return return(ans) diff --git a/R/vietoris_rips.R b/R/vietoris_rips.R index 2e46451..8933179 100644 --- a/R/vietoris_rips.R +++ b/R/vietoris_rips.R @@ -27,11 +27,12 @@ #' . Persistent homology of the resulting matrix is #' then calculated. #' +#' @importFrom phutil as_persistence #' @param dataset object on which to calculate persistent homology #' @param ... other relevant parameters #' @rdname vietoris_rips #' @export vietoris_rips -#' @return `PHom` object +#' @return `"PHom"` or `"persistence"` object #' @examples #' #' # create a 2-d point cloud of a circle (100 points) @@ -68,6 +69,9 @@ vietoris_rips.data.frame <- function(dataset, ...) { #' specified #' @param threshold maximum simplicial complex diameter to explore #' @param p prime field in which to calculate persistent homology +#' @param return_class class of output object; either `"PHom"` (default; legacy) +#' or `"persistence"` (from the +#' **[phutil](https://cran.r-project.org/package=phutil)** package) #' @rdname vietoris_rips #' @export vietoris_rips.matrix #' @export @@ -77,11 +81,18 @@ vietoris_rips.matrix <- function( threshold = -1, p = 2L, dim = NULL, + return_class = c("PHom", "persistence"), ... ) { # shortcut for special case (only 1 row should return empty PHom) - if (nrow(dataset) == 1L) return(new_PHom()) + if (nrow(dataset) == 1L) { + return(switch( + match.arg(return_class), + PHom = new_PHom(), + persistence = as_persistence(matrix(NA_real_, nrow = 0L, ncol = 3L)) + )) + } # handle `dim` if passed if (! is.null(dim)) { @@ -111,7 +122,11 @@ vietoris_rips.matrix <- function( ans <- ripser_cpp_dist(dataset, max_dim, threshold, 1., p) # coerce to 'PHom' class - ans <- new_PHom(ripser_ans_to_df(ans)) + ans <- switch( + match.arg(return_class), + PHom = new_PHom(ripser_ans_to_df(ans)), + persistence = as_persistence(ans) + ) # return return(ans) @@ -126,6 +141,7 @@ vietoris_rips.dist <- function( threshold = -1, p = 2L, dim = NULL, + return_class = c("PHom", "persistence"), ... ) { @@ -157,7 +173,11 @@ vietoris_rips.dist <- function( ans <- ripser_cpp_dist(dataset, max_dim, threshold, 1., p) # coerce to 'PHom' class - ans <- new_PHom(ripser_ans_to_df(ans)) + ans <- switch( + match.arg(return_class), + PHom = new_PHom(ripser_ans_to_df(ans)), + persistence = as_peprsistence(ans) + ) # return return(ans) diff --git a/man/cubical.Rd b/man/cubical.Rd index e9f5fb9..0ad0159 100644 --- a/man/cubical.Rd +++ b/man/cubical.Rd @@ -8,7 +8,13 @@ \usage{ cubical(dataset, ...) -\method{cubical}{array}(dataset, threshold = 9999, method = "lj", ...) +\method{cubical}{array}( + dataset, + threshold = 9999, + method = "lj", + return_class = c("PHom", "persistence"), + ... +) \method{cubical}{matrix}(dataset, ...) } @@ -21,9 +27,13 @@ cubical(dataset, ...) \item{method}{either \code{"lj"} (for Link Join) or \code{"cp"} (for Compute Pairs); see Kaji et al. (2020) \url{https://arxiv.org/abs/2005.12692} for details} + +\item{return_class}{class of output object; either \code{"PHom"} (default; legacy) +or \code{"persistence"} (from the +\strong{\href{https://cran.r-project.org/package=phutil}{phutil}} package)} } \value{ -\code{PHom} object +\code{"PHom"} or \code{"persistence"} object } \description{ This function is an R wrapper for the CubicalRipser C++ library to calculate diff --git a/man/ripserr.Rd b/man/ripserr.Rd index 4a73a2e..1800a82 100644 --- a/man/ripserr.Rd +++ b/man/ripserr.Rd @@ -22,7 +22,7 @@ Useful links: Authors: \itemize{ - \item Raoul Wadhwa \email{raoulwadhwa@gmail.com} (\href{https://orcid.org/0000-0003-0503-9580}{ORCID}) + \item Raoul R. Wadhwa \email{raoulwadhwa@gmail.com} (\href{https://orcid.org/0000-0003-0503-9580}{ORCID}) \item Matt Piekenbrock \email{matt.piekenbrock@gmail.com} \item Xinyi Zhang \email{ezhang0927@gmail.com} \item Alice Zhang \email{aliscezhang@gmail.com} (\href{https://orcid.org/0009-0001-3584-5869}{ORCID}) @@ -36,7 +36,7 @@ Other contributors: \item Aymeric Stamm \email{aymeric.stamm@cnrs.fr} (\href{https://orcid.org/0000-0002-8725-3654}{ORCID}) [contributor] \item Aidan Bryant \email{bryant.aidan15@gmail.com} [contributor] \item James Golabek \email{jamesgolabek@gmail.com} [contributor] - \item Jacob Scott (\href{https://orcid.org/0000-0003-2971-7673}{ORCID}) [laboratory director] + \item Jacob G. Scott (\href{https://orcid.org/0000-0003-2971-7673}{ORCID}) [laboratory director] \item Ulrich Bauer (Ripser; MIT license) [copyright holder, contributor] \item Takeki Sudo (Cubical Ripser; GPL-3 license) [copyright holder, contributor] \item Kazushi Ahara (Cubical Ripser; GPL-3 license) [copyright holder, contributor] diff --git a/man/vietoris_rips.Rd b/man/vietoris_rips.Rd index fed7ccb..d133026 100644 --- a/man/vietoris_rips.Rd +++ b/man/vietoris_rips.Rd @@ -14,9 +14,25 @@ vietoris_rips(dataset, ...) \method{vietoris_rips}{data.frame}(dataset, ...) -\method{vietoris_rips}{matrix}(dataset, max_dim = 1L, threshold = -1, p = 2L, dim = NULL, ...) +\method{vietoris_rips}{matrix}( + dataset, + max_dim = 1L, + threshold = -1, + p = 2L, + dim = NULL, + return_class = c("PHom", "persistence"), + ... +) -\method{vietoris_rips}{dist}(dataset, max_dim = 1L, threshold = -1, p = 2L, dim = NULL, ...) +\method{vietoris_rips}{dist}( + dataset, + max_dim = 1L, + threshold = -1, + p = 2L, + dim = NULL, + return_class = c("PHom", "persistence"), + ... +) \method{vietoris_rips}{numeric}( dataset, @@ -46,6 +62,10 @@ calculated} \item{dim}{deprecated; passed to \code{max_dim} or ignored if \code{max_dim} is specified} +\item{return_class}{class of output object; either \code{"PHom"} (default; legacy) +or \code{"persistence"} (from the +\strong{\href{https://cran.r-project.org/package=phutil}{phutil}} package)} + \item{data_dim}{desired end data dimension} \item{dim_lag}{time series lag factor between dimensions} @@ -55,7 +75,7 @@ specified} \item{method}{currently only allows \code{"qa"} (quasi-attractor method)} } \value{ -\code{PHom} object +\code{"PHom"} or \code{"persistence"} object } \description{ This function is an R wrapper for the Ripser C++ library to From 817310818dabf8fbe737675fbe40a62a0d620af4 Mon Sep 17 00:00:00 2001 From: Jason Cory Brunson Date: Wed, 20 Aug 2025 14:04:17 -0400 Subject: [PATCH 2/8] add news header + fix typo of as_persistence --- NEWS.md | 4 ++++ R/cubical.R | 2 +- R/vietoris_rips.R | 7 +++---- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/NEWS.md b/NEWS.md index fb2be3e..f18f2a0 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,7 @@ +# next version + +## output class + The new `return_class` parameter allows the user to specify whether to output persistence data in the legacy `'PHom'` class or the `'persistence'` class from {phutil}. It defaults to `'PHom'` but will switch to `'persistence'` when the `'PHom'` class is deprecated in a future version. diff --git a/R/cubical.R b/R/cubical.R index 41fa9c7..9f09aa2 100644 --- a/R/cubical.R +++ b/R/cubical.R @@ -138,4 +138,4 @@ cubical.matrix <- function(dataset, ...) { # return return(ans) -} \ No newline at end of file +} diff --git a/R/vietoris_rips.R b/R/vietoris_rips.R index 8933179..05c6272 100644 --- a/R/vietoris_rips.R +++ b/R/vietoris_rips.R @@ -63,6 +63,7 @@ vietoris_rips.data.frame <- function(dataset, ...) { return(ans) } +#' @rdname vietoris_rips #' @param max_dim maximum dimension of persistent homology features to be #' calculated #' @param dim deprecated; passed to `max_dim` or ignored if `max_dim` is @@ -72,7 +73,6 @@ vietoris_rips.data.frame <- function(dataset, ...) { #' @param return_class class of output object; either `"PHom"` (default; legacy) #' or `"persistence"` (from the #' **[phutil](https://cran.r-project.org/package=phutil)** package) -#' @rdname vietoris_rips #' @export vietoris_rips.matrix #' @export vietoris_rips.matrix <- function( @@ -176,19 +176,18 @@ vietoris_rips.dist <- function( ans <- switch( match.arg(return_class), PHom = new_PHom(ripser_ans_to_df(ans)), - persistence = as_peprsistence(ans) + persistence = as_persistence(ans) ) # return return(ans) } -#' @aliases vietoris_rips.numeric vietoris_rips.ts +#' @rdname vietoris_rips #' @param data_dim desired end data dimension #' @param dim_lag time series lag factor between dimensions #' @param sample_lag time series lag factor between samples (rows) #' @param method currently only allows `"qa"` (quasi-attractor method) -#' @rdname vietoris_rips #' @export vietoris_rips.numeric #' @export vietoris_rips.numeric <- function( From f5848a3ba7fbc74cbdefe7d242d29dee07c6d2ef Mon Sep 17 00:00:00 2001 From: Jason Cory Brunson Date: Wed, 20 Aug 2025 14:26:19 -0400 Subject: [PATCH 3/8] print example outputs + test class specifications --- R/cubical.R | 5 +++-- R/vietoris_rips.R | 2 +- tests/testthat/test-cubical-2dim.R | 12 +++++++++++- tests/testthat/test-cubical-3dim.R | 26 ++++++++++++++++++-------- tests/testthat/test-cubical-4dim.R | 26 ++++++++++++++++++-------- tests/testthat/test-vr.R | 18 ++++++++++++++++++ 6 files changed, 69 insertions(+), 20 deletions(-) diff --git a/R/cubical.R b/R/cubical.R index 9f09aa2..8715ea7 100644 --- a/R/cubical.R +++ b/R/cubical.R @@ -24,16 +24,17 @@ #' # 2-dim example #' dataset <- rnorm(10 ^ 2) #' dim(dataset) <- rep(10, 2) -#' cubical_hom2 <- cubical(dataset) +#' ( cubical_hom2 <- cubical(dataset) ) #' #' # 3-dim example #' dataset <- rnorm(8 ^ 3) #' dim(dataset) <- rep(8, 3) -#' cubical_hom3 <- cubical(dataset) +#' ( cubical_hom3 <- cubical(dataset) ) #' #' # 4-dim example #' dataset <- rnorm(5 ^ 4) #' dim(dataset) <- rep(5, 4) +#' ( cubical_hom4 <- cubical(dataset) ) # Notes: # - figure out format from `dataset` # - return_format will be "df" (opinionated) w/ additional "PHom" S3 class diff --git a/R/vietoris_rips.R b/R/vietoris_rips.R index 05c6272..cc7096b 100644 --- a/R/vietoris_rips.R +++ b/R/vietoris_rips.R @@ -41,7 +41,7 @@ #' pt.cloud <- cbind(cos(rand.angle), sin(rand.angle)) #' #' # calculate persistent homology (num.pts by 3 numeric matrix) -#' pers.hom <- vietoris_rips(pt.cloud) +#' ( pers.hom <- vietoris_rips(pt.cloud) ) # Notes: # - figure out format from `dataset` # - return_format will be "df" (opinionated) w/ additional "PHom" S3 class diff --git a/tests/testthat/test-cubical-2dim.R b/tests/testthat/test-cubical-2dim.R index 72cb044..b175d90 100644 --- a/tests/testthat/test-cubical-2dim.R +++ b/tests/testthat/test-cubical-2dim.R @@ -54,4 +54,14 @@ test_that("2-dim cubical returns same values as validated tests", { # check means of births and deaths to ensure close enough expect_equal(mean(test_output$birth), mean(output_data$birth), tolerance = 0.025) expect_equal(mean(test_output$death), mean(output_data$death), tolerance = 0.025) -}) \ No newline at end of file +}) + +test_that("specified class is returned", { + # calculate 'PHom' object + expect_s3_class(cubical(test_data, return_class = "PHom"), + "PHom") + + # calculate 'persistence' object + expect_s3_class(cubical(test_data, return_class = "persistence"), + "persistence") +}) diff --git a/tests/testthat/test-cubical-3dim.R b/tests/testthat/test-cubical-3dim.R index cd75677..7ab33b7 100644 --- a/tests/testthat/test-cubical-3dim.R +++ b/tests/testthat/test-cubical-3dim.R @@ -1,14 +1,14 @@ context("cubical 3-dim") library("ripserr") +# reproducibility +set.seed(42) + +# create data +test_data <- rnorm(10 ^ 3) +dim(test_data) <- rep(10, 3) + test_that("basic 3-dim cubical works", { - # reproducibility - set.seed(42) - - # create data - test_data <- rnorm(10 ^ 3) - dim(test_data) <- rep(10, 3) - # create cubical complex cub_comp <- ripserr::cubical(test_data) @@ -54,4 +54,14 @@ test_that("3-dim calculation returns same values as validated tests", { # check means of births and deaths to ensure close enough expect_equal(mean(test_output$birth), mean(output_data$birth)) expect_equal(mean(test_output$death), mean(output_data$death)) -}) \ No newline at end of file +}) + +test_that("specified class is returned", { + # calculate 'PHom' object + expect_s3_class(cubical(test_data, return_class = "PHom"), + "PHom") + + # calculate 'persistence' object + expect_s3_class(cubical(test_data, return_class = "persistence"), + "persistence") +}) diff --git a/tests/testthat/test-cubical-4dim.R b/tests/testthat/test-cubical-4dim.R index 568d368..58ecfee 100644 --- a/tests/testthat/test-cubical-4dim.R +++ b/tests/testthat/test-cubical-4dim.R @@ -1,14 +1,14 @@ context("cubical 4-dim") library("ripserr") +# reproducibility +set.seed(42) + +# create dataset to be used +tester <- rnorm(5 ^ 4) +dim(tester) <- rep(5, 4) + test_that("basic 4-dim cubical works", { - # reproducibility - set.seed(42) - - # create dataset to be used - tester <- rnorm(5 ^ 4) - dim(tester) <- rep(5, 4) - # calculate cubical complex for 4-dim voxel data cub_comp <- ripserr::cubical(tester) @@ -53,4 +53,14 @@ test_that("4-dim calculation returns same values as validated tests", { # check means of births and deaths to ensure close enough expect_equal(mean(test_output$birth), mean(output_data$birth), tolerance = 0.025) expect_equal(mean(test_output$death), mean(output_data$death), tolerance = 0.025) -}) \ No newline at end of file +}) + +test_that("specified class is returned", { + # calculate 'PHom' object + expect_s3_class(cubical(tester, return_class = "PHom"), + "PHom") + + # calculate 'persistence' object + expect_s3_class(cubical(tester, return_class = "persistence"), + "persistence") +}) diff --git a/tests/testthat/test-vr.R b/tests/testthat/test-vr.R index e920ab4..525d96b 100644 --- a/tests/testthat/test-vr.R +++ b/tests/testthat/test-vr.R @@ -69,3 +69,21 @@ test_that("consistency across generic methods for time series", { # compare persistent homology across classes expect_equal(num_phom, ts_phom) }) + +test_that("specified class is returned", { + # calculate 'PHom' object for each class + expect_s3_class(vietoris_rips(circle_mat, return_class = "PHom"), + "PHom") + expect_s3_class(vietoris_rips(circle_df, return_class = "PHom"), + "PHom") + expect_s3_class(vietoris_rips(circle_dist, return_class = "PHom"), + "PHom") + + # calculate 'persistence' object for each class + expect_s3_class(vietoris_rips(circle_mat, return_class = "persistence"), + "persistence") + expect_s3_class(vietoris_rips(circle_df, return_class = "persistence"), + "persistence") + expect_s3_class(vietoris_rips(circle_dist, return_class = "persistence"), + "persistence") +}) From 9ccc3e9602a1154098e027cb0ed4bd2e09a5d3b5 Mon Sep 17 00:00:00 2001 From: Jason Cory Brunson Date: Wed, 20 Aug 2025 14:41:16 -0400 Subject: [PATCH 4/8] pass metadata to persistence class coercer --- R/cubical.R | 7 ++++++- R/vietoris_rips.R | 21 ++++++++++++++++++--- man/cubical.Rd | 5 +++-- man/vietoris_rips.Rd | 2 +- 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/R/cubical.R b/R/cubical.R index 8715ea7..e4f6a10 100644 --- a/R/cubical.R +++ b/R/cubical.R @@ -116,7 +116,12 @@ cubical.array <- function( ans <- switch( match.arg(return_class), PHom = new_PHom(ans), - persistence = as_persistence(ans) + persistence = as_persistence( + ans, + engine = "ripserr::cubical", + filtration = "cubical", + parameters = list(threshold = threshold, method = method) + ) ) # return diff --git a/R/vietoris_rips.R b/R/vietoris_rips.R index cc7096b..63bb99c 100644 --- a/R/vietoris_rips.R +++ b/R/vietoris_rips.R @@ -90,7 +90,12 @@ vietoris_rips.matrix <- function( return(switch( match.arg(return_class), PHom = new_PHom(), - persistence = as_persistence(matrix(NA_real_, nrow = 0L, ncol = 3L)) + persistence = as_persistence( + matrix(NA_real_, nrow = 0L, ncol = 3L), + engine = "ripserr::vietoris_rips", + filtration = "Vietoris-Rips", + parameters = list(max_dim = max_dim, threshold = threshold, p = p) + ) )) } @@ -125,7 +130,12 @@ vietoris_rips.matrix <- function( ans <- switch( match.arg(return_class), PHom = new_PHom(ripser_ans_to_df(ans)), - persistence = as_persistence(ans) + persistence = as_persistence( + ans, + engine = "ripserr::vietoris_rips", + filtration = "Vietoris-Rips", + parameters = list(max_dim = max_dim, threshold = threshold, p = p) + ) ) # return @@ -176,7 +186,12 @@ vietoris_rips.dist <- function( ans <- switch( match.arg(return_class), PHom = new_PHom(ripser_ans_to_df(ans)), - persistence = as_persistence(ans) + persistence = as_persistence( + ans, + engine = "ripserr::vietoris_rips", + filtration = "Vietoris-Rips", + parameters = list(max_dim = max_dim, threshold = threshold, p = p) + ) ) # return diff --git a/man/cubical.Rd b/man/cubical.Rd index 0ad0159..95379f1 100644 --- a/man/cubical.Rd +++ b/man/cubical.Rd @@ -57,14 +57,15 @@ and persistent homology is then calculated using \code{cubical.array}. # 2-dim example dataset <- rnorm(10 ^ 2) dim(dataset) <- rep(10, 2) -cubical_hom2 <- cubical(dataset) +( cubical_hom2 <- cubical(dataset) ) # 3-dim example dataset <- rnorm(8 ^ 3) dim(dataset) <- rep(8, 3) -cubical_hom3 <- cubical(dataset) +( cubical_hom3 <- cubical(dataset) ) # 4-dim example dataset <- rnorm(5 ^ 4) dim(dataset) <- rep(5, 4) +( cubical_hom4 <- cubical(dataset) ) } diff --git a/man/vietoris_rips.Rd b/man/vietoris_rips.Rd index d133026..91302d1 100644 --- a/man/vietoris_rips.Rd +++ b/man/vietoris_rips.Rd @@ -111,5 +111,5 @@ rand.angle <- runif(num.pts, 0, 2*pi) pt.cloud <- cbind(cos(rand.angle), sin(rand.angle)) # calculate persistent homology (num.pts by 3 numeric matrix) -pers.hom <- vietoris_rips(pt.cloud) +( pers.hom <- vietoris_rips(pt.cloud) ) } From ae14d77ac29dc136f9b89eb5ba395c9514dae112 Mon Sep 17 00:00:00 2001 From: Jason Cory Brunson Date: Tue, 9 Sep 2025 20:44:11 -0400 Subject: [PATCH 5/8] don't import as_persistence + link to class documentation in phutil --- NAMESPACE | 1 - R/cubical.R | 4 ++-- R/vietoris_rips.R | 9 ++++----- man/cubical.Rd | 2 +- man/vietoris_rips.Rd | 2 +- 5 files changed, 8 insertions(+), 10 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index 2234648..65af230 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -25,7 +25,6 @@ export(vietoris_rips.matrix) export(vietoris_rips.numeric) export(vietoris_rips.ts) importFrom(Rcpp,sourceCpp) -importFrom(phutil,as_persistence) importFrom(utils,head) importFrom(utils,tail) useDynLib(ripserr) diff --git a/R/cubical.R b/R/cubical.R index e4f6a10..79e5691 100644 --- a/R/cubical.R +++ b/R/cubical.R @@ -18,7 +18,7 @@ #' @param ... other relevant parameters #' @rdname cubical #' @export cubical -#' @return `"PHom"` or `"persistence"` object +#' @return `"PHom"` or [`"persistence"`][phutil::as_persistence] object #' @examples #' #' # 2-dim example @@ -116,7 +116,7 @@ cubical.array <- function( ans <- switch( match.arg(return_class), PHom = new_PHom(ans), - persistence = as_persistence( + persistence = phutil::as_persistence( ans, engine = "ripserr::cubical", filtration = "cubical", diff --git a/R/vietoris_rips.R b/R/vietoris_rips.R index 63bb99c..327a406 100644 --- a/R/vietoris_rips.R +++ b/R/vietoris_rips.R @@ -27,12 +27,11 @@ #' . Persistent homology of the resulting matrix is #' then calculated. #' -#' @importFrom phutil as_persistence #' @param dataset object on which to calculate persistent homology #' @param ... other relevant parameters #' @rdname vietoris_rips #' @export vietoris_rips -#' @return `"PHom"` or `"persistence"` object +#' @return `"PHom"` or [`"persistence"`][phutil::as_persistence] object #' @examples #' #' # create a 2-d point cloud of a circle (100 points) @@ -90,7 +89,7 @@ vietoris_rips.matrix <- function( return(switch( match.arg(return_class), PHom = new_PHom(), - persistence = as_persistence( + persistence = phutil::as_persistence( matrix(NA_real_, nrow = 0L, ncol = 3L), engine = "ripserr::vietoris_rips", filtration = "Vietoris-Rips", @@ -130,7 +129,7 @@ vietoris_rips.matrix <- function( ans <- switch( match.arg(return_class), PHom = new_PHom(ripser_ans_to_df(ans)), - persistence = as_persistence( + persistence = phutil::as_persistence( ans, engine = "ripserr::vietoris_rips", filtration = "Vietoris-Rips", @@ -186,7 +185,7 @@ vietoris_rips.dist <- function( ans <- switch( match.arg(return_class), PHom = new_PHom(ripser_ans_to_df(ans)), - persistence = as_persistence( + persistence = phutil::as_persistence( ans, engine = "ripserr::vietoris_rips", filtration = "Vietoris-Rips", diff --git a/man/cubical.Rd b/man/cubical.Rd index 95379f1..d28cfb9 100644 --- a/man/cubical.Rd +++ b/man/cubical.Rd @@ -33,7 +33,7 @@ or \code{"persistence"} (from the \strong{\href{https://cran.r-project.org/package=phutil}{phutil}} package)} } \value{ -\code{"PHom"} or \code{"persistence"} object +\code{"PHom"} or \code{\link[phutil:persistence]{"persistence"}} object } \description{ This function is an R wrapper for the CubicalRipser C++ library to calculate diff --git a/man/vietoris_rips.Rd b/man/vietoris_rips.Rd index 91302d1..90b8706 100644 --- a/man/vietoris_rips.Rd +++ b/man/vietoris_rips.Rd @@ -75,7 +75,7 @@ or \code{"persistence"} (from the \item{method}{currently only allows \code{"qa"} (quasi-attractor method)} } \value{ -\code{"PHom"} or \code{"persistence"} object +\code{"PHom"} or \code{\link[phutil:persistence]{"persistence"}} object } \description{ This function is an R wrapper for the Ripser C++ library to From 31b346a9c51eac89cbabf283a4eb973a82d22890 Mon Sep 17 00:00:00 2001 From: Jason Cory Brunson Date: Tue, 9 Sep 2025 21:25:10 -0400 Subject: [PATCH 6/8] soft-deprecate PHom class --- DESCRIPTION | 3 ++- R/PHom.R | 38 +++++++++++++++++++++++++++----------- R/cubical.R | 10 +++++++++- R/vietoris_rips.R | 30 +++++++++++++++++++++++++++--- 4 files changed, 65 insertions(+), 16 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 960983e..6ed44a2 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -80,7 +80,8 @@ Imports: Rcpp (>= 1.0), stats (>= 3.0), utils (>= 3.0), - phutil + phutil, + lifecycle Suggests: covr (>= 3.5), knitr (>= 1.29), diff --git a/R/PHom.R b/R/PHom.R index 56bf7a6..855b39b 100644 --- a/R/PHom.R +++ b/R/PHom.R @@ -11,6 +11,13 @@ new_PHom <- function(x = data.frame(dimension = integer(0), dim_col = 1, b_col = 2, d_col = 3) { + lifecycle::deprecate_soft( + "1.1.0", + I("'PHom' class"), + with = I("'persistence' from the {phutil} package"), + id = "PHom" + ) + # parameter validity checked in PHom helper # construct df for PHom object @@ -128,7 +135,12 @@ valid_colval <- function(df, val, val_name) { #' # print feature details to confirm accuracy #' print.data.frame(df_phom) PHom <- function(x, dim_col = 1, birth_col = 2, death_col = 3) { - # deprecate_PHom() + lifecycle::deprecate_soft( + "1.1.0", + I("'PHom' class"), + with = I("'persistence' from the {phutil} package"), + id = "PHom" + ) ## basic parameter checks (column nums/names are valid, etc.) if (!is.data.frame(x)) { @@ -175,6 +187,13 @@ PHom <- function(x, dim_col = 1, birth_col = 2, death_col = 3) { #' # print feature details to confirm accuracy #' print.data.frame(df_phom) as.PHom <- function(x, dim_col = 1, birth_col = 2, death_col = 3) { + lifecycle::deprecate_soft( + "1.1.0", + I("'PHom' class"), + with = I("'persistence' from the {phutil} package"), + id = "PHom" + ) + x <- as.data.frame(x) return(PHom(x)) @@ -203,7 +222,12 @@ as.PHom <- function(x, dim_col = 1, birth_col = 2, death_col = 3) { #' # confirm that persistence data is NOT valid #' is.PHom(df) is.PHom <- function(x) { - # deprecate_PHom() + lifecycle::deprecate_soft( + "1.1.0", + I("'PHom' class"), + with = I("'persistence' from the {phutil} package"), + id = "PHom" + ) # use validate to implement checks return( @@ -257,7 +281,7 @@ print.PHom <- function(x, ...) { "; max = ", signif(max(x$death[is.finite(x$death)], na.rm = TRUE), digits = 5), - "."), + ".\n"), "") cat(paste(ans1, ans2, ans3, sep = "\n\n")) @@ -313,11 +337,3 @@ tail.PHom <- function(x, ...) { x <- as.data.frame(x) tail(x, ...) } - -# deprecate_PHom <- function() { -# lifecycle::deprecate_soft( -# "1.0.0", -# I("'PHom' class"), -# with = I("'persistence' from the {phutil} package") -# ) -# } diff --git a/R/cubical.R b/R/cubical.R index 79e5691..662f9eb 100644 --- a/R/cubical.R +++ b/R/cubical.R @@ -115,7 +115,15 @@ cubical.array <- function( # convert data frame to a PHom object ans <- switch( match.arg(return_class), - PHom = new_PHom(ans), + PHom = { + lifecycle::deprecate_soft( + "1.1.0", + I("'PHom' class"), + with = I("'persistence' from the {phutil} package"), + id = "PHom" + ) + new_PHom(ans) + }, persistence = phutil::as_persistence( ans, engine = "ripserr::cubical", diff --git a/R/vietoris_rips.R b/R/vietoris_rips.R index 327a406..57ff39c 100644 --- a/R/vietoris_rips.R +++ b/R/vietoris_rips.R @@ -88,7 +88,15 @@ vietoris_rips.matrix <- function( if (nrow(dataset) == 1L) { return(switch( match.arg(return_class), - PHom = new_PHom(), + PHom = { + lifecycle::deprecate_soft( + "1.1.0", + I("'PHom' class"), + with = I("'persistence' from the {phutil} package"), + id = "PHom" + ) + new_PHom() + }, persistence = phutil::as_persistence( matrix(NA_real_, nrow = 0L, ncol = 3L), engine = "ripserr::vietoris_rips", @@ -128,7 +136,15 @@ vietoris_rips.matrix <- function( # coerce to 'PHom' class ans <- switch( match.arg(return_class), - PHom = new_PHom(ripser_ans_to_df(ans)), + PHom = { + lifecycle::deprecate_soft( + "1.1.0", + I("'PHom' class"), + with = I("'persistence' from the {phutil} package"), + id = "PHom" + ) + new_PHom(ripser_ans_to_df(ans)) + }, persistence = phutil::as_persistence( ans, engine = "ripserr::vietoris_rips", @@ -184,7 +200,15 @@ vietoris_rips.dist <- function( # coerce to 'PHom' class ans <- switch( match.arg(return_class), - PHom = new_PHom(ripser_ans_to_df(ans)), + PHom = { + lifecycle::deprecate_soft( + "1.1.0", + I("'PHom' class"), + with = I("'persistence' from the {phutil} package"), + id = "PHom" + ) + new_PHom(ripser_ans_to_df(ans)) + }, persistence = phutil::as_persistence( ans, engine = "ripserr::vietoris_rips", From c41c26d7f0798edb2ff16057a30ab2a8be77a61b Mon Sep 17 00:00:00 2001 From: Jason Cory Brunson Date: Tue, 30 Sep 2025 17:44:39 -0400 Subject: [PATCH 7/8] extend cubical to 1-dimensional arrays and vectors --- NAMESPACE | 2 ++ NEWS.md | 19 ++++++++++++++----- R/cubical.R | 22 +++++++++++++++++++++- R/utility.R | 8 ++++++-- man/cubical.Rd | 8 ++++++++ 5 files changed, 51 insertions(+), 8 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index 5e6e424..652e518 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -2,6 +2,7 @@ S3method(cubical,array) S3method(cubical,matrix) +S3method(cubical,numeric) S3method(head,PHom) S3method(print,PHom) S3method(tail,PHom) @@ -17,6 +18,7 @@ export(as.PHom) export(cubical) export(cubical.array) export(cubical.matrix) +export(cubical.numeric) export(is.PHom) export(vietoris_rips) export(vietoris_rips.data.frame) diff --git a/NEWS.md b/NEWS.md index ab6ec3b..1069428 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,17 +1,26 @@ # next version -## permission for deaths before births and superlevel set filtrations of rasters +## Vietoris-Rips PH -A logical argument `sublevel` has been added to `cubical` that, when `FALSE`, will pre- and post-transform raster data in order to obtain superlevel set persistent homology. -Enabling this, an assertion that all `birth < death` has been removed from checks of persistence data. - -## sliding window embeddings of multivariable time series (breaking change) +### sliding window embeddings of multivariable time series (breaking change) Previously only univariable time series could be passed to `vietoris_rips()` via the sliding window embedding (used for quasi-attractor detection). An additional unexported embedding function has been written to handle multivariable time series. (It underperforms the original function on univariable time series, which therefore continue to rely on the original.) Furthermore, whereas `data_dim` previously defaulted to `2`, it now defaults to the number of observations per time unit of a time series as recovered by `tsp()`. (The behavior for unclassed numeric vectors remains unchanged.) +## cubical PH + +### functionality for 1-dimensional arrays + +`cubical()` can now handle 1-dimensional arrays (for which no dedicated source code exists) by treating them as 2-dimensional (with an expanse of 1 in the second dimension). +This enables the new method `cubical.numeric()` to accept vectors. + +### deaths before births and superlevel set filtrations + +A logical argument `sublevel` has been added to `cubical` that, when `FALSE`, will pre- and post-transform raster data in order to obtain superlevel set persistent homology. +Enabling this, an assertion that all `birth < death` has been removed from checks of persistence data. + # ripserr 1.0.0 This major version replaces an outdated version of the Ripser C++ library with its current version. diff --git a/R/cubical.R b/R/cubical.R index 82d0dae..be264ba 100644 --- a/R/cubical.R +++ b/R/cubical.R @@ -24,6 +24,11 @@ #' @return `PHom` object #' @examples #' +#' # 1-dim example +#' dataset <- rnorm(1500) +#' dim(dataset) <- 1500 +#' cubical_hom1 <- cubical(dataset) +#' #' # 2-dim example #' dataset <- rnorm(10 ^ 2) #' dim(dataset) <- rep(10, 2) @@ -74,6 +79,8 @@ cubical.array <- function( method = method) validate_arr_cub(dataset) + # if dataset is 1-dimensional, treat it as 2-dimensional + if (length(dim(dataset)) == 1L) dim(dataset) <- c(dim(dataset), 1L) # transform method parameter for C++ function method_int <- switch(method, @@ -146,4 +153,17 @@ cubical.matrix <- function(dataset, ...) { # return return(ans) -} \ No newline at end of file +} + +#' @rdname cubical +#' @export cubical.numeric +#' @export +cubical.numeric <- function(dataset, ...) { + # convert the numeric vector to a 1-dimensional array + dataset <- as.array(dataset, dim = 1L) + + # calculate persistent homology using cubical.array + ans <- cubical.array(dataset, ...) + + return(ans) +} diff --git a/R/utility.R b/R/utility.R index 30026cb..539e077 100644 --- a/R/utility.R +++ b/R/utility.R @@ -122,7 +122,7 @@ validate_arr_cub <- function(dataset) { error_class(dataset, "dataset", "array") # dataset should have either 2, 3, or 4 dimensions (only ones supported) - if (!(length(dim(dataset)) %in% c(2, 3, 4))) { + if (!(length(dim(dataset)) %in% seq(4))) { stop(paste("dataset parameter must have either 2, 3, or 4 dimensions,", "passed argument has", length(dim(dataset)), "dimensions")) } @@ -139,7 +139,11 @@ validate_arr_cub <- function(dataset) { } # make sure dataset is not too large - if (length(dim(dataset)) == 2) { + if (length(dim(dataset)) == 1L) { + if (dim(dataset) > 2000) { + stop(paste("Max size for dim 1 = 2000; passed size =", dim(dataset))) + } + } else if (length(dim(dataset)) == 2) { if (dim(dataset)[1] > 2000 | dim(dataset)[2] > 1000) { stop(paste("Max size for dim 2 = 2000 x 1000; passed size =", diff --git a/man/cubical.Rd b/man/cubical.Rd index b2f1ea9..8ab3c15 100644 --- a/man/cubical.Rd +++ b/man/cubical.Rd @@ -4,6 +4,7 @@ \alias{cubical} \alias{cubical.array} \alias{cubical.matrix} +\alias{cubical.numeric} \title{Calculating Persistent Homology via a Cubical Complex} \usage{ cubical(dataset, ...) @@ -11,6 +12,8 @@ cubical(dataset, ...) \method{cubical}{array}(dataset, threshold = 9999, method = "lj", sublevel = TRUE, ...) \method{cubical}{matrix}(dataset, ...) + +\method{cubical}{numeric}(dataset, ...) } \arguments{ \item{dataset}{object on which to calculate persistent homology} @@ -50,6 +53,11 @@ persistence diagram in which \code{death} precedes \code{birth}. } \examples{ +# 1-dim example +dataset <- rnorm(1500) +dim(dataset) <- 1500 +cubical_hom1 <- cubical(dataset) + # 2-dim example dataset <- rnorm(10 ^ 2) dim(dataset) <- rep(10, 2) From a3373bf46c8f555ba67e6593ceaf870bd2da69b7 Mon Sep 17 00:00:00 2001 From: Jason Cory Brunson Date: Tue, 30 Sep 2025 17:45:37 -0400 Subject: [PATCH 8/8] increment version number --- DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index ac13e0d..98e102a 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: ripserr Title: Calculate Persistent Homology with Ripser-Based Engines -Version: 1.0.0 +Version: 1.0.0.0003 Authors@R: c(person(given = "Raoul R.", family = "Wadhwa",