| Type: | Package |
| Title: | Discrete Morse Theory for 3D Meshes Derived from Point Clouds |
| Version: | 1.0.0 |
| Date: | 2026-07-07 |
| Description: | Ultra-fast computation of discrete Morse (Marston Morse) gradient vector fields and critical simplices (0-simplices: vertices, 1-simplices: edges, 2-simplices: faces) on 3D triangular meshes from point clouds. Provides C++ backend with parallel processing for large-scale topological analysis, including connected component analysis and visualization tools. Perfect for Light Detection and Ranging ('LiDAR') data, computational topology, and geometric analysis applications. The implementation follows Forman (1998) <doi:10.1007/PL00009307> for discrete Morse theory, with extensions for 3D mesh processing. |
| Language: | en-US |
| License: | MIT + file LICENSE |
| URL: | https://github.com/DijoG/DiscreteMorseR |
| BugReports: | https://github.com/DijoG/DiscreteMorseR/issues |
| Depends: | R (≥ 3.5.0) |
| Imports: | Rcpp (≥ 1.0.10), dplyr (≥ 1.0.0), purrr (≥ 0.3.0), stringr (≥ 1.4.0), data.table (≥ 1.12.0), gtools (≥ 3.8.0), readr (≥ 1.3.0), future (≥ 1.18.0), furrr (≥ 0.2.0) |
| Suggests: | clustermq (≥ 0.8.0), ggplot2 (≥ 3.3.0), testthat (≥ 3.0.0), knitr (≥ 1.30), rmarkdown (≥ 2.0), tictoc |
| LinkingTo: | Rcpp, RcppArmadillo |
| SystemRequirements: | C++17 |
| Encoding: | UTF-8 |
| RoxygenNote: | 7.3.1 |
| Config/testthat/edition: | 3 |
| NeedsCompilation: | yes |
| Packaged: | 2026-07-07 13:50:56 UTC; Dijo |
| Author: | Gergo Dioszegi |
| Maintainer: | Gergo Dioszegi <dijogergo@gmail.com> |
| Repository: | CRAN |
| Date/Publication: | 2026-07-16 12:50:40 UTC |
DiscreteMorseR: Discrete Morse Theory Analysis for 3D Meshes
Description
A package for computing discrete Morse vector fields, critical simplices, and performing topological analysis of 3D mesh data using Discrete Morse Theory.
Details
The package provides functions to analyze 3D triangular meshes, compute discrete Morse functions, identify critical simplices, and visualize the Morse complex structure.
Author(s)
Gergo Dioszegi
See Also
Useful links:
Report bugs at https://github.com/DijoG/DiscreteMorseR/issues
Add decimal formatting
Description
Add decimal formatting
Usage
add_DECIMAL(x, k)
Arguments
x |
Numeric vector |
k |
Number of decimal places |
Value
Formatted character vector
Compute Morse complex from mesh
Description
Compute Morse complex from mesh
Usage
compute_MORSE_complex(
mesh,
output_dir = NULL,
parallel = TRUE,
cores = 4,
batch_size = NULL
)
Arguments
mesh |
Mesh object from get_CCMESH() |
output_dir |
Optional directory to save results. If provided, writes: - vertices.txt, edges.txt, faces.txt: Mesh simplices - vector_field.txt: Gradient vector field pairs - critical_simplices.txt: Critical simplices - lowerSTAR.txt: Lower star filtration data |
parallel |
Whether to use parallel processing (default: TRUE) |
cores |
Number of cores for parallel processing (default: 4) |
batch_size |
Number of vertices per batch in parallel processing |
Value
A list with three components:
vector_field |
Character vector of gradient pairs in format "from:to" |
critical |
Character vector of critical simplices (vertices, edges, faces) |
simplices |
List containing vertices, edges, and faces data frames |
Examples
# Create a tetrahedron mesh
vertices <- matrix(c(0,0,0, 1,0,0, 0,1,0, 0,0,1), ncol=3, byrow=TRUE)
colnames(vertices) <- c("X", "Y", "Z")
faces <- matrix(c(1,2,3, 1,2,4, 1,3,4, 2,3,4), ncol=3, byrow=TRUE)
colnames(faces) <- c("i1", "i2", "i3")
# Extract unique edges from faces
all_edges <- rbind(faces[,c(1,2)], faces[,c(1,3)], faces[,c(2,3)])
unique_edges <- unique(t(apply(all_edges, 1, sort)))
edges <- data.frame(i1 = unique_edges[,1], i2 = unique_edges[,2])
# Create mesh object
mesh <- list(vertices = vertices, faces = faces, edges = edges)
attr(mesh, "input_truth") <- 1:nrow(vertices)
# Compute Morse complex (sequential mode for CRAN checks)
result <- compute_MORSE_complex(mesh, parallel = FALSE)
# View results
print(paste("Critical simplices:", length(result$critical)))
print(paste("Gradient pairs:", length(result$vector_field)))
Compute lower star in parallel
Description
Compute lower star in parallel
Usage
compute_lowerSTAR_parallel(
vertex,
edge,
face,
output_dir = NULL,
cores = NULL,
batch_size = NULL
)
Arguments
vertex |
Vertex data |
edge |
Edge data |
face |
Face data |
output_dir |
Output directory |
cores |
Number of cores (default: available cores-1) |
batch_size |
Number of vertices per batch |
Value
A list of lower star sets, one for each vertex. Each element contains:
id |
Vertex ID |
lexi_label |
Lexicographically sorted simplex label |
lexi_id |
Lexicographically sorted simplex IDs |
Examples
# Small example for automatic testing
# Create synthetic mesh
set.seed(456)
n_verts <- 50
# Generate random vertices
vertices <- matrix(rnorm(n_verts * 3), ncol = 3)
colnames(vertices) <- c("X", "Y", "Z")
# Create edges (connect each vertex to 3-5 random others)
edges_list <- list()
for (i in 1:n_verts) {
n_edges <- sample(3:5, 1)
neighbors <- sample(setdiff(1:n_verts, i), min(n_edges, n_verts-1))
for (j in neighbors) {
edges_list[[length(edges_list) + 1]] <- c(min(i, j), max(i, j))
}
}
edges <- unique(do.call(rbind, edges_list))
colnames(edges) <- c("i1", "i2")
# Create faces (triangulate using edge connections)
faces_list <- list()
for (i in 1:n_verts) {
connected <- unique(c(
edges[edges[,1] == i, 2],
edges[edges[,2] == i, 1]
))
if (length(connected) >= 3) {
for (j in 1:(min(length(connected), 4) - 1)) {
for (k in (j+1):(min(length(connected), 4))) {
if (length(faces_list) < n_verts * 2) {
faces_list[[length(faces_list) + 1]] <- c(i, connected[j], connected[k])
}
}
}
}
}
faces <- unique(do.call(rbind, faces_list))
colnames(faces) <- c("i1", "i2", "i3")
# Create mesh object
mesh <- list(
vertices = vertices,
faces = faces,
edges = data.frame(edges)
)
attr(mesh, "input_truth") <- 1:n_verts
# Compute Morse complex sequentially (for CRAN checks)
result_seq <- compute_MORSE_complex(mesh, parallel = FALSE)
# Results
str(result_seq)
# Parallel computation (if clustermq is available)
if (requireNamespace("clustermq", quietly = TRUE)) {
# Compute in parallel with 2 cores
result_par <- compute_MORSE_complex(
mesh,
output_dir = tempdir(),
parallel = TRUE,
cores = 2
)
cat("Parallel results:\n")
cat(" Critical simplices:", length(result_par$critical), "\n")
cat(" Gradient pairs:", length(result_par$vector_field), "\n")
# Verify results match
if (length(result_seq$critical) == length(result_par$critical) &&
length(result_seq$vector_field) == length(result_par$vector_field)) {
cat("[OK] Sequential and parallel results match!\n")
}
}
Ultra-fast mesh preparation with connected components
Description
Ultra-fast mesh preparation with connected components
Usage
get_CCMESH(alphahull, select_largest = TRUE)
Arguments
alphahull |
Alphahull generated by ahull3D::ahull3d() |
select_largest |
If TRUE, returns only largest connected component (default: TRUE) If FALSE, returns list of all connected components |
Value
If select_largest = TRUE: A mesh list with components:
vertices |
Nx3 matrix of vertex coordinates (X, Y, Z) |
faces |
Mx3 matrix of face indices (i1, i2, i3) |
edges |
Kx2 data frame of edge indices (i1, i2) |
with attribute "input_truth" containing propagated labels. If select_largest = FALSE: A list of mesh objects (same structure as above) with attributes "n_components" and "component_sizes".
Examples
# Small example for automatic testing (runs in < 5 seconds)
# Create a synthetic alpha hull object mimicking ahull3D output
set.seed(123)
n_vertices <- 40
# Generate random points on a sphere surface (like alpha hull)
theta <- runif(n_vertices, 0, 2*pi)
phi <- acos(runif(n_vertices, -1, 1))
r <- 2 + runif(n_vertices, -0.5, 0.5) # Slight radial variation
# Convert to Cartesian coordinates
vertices <- cbind(
r * sin(phi) * cos(theta),
r * sin(phi) * sin(theta),
r * cos(phi)
)
colnames(vertices) <- c("X", "Y", "Z")
# Create a triangulation (simulating alpha hull faces)
# Use a simple approach: connect nearby points
dist_matrix <- as.matrix(dist(vertices))
faces <- matrix(NA, nrow = n_vertices * 2, ncol = 3)
face_count <- 0
for (i in 1:n_vertices) {
# Find nearest neighbors
neighbors <- order(dist_matrix[i, ])[2:min(6, n_vertices)]
for (j in 1:(length(neighbors) - 1)) {
for (k in (j+1):length(neighbors)) {
if (face_count < n_vertices * 2) {
face_count <- face_count + 1
faces[face_count, ] <- c(i, neighbors[j], neighbors[k])
}
}
}
}
faces <- unique(faces[!is.na(faces[,1]), ])
colnames(faces) <- c("i1", "i2", "i3")
# Create alpha hull-like object with mesh3d structure
alpha_hull <- list(
vb = t(cbind(vertices, 1)), # 4xN matrix with homogeneous coordinates
it = t(faces), # 3xM matrix of faces
normals = t(cbind(vertices / sqrt(rowSums(vertices^2)), 1)) # Approximate normals
)
class(alpha_hull) <- "mesh3d"
# Add attributes that ahull3D would provide
input_truth <- sample(1:20, n_vertices, replace = TRUE)
attr(alpha_hull, "input_truth") <- input_truth
# Create face_truth by propagating vertex labels
face_truth <- matrix(input_truth[faces], nrow = 3, byrow = FALSE)
attr(alpha_hull, "face_truth") <- face_truth
attr(alpha_hull, "alpha") <- 0.5
# Verify structure matches ahull3D output
str(alpha_hull)
# Extract mesh using get_CCMESH
mesh <- get_CCMESH(alpha_hull, select_largest = TRUE)
# Check results
print(paste("Vertices:", nrow(mesh$vertices),
"Faces:", nrow(mesh$faces),
"Edges:", nrow(mesh$edges)))
print(paste("Input truth propagated:",
length(attr(mesh, "input_truth")), "labels"))
# Larger example with more complex structure
set.seed(456)
n_vertices <- 100
# Generate points on multiple surfaces (simulating alpha hull with components)
theta <- runif(n_vertices, 0, 2*pi)
phi <- acos(runif(n_vertices, -1, 1))
r <- 3 + runif(n_vertices, -1, 1)
vertices <- cbind(
r * sin(phi) * cos(theta),
r * sin(phi) * sin(theta),
r * cos(phi)
)
colnames(vertices) <- c("X", "Y", "Z")
# Create more complex triangulation
dist_matrix <- as.matrix(dist(vertices))
faces <- matrix(NA, nrow = n_vertices * 3, ncol = 3)
face_count <- 0
for (i in 1:n_vertices) {
neighbors <- order(dist_matrix[i, ])[2:min(8, n_vertices)]
for (j in 1:(length(neighbors) - 1)) {
for (k in (j+1):length(neighbors)) {
if (face_count < n_vertices * 3) {
face_count <- face_count + 1
faces[face_count, ] <- c(i, neighbors[j], neighbors[k])
}
}
}
}
faces <- unique(faces[!is.na(faces[,1]), ])
colnames(faces) <- c("i1", "i2", "i3")
alpha_hull <- list(
vb = t(cbind(vertices, 1)),
it = t(faces),
normals = t(cbind(vertices / sqrt(rowSums(vertices^2)), 1))
)
class(alpha_hull) <- "mesh3d"
input_truth <- sample(1:30, n_vertices, replace = TRUE)
attr(alpha_hull, "input_truth") <- input_truth
face_truth <- matrix(input_truth[faces], nrow = 3, byrow = FALSE)
attr(alpha_hull, "face_truth") <- face_truth
attr(alpha_hull, "alpha") <- 0.8
# Extract mesh with all components
mesh <- get_CCMESH(alpha_hull, select_largest = FALSE)
print(paste("Number of connected components:",
attr(mesh, "n_components")))
print("Component sizes:")
print(attr(mesh, "component_sizes"))
Extract and process simplices from mesh
Description
Extract and process simplices from mesh
Usage
get_SIMPLICES(mesh, txt_dirout = "")
Arguments
mesh |
Mesh object from get_CCMESH() |
txt_dirout |
Directory for output files (optional) |
Value
List of processed simplices
Compute lexicographically sorted simplex labels
Description
Compute lexicographically sorted simplex labels
Usage
get_lexIDLAB(df)
Arguments
df |
Data frame with label and idlabel columns |
Value
Data table with lexicographically sorted labels
Compute lower star filtration for vertices
Description
Compute lower star filtration for vertices
Usage
get_lowerSTAR(vertex, edge, face, dirout = NULL, cores = 1)
Arguments
vertex |
Vertex data |
edge |
Edge data |
face |
Face data |
dirout |
Output directory (optional) |
cores |
Number of cores (for consistency) |
Value
List of lower star sets
Fast simplex center calculation
Description
Fast simplex center calculation
Usage
get_simplexCENTER(simplex, vertices_matrix)
Arguments
simplex |
Simplex identifier string |
vertices_matrix |
Vertex coordinates as numeric matrix (columns: X, Y, Z) |
Value
Numeric vector of coordinates
Fast Morse complex visualization using get_simplexCENTER()
Description
Fast Morse complex visualization using get_simplexCENTER()
Usage
visualize_MORSE_2d(
morse_complex,
projection = "XZ",
point_alpha = 0.6,
point_size = 1,
max_points = 30000,
plot_gradient = TRUE,
plot_critical = TRUE
)
Arguments
morse_complex |
Output from compute_MORSE_complex() |
projection |
Projection plane: "XY", "XZ", or "YZ" (default: "XZ") |
point_alpha |
Point transparency (default: 0.6) |
point_size |
Point size (default: 1) |
max_points |
Maximum points to plot per category (default: 30000) |
plot_gradient |
Whether to plot gradient arrows (default: TRUE) |
plot_critical |
Whether to plot critical points (default: TRUE) |
Value
A ggplot2 object showing the Morse complex in 2D projection. The plot displays: - Grey arrows: Gradient vector field pairs - Red circles: Critical vertices (0-simplices) - Blue plus signs: Critical edges (1-simplices) - Green diamonds: Critical faces (2-simplices)
Examples
# Simple example (runs in < 5 seconds)
if (requireNamespace("ggplot2", quietly = TRUE)) {
set.seed(123)
vertices <- matrix(rnorm(30), ncol = 3)
colnames(vertices) <- c("X", "Y", "Z")
edges <- data.frame(
i1 = sample(1:10, 15, replace = TRUE),
i2 = sample(1:10, 15, replace = TRUE)
)
edges <- unique(edges[edges$i1 != edges$i2, ])
faces <- data.frame(
i1 = sample(1:10, 10, replace = TRUE),
i2 = sample(1:10, 10, replace = TRUE),
i3 = sample(1:10, 10, replace = TRUE)
)
faces <- unique(faces[faces$i1 != faces$i2 &
faces$i2 != faces$i3 &
faces$i1 != faces$i3, ])
mesh <- list(vertices = vertices, faces = as.matrix(faces), edges = edges)
attr(mesh, "input_truth") <- 1:nrow(vertices)
result <- compute_MORSE_complex(mesh, parallel = FALSE)
p <- visualize_MORSE_2d(result, plot_gradient = FALSE)
print(p)
}
# Larger example with gradient arrows
if (requireNamespace("ggplot2", quietly = TRUE)) {
set.seed(456)
n_verts <- 80
vertices <- matrix(rnorm(n_verts * 3), ncol = 3)
colnames(vertices) <- c("X", "Y", "Z")
edges <- data.frame(
i1 = sample(1:n_verts, 40, replace = TRUE),
i2 = sample(1:n_verts, 40, replace = TRUE)
)
edges <- unique(edges[edges$i1 != edges$i2, ])
faces <- data.frame(
i1 = sample(1:n_verts, 30, replace = TRUE),
i2 = sample(1:n_verts, 30, replace = TRUE),
i3 = sample(1:n_verts, 30, replace = TRUE)
)
faces <- unique(faces[faces$i1 != faces$i2 &
faces$i2 != faces$i3 &
faces$i1 != faces$i3, ])
mesh <- list(vertices = vertices, faces = as.matrix(faces), edges = edges)
attr(mesh, "input_truth") <- 1:nrow(vertices)
result <- compute_MORSE_complex(mesh, parallel = FALSE)
p <- visualize_MORSE_2d(result, plot_gradient = TRUE)
print(p)
}