From f1a587feb16936af1a2a8e6a143676d062d1de5d Mon Sep 17 00:00:00 2001 From: Neclow Date: Fri, 13 Feb 2026 10:31:24 +0000 Subject: [PATCH 1/4] feat(r): add balance indices --- r-phylo2vec/NAMESPACE | 3 + r-phylo2vec/R/extendr-wrappers.R | 35 +++++++++- r-phylo2vec/R/stats.R | 15 ----- r-phylo2vec/man/b2.Rd | 20 ++++++ r-phylo2vec/man/cophenetic_distances.Rd | 15 +---- r-phylo2vec/man/get_common_ancestor.Rd | 4 +- r-phylo2vec/man/leaf_depth_variance.Rd | 17 +++++ r-phylo2vec/man/sackin.Rd | 18 ++++++ r-phylo2vec/src/rust/src/lib.rs | 47 ++++++++++++++ r-phylo2vec/tests/testthat/test_stats.R | 85 +++++++++++++++++++++++++ 10 files changed, 228 insertions(+), 31 deletions(-) create mode 100644 r-phylo2vec/man/b2.Rd create mode 100644 r-phylo2vec/man/leaf_depth_variance.Rd create mode 100644 r-phylo2vec/man/sackin.Rd diff --git a/r-phylo2vec/NAMESPACE b/r-phylo2vec/NAMESPACE index 67144aa..c337f31 100644 --- a/r-phylo2vec/NAMESPACE +++ b/r-phylo2vec/NAMESPACE @@ -2,6 +2,7 @@ export(add_leaf) export(apply_label_mapping) +export(b2) export(check_m) export(check_v) export(cophenetic_distances) @@ -16,6 +17,7 @@ export(get_node_depth) export(get_node_depths) export(has_branch_lengths) export(incidence) +export(leaf_depth_variance) export(load_newick) export(load_p2v) export(pre_precision) @@ -25,6 +27,7 @@ export(remove_branch_lengths) export(remove_leaf) export(remove_parent_labels) export(robinson_foulds) +export(sackin) export(sample_matrix) export(sample_tree) export(sample_vector) diff --git a/r-phylo2vec/R/extendr-wrappers.R b/r-phylo2vec/R/extendr-wrappers.R index 5410835..ccb0c20 100644 --- a/r-phylo2vec/R/extendr-wrappers.R +++ b/r-phylo2vec/R/extendr-wrappers.R @@ -19,6 +19,18 @@ NULL #' @export add_leaf <- function(vector, leaf, branch) .Call(wrap__add_leaf, vector, leaf, branch) +#' Compute the B2 index of a tree from Shao and Sokal (1990). +#' +#' The B2 index is a measure of tree balance based on the probabilities of random walks from the root to each leaf. +#' For a binary tree, the B2 index can be calculated as the sum of the depth of each leaf multiplied by 2 raised to the power of negative depth of that leaf. +#' Higher values indicate more balanced trees, while lower values indicate more imbalanced trees. +#' For more details, see https://doi.org/10.1007/s00285-021-01662-7. +#' +#' @param vector phylo2vec vector representation of a tree topology +#' @return B2 index (numeric) +#' @export +b2 <- function(vector) .Call(wrap__b2, vector) + #' Apply an integer-taxon label mapping (label_mapping) #' to an integer-based newick (where leaves are integers) #' and produce a mapped Newick (where leaves are strings (taxa)) @@ -61,7 +73,7 @@ check_v <- function(vector) invisible(.Call(wrap__check_v, vector)) #' @param unrooted If true, the distance is calculated as the distance between each leaf and their most recent common ancestor, multiplied by 2. If false, the distance is calculated as the distance from each leaf to their most recent common ancestor. #' @return Cophenetic distance matrix (shape: (n_leaves, n_leaves)) #' @export -cophenetic_distances <- function(tree, unrooted) .Call(wrap__cophenetic_distances, tree, unrooted) +cophenetic_distances <- function(tree, unrooted = FALSE) .Call(wrap__cophenetic_distances, tree, unrooted) #' Create an integer-taxon label mapping (label_mapping) #' from a string-based newick (where leaves are strings) @@ -112,7 +124,7 @@ from_pairs <- function(pairs) .Call(wrap__from_pairs, pairs) #' Similar to ape's `getMRCA` function in R (for leaf nodes) #' and ETE's `get_common_ancestor` in Python (for all nodes), but for phylo2vec vectors. #' -#' @param vector phylo2vec vector representation of a tree topology +#' @param tree A phylo2vec tree #' @param node1 The first node (0-indexed) #' @param node2 The second node (0-indexed) #' @return The common ancestor node (0-indexed) @@ -152,6 +164,15 @@ incidence_csr <- function(input_vector) .Call(wrap__incidence_csr, input_vector) incidence_dense <- function(input_vector) .Call(wrap__incidence_dense, input_vector) +#' Compute the variance of leaf depths in a tree. +#' +#' Higher values indicate more imbalanced trees, while lower values indicate more balanced trees. +#' +#' @param vector phylo2vec vector representation of a tree topology +#' @return Variance of leaf depths (numeric) +#' @export +leaf_depth_variance <- function(vector) .Call(wrap__leaf_depth_variance, vector) + #' Produce an ordered version (i.e., birth-death process version) #' of a phylo2vec vector using the Queue Shuffle algorithm. #' @@ -214,6 +235,16 @@ remove_leaf <- function(vector, leaf) .Call(wrap__remove_leaf, vector, leaf) #' @export robinson_foulds <- function(v1, v2, normalize) .Call(wrap__robinson_foulds, v1, v2, normalize) +#' Compute the Sackin index of a tree. +#' +#' The Sackin index is a measure of tree imbalance, defined as the sum of the depths of all leaves in the tree. +#' Higher values indicate more imbalanced trees, while lower values indicate more balanced trees. +#' +#' @param vector phylo2vec vector representation of a tree topology +#' @return Sackin index (numeric) +#' @export +sackin <- function(vector) .Call(wrap__sackin, vector) + #' Sample a random tree with branch lengths via phylo2vec #' #' @param n_leaves Number of leaves (must be at least 2) diff --git a/r-phylo2vec/R/stats.R b/r-phylo2vec/R/stats.R index 134f13f..74466cd 100644 --- a/r-phylo2vec/R/stats.R +++ b/r-phylo2vec/R/stats.R @@ -27,21 +27,6 @@ precision <- function(vector_or_matrix) { a - b %*% solve(c, d) } -#' Compute the cophenetic distance matrix of a phylo2vec tree. -#' -#' The cophenetic distance between two leaves is the distance from each leaf -#' to their most recent common ancestor. -#' For vectors, this is the topological distance. -#' For matrices, this uses branch lengths. -#' -#' @param tree phylo2vec vector (1D) or matrix (2D) -#' @param unrooted If TRUE, compute unrooted distances. Default is FALSE. -#' @return Cophenetic distance matrix (shape: (n_leaves, n_leaves)) -#' @export -cophenetic_distances <- function(tree, unrooted = FALSE) { - .Call(wrap__cophenetic_distances, tree, unrooted) -} - #' Compute the Robinson-Foulds distance between two trees. #' #' RF distance counts the number of bipartitions (splits) that differ diff --git a/r-phylo2vec/man/b2.Rd b/r-phylo2vec/man/b2.Rd new file mode 100644 index 0000000..5d05ddb --- /dev/null +++ b/r-phylo2vec/man/b2.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/extendr-wrappers.R +\name{b2} +\alias{b2} +\title{Compute the B2 index of a tree from Shao and Sokal (1990).} +\usage{ +b2(vector) +} +\arguments{ +\item{vector}{phylo2vec vector representation of a tree topology} +} +\value{ +B2 index (numeric) +} +\description{ +The B2 index is a measure of tree balance based on the probabilities of random walks from the root to each leaf. +For a binary tree, the B2 index can be calculated as the sum of the depth of each leaf multiplied by 2 raised to the power of negative depth of that leaf. +Higher values indicate more balanced trees, while lower values indicate more imbalanced trees. +For more details, see https://doi.org/10.1007/s00285-021-01662-7. +} diff --git a/r-phylo2vec/man/cophenetic_distances.Rd b/r-phylo2vec/man/cophenetic_distances.Rd index 2e94097..ac3d34a 100644 --- a/r-phylo2vec/man/cophenetic_distances.Rd +++ b/r-phylo2vec/man/cophenetic_distances.Rd @@ -1,29 +1,20 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/extendr-wrappers.R, R/stats.R +% Please edit documentation in R/extendr-wrappers.R \name{cophenetic_distances} \alias{cophenetic_distances} \title{Get the (topological) cophenetic distance matrix of a phylo2vec tree} \usage{ -cophenetic_distances(tree, unrooted = FALSE) - cophenetic_distances(tree, unrooted = FALSE) } \arguments{ -\item{tree}{phylo2vec vector (1D) or matrix (2D)} +\item{tree}{A phylo2vec tree} -\item{unrooted}{If TRUE, compute unrooted distances. Default is FALSE.} +\item{unrooted}{If true, the distance is calculated as the distance between each leaf and their most recent common ancestor, multiplied by 2. If false, the distance is calculated as the distance from each leaf to their most recent common ancestor.} } \value{ -Cophenetic distance matrix (shape: (n_leaves, n_leaves)) - Cophenetic distance matrix (shape: (n_leaves, n_leaves)) } \description{ The cophenetic distance between two leaves is the distance from each leaf to their most recent common ancestor. For phylo2vec vectors, this is the topological distance. For phylo2vec matrices, this is the distance with branch lengths. - -The cophenetic distance between two leaves is the distance from each leaf -to their most recent common ancestor. -For vectors, this is the topological distance. -For matrices, this uses branch lengths. } diff --git a/r-phylo2vec/man/get_common_ancestor.Rd b/r-phylo2vec/man/get_common_ancestor.Rd index 2cfe87a..a76722d 100644 --- a/r-phylo2vec/man/get_common_ancestor.Rd +++ b/r-phylo2vec/man/get_common_ancestor.Rd @@ -10,11 +10,11 @@ and ETE's \code{get_common_ancestor} in Python (for all nodes), but for phylo2ve get_common_ancestor(tree, node1, node2) } \arguments{ +\item{tree}{A phylo2vec tree} + \item{node1}{The first node (0-indexed)} \item{node2}{The second node (0-indexed)} - -\item{vector}{phylo2vec vector representation of a tree topology} } \value{ The common ancestor node (0-indexed) diff --git a/r-phylo2vec/man/leaf_depth_variance.Rd b/r-phylo2vec/man/leaf_depth_variance.Rd new file mode 100644 index 0000000..5b7620f --- /dev/null +++ b/r-phylo2vec/man/leaf_depth_variance.Rd @@ -0,0 +1,17 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/extendr-wrappers.R +\name{leaf_depth_variance} +\alias{leaf_depth_variance} +\title{Compute the variance of leaf depths in a tree.} +\usage{ +leaf_depth_variance(vector) +} +\arguments{ +\item{vector}{phylo2vec vector representation of a tree topology} +} +\value{ +Variance of leaf depths (numeric) +} +\description{ +Higher values indicate more imbalanced trees, while lower values indicate more balanced trees. +} diff --git a/r-phylo2vec/man/sackin.Rd b/r-phylo2vec/man/sackin.Rd new file mode 100644 index 0000000..6a947ec --- /dev/null +++ b/r-phylo2vec/man/sackin.Rd @@ -0,0 +1,18 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/extendr-wrappers.R +\name{sackin} +\alias{sackin} +\title{Compute the Sackin index of a tree.} +\usage{ +sackin(vector) +} +\arguments{ +\item{vector}{phylo2vec vector representation of a tree topology} +} +\value{ +Sackin index (numeric) +} +\description{ +The Sackin index is a measure of tree imbalance, defined as the sum of the depths of all leaves in the tree. +Higher values indicate more imbalanced trees, while lower values indicate more balanced trees. +} diff --git a/r-phylo2vec/src/rust/src/lib.rs b/r-phylo2vec/src/rust/src/lib.rs index f31e92e..0a29735 100644 --- a/r-phylo2vec/src/rust/src/lib.rs +++ b/r-phylo2vec/src/rust/src/lib.rs @@ -9,6 +9,7 @@ use phylo2vec::matrix::convert as mconvert; use phylo2vec::matrix::graph as mgraph; use phylo2vec::matrix::ops as mops; use phylo2vec::newick; +use phylo2vec::vector::balance as vbalance; use phylo2vec::vector::base as vbase; use phylo2vec::vector::convert as vconvert; use phylo2vec::vector::distance as vdist; @@ -634,12 +635,56 @@ fn robinson_foulds(v1: Vec, v2: Vec, normalize: bool) -> f64 { vdist::robinson_foulds(&v1_usize, &v2_usize, normalize) } +/// Compute the Sackin index of a tree. +/// +/// The Sackin index is a measure of tree imbalance, defined as the sum of the depths of all leaves in the tree. +/// Higher values indicate more imbalanced trees, while lower values indicate more balanced trees. +/// +/// @param vector phylo2vec vector representation of a tree topology +/// @return Sackin index (numeric) +/// @export +#[extendr] +fn sackin(vector: Vec) -> i32 { + let v_usize = as_usize(vector); + vbalance::sackin(&v_usize) as i32 +} + +/// Compute the variance of leaf depths in a tree. +/// +/// Higher values indicate more imbalanced trees, while lower values indicate more balanced trees. +/// +/// @param vector phylo2vec vector representation of a tree topology +/// @return Variance of leaf depths (numeric) +/// @export +#[extendr] +fn leaf_depth_variance(vector: Vec) -> f64 { + let v_usize = as_usize(vector); + vbalance::leaf_depth_variance(&v_usize) +} + +/// Compute the B2 index of a tree from Shao and Sokal (1990). +/// +/// The B2 index is a measure of tree balance based on the probabilities of random walks from the root to each leaf. +/// For a binary tree, the B2 index can be calculated as the sum of the depth of each leaf multiplied by 2 raised to the power of negative depth of that leaf. +/// Higher values indicate more balanced trees, while lower values indicate more imbalanced trees. +/// For more details, see https://doi.org/10.1007/s00285-021-01662-7. +/// +/// @param vector phylo2vec vector representation of a tree topology +/// @return B2 index (numeric) +/// @export +#[extendr] +fn b2(vector: Vec) -> f64 { + let v_usize = as_usize(vector); + vbalance::b2(&v_usize) +} + // Macro to generate exports. // This ensures exported functions are registered with R. // See corresponding C code in `entrypoint.c`. extendr_module! { mod phylo2vec; fn add_leaf; + fn b2; fn apply_label_mapping; fn check_m; fn check_v; @@ -657,12 +702,14 @@ extendr_module! { fn incidence_csc; fn incidence_csr; fn incidence_dense; + fn leaf_depth_variance; fn queue_shuffle; fn remove_branch_lengths; fn remove_parent_labels; fn pre_precision; fn remove_leaf; fn robinson_foulds; + fn sackin; fn sample_matrix; fn sample_vector; fn to_ancestry; diff --git a/r-phylo2vec/tests/testthat/test_stats.R b/r-phylo2vec/tests/testthat/test_stats.R index b315e86..0b1e812 100644 --- a/r-phylo2vec/tests/testthat/test_stats.R +++ b/r-phylo2vec/tests/testthat/test_stats.R @@ -224,6 +224,91 @@ test_that(desc = "robinson_foulds_normalized_bounds", { } }) +# Balance index tests + +test_that(desc = "sackin_manual", { + # Small trees + expect_equal(sackin(as.integer(c(0))), 2) + expect_equal(sackin(as.integer(c(0, 2, 2, 3))), 12) + # Ladder trees: S = n(n + 1) / 2 - 1 + expect_equal(sackin(as.integer(c(0, 0))), 5) + expect_equal(sackin(as.integer(c(0, 0, 0))), 9) + expect_equal(sackin(as.integer(c(0, 0, 0, 0))), 14) + expect_equal(sackin(as.integer(rep(0, 49))), 1274) + # Balanced trees: S = n * log2(n) + expect_equal(sackin(as.integer(c(0, 2, 2))), 8) + expect_equal(sackin(as.integer(c(0, 2, 2, 6, 4, 6, 6))), 24) + expect_equal( + sackin(as.integer(c(0, 2, 2, 6, 4, 6, 6, 14, 8, 10, 10, 14, 12, 14, 14))), + 64 + ) +}) + +test_that(desc = "leaf_depth_variance_manual", { + # Small trees + expect_equal(leaf_depth_variance(as.integer(c(0))), 0.0) + expect_true(allclose(leaf_depth_variance(as.integer(c(0, 2, 2, 3))), 0.24)) + # Balanced trees: Var = 0 + expect_equal(leaf_depth_variance(as.integer(c(0, 2, 2))), 0.0) + expect_equal(leaf_depth_variance(as.integer(c(0, 2, 2, 6, 4, 6, 6))), 0.0) + # Ladder trees: Var = (n - 1)(n - 2)(n^2 + 3n - 6) / (12n^2) + expect_true(allclose(leaf_depth_variance(as.integer(c(0, 0))), 0.2222222)) + expect_true(allclose(leaf_depth_variance(as.integer(c(0, 0, 0))), 0.6875)) + expect_true(allclose(leaf_depth_variance(as.integer(c(0, 0, 0, 0))), 1.36)) + expect_true(allclose(leaf_depth_variance(as.integer(rep(0, 49))), 207.2896)) +}) + +test_that(desc = "b2_manual", { + # Small trees + expect_true(allclose(b2(as.integer(c(0))), 1.0)) + expect_true(allclose(b2(as.integer(c(0, 2, 2, 3))), 2.25)) + # Ladder trees: B2 = 2 - 2^(2 - n) + expect_true(allclose(b2(as.integer(c(0, 0))), 1.5)) + expect_true(allclose(b2(as.integer(c(0, 0, 0))), 1.75)) + expect_true(allclose(b2(as.integer(c(0, 0, 0, 0))), 1.875)) + expect_true(allclose(b2(as.integer(c(0, 0, 0, 0, 0))), 1.9375)) + # Balanced trees: B2 = log2(n) + expect_true(allclose(b2(as.integer(c(0, 2, 2))), 2.0)) + expect_true(allclose(b2(as.integer(c(0, 2, 2, 6, 4, 6, 6))), 3.0)) + expect_true(allclose( + b2(as.integer(c(0, 2, 2, 6, 4, 6, 6, 14, 8, 10, 10, 14, 12, 14, 14))), + 4.0 + )) +}) + +test_that(desc = "sackin_matches_treestats", { + for (n_leaves in seq(MIN_N_LEAVES, MAX_N_LEAVES_STATS)) { + for (j in seq_len(N_REPEATS)) { + v <- sample_vector(n_leaves, FALSE) + tr <- ape::read.tree(text = to_newick(v)) + expect_equal(sackin(v), treestats::sackin(tr)) + } + } +}) + +test_that(desc = "b2_matches_treestats", { + for (n_leaves in seq(MIN_N_LEAVES, MAX_N_LEAVES_STATS)) { + for (j in seq_len(N_REPEATS)) { + v <- sample_vector(n_leaves, FALSE) + tr <- ape::read.tree(text = to_newick(v)) + expect_true(allclose(b2(v), treestats::b2(tr))) + } + } +}) + +test_that(desc = "leaf_depth_variance_matches_treestats", { + for (n_leaves in seq(MIN_N_LEAVES, MAX_N_LEAVES_STATS)) { + for (j in seq_len(N_REPEATS)) { + v <- sample_vector(n_leaves, FALSE) + tr <- ape::read.tree(text = to_newick(v)) + expect_true(allclose( + leaf_depth_variance(v), + treestats::var_leaf_depth(tr) + )) + } + } +}) + test_that(desc = "robinson_foulds_matches_treedist", { for (n_leaves in seq(MIN_N_LEAVES, MAX_N_LEAVES_STATS)) { for (j in seq_len(N_REPEATS)) { From a951cf5ab1afc1019f7ff3b93162d3ba386abcfc Mon Sep 17 00:00:00 2001 From: Neclow Date: Fri, 13 Feb 2026 10:31:42 +0000 Subject: [PATCH 2/4] docs: add treestats install recommendation in dev docs --- docs/development.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/development.md b/docs/development.md index a722169..8ef6b06 100644 --- a/docs/development.md +++ b/docs/development.md @@ -158,6 +158,13 @@ To quickly install the package and run it, simply run the following pixi run -e r-phylo2vec install-r ``` +Some R test dependencies are not available on conda-forge and must be installed +once from CRAN: + +```console +pixi run -e r-phylo2vec Rscript -e "install.packages('treestats', repos='https://cloud.r-project.org')" +``` + Once the package is installed you can open up the R terminal: ```console From b31193338c99146cdde24264f91d772f66fdd28b Mon Sep 17 00:00:00 2001 From: Neclow Date: Fri, 13 Feb 2026 10:40:07 +0000 Subject: [PATCH 3/4] fix(r): add skip_if_not_installed for treestats --- r-phylo2vec/tests/testthat/test_stats.R | 3 +++ 1 file changed, 3 insertions(+) diff --git a/r-phylo2vec/tests/testthat/test_stats.R b/r-phylo2vec/tests/testthat/test_stats.R index 0b1e812..8ce5aaf 100644 --- a/r-phylo2vec/tests/testthat/test_stats.R +++ b/r-phylo2vec/tests/testthat/test_stats.R @@ -277,6 +277,7 @@ test_that(desc = "b2_manual", { }) test_that(desc = "sackin_matches_treestats", { + skip_if_not_installed("treestats") for (n_leaves in seq(MIN_N_LEAVES, MAX_N_LEAVES_STATS)) { for (j in seq_len(N_REPEATS)) { v <- sample_vector(n_leaves, FALSE) @@ -287,6 +288,7 @@ test_that(desc = "sackin_matches_treestats", { }) test_that(desc = "b2_matches_treestats", { + skip_if_not_installed("treestats") for (n_leaves in seq(MIN_N_LEAVES, MAX_N_LEAVES_STATS)) { for (j in seq_len(N_REPEATS)) { v <- sample_vector(n_leaves, FALSE) @@ -297,6 +299,7 @@ test_that(desc = "b2_matches_treestats", { }) test_that(desc = "leaf_depth_variance_matches_treestats", { + skip_if_not_installed("treestats") for (n_leaves in seq(MIN_N_LEAVES, MAX_N_LEAVES_STATS)) { for (j in seq_len(N_REPEATS)) { v <- sample_vector(n_leaves, FALSE) From 2ef79bd0f98303a59187222390fd9ab086d18e38 Mon Sep 17 00:00:00 2001 From: Neclow Date: Fri, 13 Feb 2026 11:05:13 +0000 Subject: [PATCH 4/4] fix(r): fix copilot suggestion for RF with default --- r-phylo2vec/R/extendr-wrappers.R | 2 +- r-phylo2vec/src/rust/src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/r-phylo2vec/R/extendr-wrappers.R b/r-phylo2vec/R/extendr-wrappers.R index ccb0c20..f7d95d0 100644 --- a/r-phylo2vec/R/extendr-wrappers.R +++ b/r-phylo2vec/R/extendr-wrappers.R @@ -233,7 +233,7 @@ remove_leaf <- function(vector, leaf) .Call(wrap__remove_leaf, vector, leaf) #' @param normalize If TRUE, return normalized distance in range `[0.0, 1.0]` #' @return RF distance (numeric) #' @export -robinson_foulds <- function(v1, v2, normalize) .Call(wrap__robinson_foulds, v1, v2, normalize) +robinson_foulds <- function(v1, v2, normalize = FALSE) .Call(wrap__robinson_foulds, v1, v2, normalize) #' Compute the Sackin index of a tree. #' diff --git a/r-phylo2vec/src/rust/src/lib.rs b/r-phylo2vec/src/rust/src/lib.rs index 0a29735..0f4f6c6 100644 --- a/r-phylo2vec/src/rust/src/lib.rs +++ b/r-phylo2vec/src/rust/src/lib.rs @@ -629,7 +629,7 @@ fn incidence_csr(input_vector: Vec) -> List { /// @return RF distance (numeric) /// @export #[extendr] -fn robinson_foulds(v1: Vec, v2: Vec, normalize: bool) -> f64 { +fn robinson_foulds(v1: Vec, v2: Vec, #[default = "FALSE"] normalize: bool) -> f64 { let v1_usize = as_usize(v1); let v2_usize = as_usize(v2); vdist::robinson_foulds(&v1_usize, &v2_usize, normalize)