From c365267d102770cd78dac0a1df47a93cce14577d Mon Sep 17 00:00:00 2001 From: Colin Roberts Date: Sun, 15 Feb 2026 06:51:22 -0700 Subject: [PATCH 1/9] refactor --- CMakeLists.txt | 11 +- Makefile | 12 +- README.md | 30 +- benches/bench_dod.cpp | 122 +++---- benches/bench_geometry.cpp | 60 ++-- benches/bench_pipelines.cpp | 138 ++++---- include/igneous/data/buffers.hpp | 66 ---- include/igneous/data/mesh.hpp | 37 --- include/igneous/data/space.hpp | 86 +++++ include/igneous/data/structure.hpp | 31 ++ .../diffusion_geometry.hpp} | 304 +----------------- .../structures/discrete_exterior_calculus.hpp | 278 ++++++++++++++++ include/igneous/igneous.hpp | 11 +- include/igneous/io/exporter.hpp | 52 +-- include/igneous/io/importer.hpp | 31 +- include/igneous/ops/{ => dec}/curvature.hpp | 57 ++-- include/igneous/ops/{ => dec}/flow.hpp | 63 ++-- include/igneous/ops/diffusion/basis.hpp | 4 +- include/igneous/ops/diffusion/forms.hpp | 36 +-- include/igneous/ops/diffusion/geometry.hpp | 280 ++++++++-------- include/igneous/ops/diffusion/hodge.hpp | 24 +- include/igneous/ops/diffusion/products.hpp | 8 +- include/igneous/ops/diffusion/spectral.hpp | 170 +++++----- include/igneous/ops/transform.hpp | 12 +- notes/structure_refactor/journal.md | 90 ++++++ scripts/diffgeo/compare_diffgeo_ops.py | 4 +- scripts/diffgeo/run_cpp_diffgeo_ops.sh | 2 +- scripts/diffgeo/run_parity_round.sh | 4 +- scripts/diffgeo/run_reference_diffgeo_ops.py | 2 +- scripts/perf/compare_against_main.py | 2 +- src/main_diffusion.cpp | 24 +- ...pology.cpp => main_diffusion_geometry.cpp} | 90 +++--- src/main_hodge.cpp | 70 ++-- src/main_mesh.cpp | 26 +- src/main_point.cpp | 19 +- src/main_spectral.cpp | 18 +- tests/test_diffgeo_cli_outputs.sh | 2 +- tests/test_diffgeo_parity_optional.sh | 4 +- tests/test_io_meshes.cpp | 35 +- tests/test_ops_curvature_flow.cpp | 35 +- tests/test_ops_diffusion_basis.cpp | 8 +- tests/test_ops_diffusion_forms.cpp | 56 ++-- tests/test_ops_diffusion_wedge.cpp | 30 +- tests/test_ops_hodge.cpp | 36 +-- tests/test_ops_spectral_geometry.cpp | 28 +- ...gy_triangle.cpp => test_structure_dec.cpp} | 6 +- ... => test_structure_diffusion_geometry.cpp} | 57 ++-- visualizations/README.md | 4 +- ...ogy.py => view_main_diffusion_geometry.py} | 14 +- 49 files changed, 1316 insertions(+), 1273 deletions(-) delete mode 100644 include/igneous/data/buffers.hpp delete mode 100644 include/igneous/data/mesh.hpp create mode 100644 include/igneous/data/space.hpp create mode 100644 include/igneous/data/structure.hpp rename include/igneous/data/{topology.hpp => structures/diffusion_geometry.hpp} (63%) create mode 100644 include/igneous/data/structures/discrete_exterior_calculus.hpp rename include/igneous/ops/{ => dec}/curvature.hpp (67%) rename include/igneous/ops/{ => dec}/flow.hpp (57%) create mode 100644 notes/structure_refactor/journal.md rename src/{main_diffusion_topology.cpp => main_diffusion_geometry.cpp} (82%) rename tests/{test_topology_triangle.cpp => test_structure_dec.cpp} (83%) rename tests/{test_topology_diffusion.cpp => test_structure_diffusion_geometry.cpp} (67%) rename visualizations/{view_main_diffusion_topology.py => view_main_diffusion_geometry.py} (88%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 65bc086..0278841 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -52,8 +52,9 @@ function(add_igneous_test name source) endfunction() add_igneous_test(test_algebra tests/test_algebra.cpp) -add_igneous_test(test_topology_triangle tests/test_topology_triangle.cpp) -add_igneous_test(test_topology_diffusion tests/test_topology_diffusion.cpp) +add_igneous_test(test_structure_dec tests/test_structure_dec.cpp) +add_igneous_test(test_structure_diffusion_geometry + tests/test_structure_diffusion_geometry.cpp) add_igneous_test(test_ops_curvature_flow tests/test_ops_curvature_flow.cpp) add_igneous_test(test_ops_spectral_geometry tests/test_ops_spectral_geometry.cpp) add_igneous_test(test_ops_hodge tests/test_ops_hodge.cpp) @@ -89,8 +90,8 @@ target_link_libraries(igneous-spectral PRIVATE igneous fmt::fmt) add_executable(igneous-hodge src/main_hodge.cpp) target_link_libraries(igneous-hodge PRIVATE igneous fmt::fmt) -add_executable(igneous-diffusion-topology src/main_diffusion_topology.cpp) -target_link_libraries(igneous-diffusion-topology PRIVATE igneous fmt::fmt) +add_executable(igneous-diffusion-geometry src/main_diffusion_geometry.cpp) +target_link_libraries(igneous-diffusion-geometry PRIVATE igneous fmt::fmt) add_test( NAME test_hodge_cli_outputs @@ -106,7 +107,7 @@ set_tests_properties(test_hodge_parity_optional PROPERTIES LABELS "hodge;parity" add_test( NAME test_diffgeo_cli_outputs COMMAND bash ${CMAKE_SOURCE_DIR}/tests/test_diffgeo_cli_outputs.sh - $) + $) set_tests_properties(test_diffgeo_cli_outputs PROPERTIES LABELS "diffgeo;cli") add_test( diff --git a/Makefile b/Makefile index f0d3dfc..394039c 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all debug release build clean test test-all test-algebra test-topology test-ops bench bench-memory bench-geometry bench-dod bench-deep run-mesh run-diffusion run-spectral run-hodge +.PHONY: all debug release build clean test test-all test-algebra test-structure test-ops bench bench-memory bench-geometry bench-dod bench-deep run-mesh run-diffusion run-spectral run-hodge all: build @@ -15,7 +15,7 @@ clean: rm -rf build test: debug - cmake --build build --target test_algebra test_topology_triangle test_topology_diffusion test_ops_curvature_flow test_ops_spectral_geometry test_ops_hodge test_io_meshes + cmake --build build --target test_algebra test_structure_dec test_structure_diffusion_geometry test_ops_curvature_flow test_ops_spectral_geometry test_ops_hodge test_io_meshes ctest --test-dir build --output-on-failure --verbose test-all: test @@ -24,10 +24,10 @@ test-algebra: debug cmake --build build --target test_algebra ./build/test_algebra -test-topology: debug - cmake --build build --target test_topology_triangle test_topology_diffusion - ./build/test_topology_triangle - ./build/test_topology_diffusion +test-structure: debug + cmake --build build --target test_structure_dec test_structure_diffusion_geometry + ./build/test_structure_dec + ./build/test_structure_diffusion_geometry test-ops: debug cmake --build build --target test_ops_curvature_flow test_ops_spectral_geometry test_ops_hodge diff --git a/README.md b/README.md index 10d8dc5..884af75 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,13 @@ [![CodeQL](https://github.com/harnesslabs/igneous/actions/workflows/codeql.yml/badge.svg)](https://github.com/harnesslabs/igneous/actions/workflows/codeql.yml) [![Release](https://github.com/harnesslabs/igneous/actions/workflows/release.yml/badge.svg)](https://github.com/harnesslabs/igneous/actions/workflows/release.yml) -Data-oriented C++23 geometry and topology engine with a throughput-first CPU pipeline. +Data-oriented C++23 geometry and structure engine with a throughput-first CPU pipeline. ## Highlights - SoA geometry storage (`x`, `y`, `z`) for cache-friendly traversal. -- Triangle topology with explicit face arrays and CSR adjacency (faces + vertex neighbors). -- Diffusion topology with k-NN graph construction (`nanoflann`) and sparse Markov chain (`Eigen`). +- `DiscreteExteriorCalculus` structure with explicit face arrays and CSR adjacency (faces + vertex neighbors). +- `DiffusionGeometry` structure with k-NN graph construction (`nanoflann`) and sparse Markov chain (`Eigen`). - Spectral and Hodge operator stack (Gram, weak exterior derivative, curl energy, Hodge spectrum). - Doctest correctness suite and Google Benchmark performance suite. @@ -44,6 +44,7 @@ cmake --build build -j8 ./build/igneous-diffusion assets/bunny.obj ./build/igneous-spectral assets/bunny.obj ./build/igneous-hodge +./build/igneous-diffusion-geometry ``` ## Visualizations @@ -56,7 +57,7 @@ python3 visualizations/view_main_mesh.py --run --open python3 visualizations/view_main_diffusion.py --run --open python3 visualizations/view_main_spectral.py --run --open python3 visualizations/view_main_hodge.py --run --open -python3 visualizations/view_main_diffusion_topology.py --run --open +python3 visualizations/view_main_diffusion_geometry.py --run --open ``` Detailed usage is documented in `visualizations/README.md`. @@ -76,7 +77,7 @@ Standard parity round: ./scripts/hodge/run_parity_round.sh ``` -## Diffusion Topology Parity Workflow +## Diffusion Geometry Parity Workflow Standard torus+sphere parity round: @@ -113,8 +114,8 @@ ctest --test-dir build --output-on-failure --verbose Current suites: - `test_algebra` -- `test_topology_triangle` -- `test_topology_diffusion` +- `test_structure_dec` +- `test_structure_diffusion_geometry` - `test_ops_curvature_flow` - `test_ops_spectral_geometry` - `test_ops_hodge` @@ -161,7 +162,7 @@ IGNEOUS_BENCH_MODE=1 IGNEOUS_BACKEND=parallel IGNEOUS_NUM_THREADS=8 \ Benchmark groups: -- `bench_mesh_topology_build` +- `bench_mesh_structure_build` - `bench_curvature_kernel` - `bench_flow_kernel` - `bench_diffusion_build` @@ -178,7 +179,7 @@ Pipeline benchmark groups: - `bench_pipeline_diffusion_main` - `bench_pipeline_spectral_main` - `bench_pipeline_hodge_main` -- `bench_hodge_phase_topology` +- `bench_hodge_phase_structure_build` - `bench_hodge_phase_eigenbasis` - `bench_hodge_phase_gram` - `bench_hodge_phase_weak_derivative` @@ -236,8 +237,7 @@ git push origin vX.Y.Z ```cpp #include -using Sig = igneous::core::Euclidean3D; -using Mesh = igneous::data::Mesh; +using Mesh = igneous::data::Space; int main() { Mesh mesh; @@ -246,11 +246,11 @@ int main() { std::vector H; std::vector K; - igneous::ops::CurvatureWorkspace curvature_ws; - igneous::ops::FlowWorkspace flow_ws; + igneous::ops::dec::CurvatureWorkspace curvature_ws; + igneous::ops::dec::FlowWorkspace flow_ws; - igneous::ops::compute_curvature_measures(mesh, H, K, curvature_ws); - igneous::ops::integrate_mean_curvature_flow(mesh, 0.01f, flow_ws); + igneous::ops::dec::compute_curvature_measures(mesh, H, K, curvature_ws); + igneous::ops::dec::integrate_mean_curvature_flow(mesh, 0.01f, flow_ws); } ``` diff --git a/benches/bench_dod.cpp b/benches/bench_dod.cpp index 022a3dc..41eafcc 100644 --- a/benches/bench_dod.cpp +++ b/benches/bench_dod.cpp @@ -7,16 +7,16 @@ #include #include -#include -#include -#include +#include +#include +#include #include #include #include using MeshSig = igneous::core::Euclidean3D; -using SurfaceMesh = igneous::data::Mesh; -using DiffusionMesh = igneous::data::Mesh; +using SurfaceMesh = igneous::data::Space; +using DiffusionMesh = igneous::data::Space; namespace { struct BenchEnvSetup { @@ -26,7 +26,7 @@ struct BenchEnvSetup { static SurfaceMesh make_grid_mesh(int side_length) { SurfaceMesh mesh; - mesh.geometry.reserve(static_cast(side_length * side_length)); + mesh.reserve(static_cast(side_length * side_length)); const float scale = 10.0f / static_cast(side_length); for (int y = 0; y < side_length; ++y) { @@ -34,11 +34,11 @@ static SurfaceMesh make_grid_mesh(int side_length) { const float px = x * scale; const float py = y * scale; const float pz = std::sin(px) + std::cos(py); - mesh.geometry.push_point({px, py, pz}); + mesh.push_point({px, py, pz}); } } - mesh.topology.faces_to_vertices.reserve( + mesh.structure.faces_to_vertices.reserve( static_cast((side_length - 1) * (side_length - 1) * 6)); for (int y = 0; y < side_length - 1; ++y) { @@ -48,24 +48,24 @@ static SurfaceMesh make_grid_mesh(int side_length) { const uint32_t i2 = static_cast((y + 1) * side_length + x); const uint32_t i3 = static_cast((y + 1) * side_length + x + 1); - mesh.topology.faces_to_vertices.push_back(i0); - mesh.topology.faces_to_vertices.push_back(i1); - mesh.topology.faces_to_vertices.push_back(i2); + mesh.structure.faces_to_vertices.push_back(i0); + mesh.structure.faces_to_vertices.push_back(i1); + mesh.structure.faces_to_vertices.push_back(i2); - mesh.topology.faces_to_vertices.push_back(i1); - mesh.topology.faces_to_vertices.push_back(i3); - mesh.topology.faces_to_vertices.push_back(i2); + mesh.structure.faces_to_vertices.push_back(i1); + mesh.structure.faces_to_vertices.push_back(i3); + mesh.structure.faces_to_vertices.push_back(i2); } } - mesh.topology.build({mesh.geometry.num_points(), true}); + mesh.structure.build({mesh.num_points(), true}); return mesh; } static DiffusionMesh make_diffusion_cloud(size_t n_points) { setenv("IGNEOUS_BENCH_MODE", "1", 1); DiffusionMesh mesh; - mesh.geometry.reserve(n_points); + mesh.reserve(n_points); std::mt19937 rng(42); std::uniform_real_distribution angle_dist(0.0f, 6.283185f); @@ -79,23 +79,23 @@ static DiffusionMesh make_diffusion_cloud(size_t n_points) { const float x = (2.0f + r * std::cos(v)) * std::cos(u); const float y = (2.0f + r * std::cos(v)) * std::sin(u); const float z = r * std::sin(v); - mesh.geometry.push_point({x, y, z}); + mesh.push_point({x, y, z}); } - mesh.topology.build({mesh.geometry.x_span(), mesh.geometry.y_span(), - mesh.geometry.z_span(), 32}); + mesh.structure.build({mesh.x_span(), mesh.y_span(), + mesh.z_span(), 32}); return mesh; } -static void bench_mesh_topology_build(benchmark::State &state) { +static void bench_mesh_structure_build(benchmark::State &state) { SurfaceMesh mesh = make_grid_mesh(static_cast(state.range(0))); for (auto _ : state) { - mesh.topology.vertex_face_offsets.clear(); - mesh.topology.vertex_face_data.clear(); - mesh.topology.vertex_neighbor_offsets.clear(); - mesh.topology.vertex_neighbor_data.clear(); - mesh.topology.build({mesh.geometry.num_points(), true}); - benchmark::DoNotOptimize(mesh.topology.vertex_neighbor_data.data()); + mesh.structure.vertex_face_offsets.clear(); + mesh.structure.vertex_face_data.clear(); + mesh.structure.vertex_neighbor_offsets.clear(); + mesh.structure.vertex_neighbor_data.clear(); + mesh.structure.build({mesh.num_points(), true}); + benchmark::DoNotOptimize(mesh.structure.vertex_neighbor_data.data()); } } @@ -103,10 +103,10 @@ static void bench_curvature_kernel(benchmark::State &state) { SurfaceMesh mesh = make_grid_mesh(static_cast(state.range(0))); std::vector H; std::vector K; - igneous::ops::CurvatureWorkspace ws; + igneous::ops::dec::CurvatureWorkspace ws; for (auto _ : state) { - igneous::ops::compute_curvature_measures(mesh, H, K, ws); + igneous::ops::dec::compute_curvature_measures(mesh, H, K, ws); benchmark::DoNotOptimize(H.data()); benchmark::DoNotOptimize(K.data()); } @@ -114,11 +114,11 @@ static void bench_curvature_kernel(benchmark::State &state) { static void bench_flow_kernel(benchmark::State &state) { SurfaceMesh mesh = make_grid_mesh(static_cast(state.range(0))); - igneous::ops::FlowWorkspace ws; + igneous::ops::dec::FlowWorkspace ws; for (auto _ : state) { - igneous::ops::integrate_mean_curvature_flow(mesh, 0.01f, ws); - benchmark::DoNotOptimize(mesh.geometry.x.data()); + igneous::ops::dec::integrate_mean_curvature_flow(mesh, 0.01f, ws); + benchmark::DoNotOptimize(mesh.x.data()); } } @@ -126,20 +126,20 @@ static void bench_diffusion_build(benchmark::State &state) { DiffusionMesh mesh = make_diffusion_cloud(static_cast(state.range(0))); for (auto _ : state) { - mesh.topology.build({mesh.geometry.x_span(), mesh.geometry.y_span(), - mesh.geometry.z_span(), 32}); - benchmark::DoNotOptimize(mesh.topology.markov_values.size()); + mesh.structure.build({mesh.x_span(), mesh.y_span(), + mesh.z_span(), 32}); + benchmark::DoNotOptimize(mesh.structure.markov_values.size()); } } static void bench_markov_step(benchmark::State &state) { DiffusionMesh mesh = make_diffusion_cloud(static_cast(state.range(0))); - Eigen::VectorXf u = Eigen::VectorXf::Ones(static_cast(mesh.geometry.num_points())); + Eigen::VectorXf u = Eigen::VectorXf::Ones(static_cast(mesh.num_points())); Eigen::VectorXf u_next = - Eigen::VectorXf::Zero(static_cast(mesh.geometry.num_points())); + Eigen::VectorXf::Zero(static_cast(mesh.num_points())); for (auto _ : state) { - igneous::ops::apply_markov_transition(mesh, u, u_next); + igneous::ops::diffusion::apply_markov_transition(mesh, u, u_next); u.swap(u_next); benchmark::DoNotOptimize(u.data()); } @@ -149,13 +149,13 @@ static void bench_markov_multi_step(benchmark::State &state) { const size_t n_points = static_cast(state.range(0)); const int steps = static_cast(state.range(1)); DiffusionMesh mesh = make_diffusion_cloud(n_points); - Eigen::VectorXf u = Eigen::VectorXf::Ones(static_cast(mesh.geometry.num_points())); + Eigen::VectorXf u = Eigen::VectorXf::Ones(static_cast(mesh.num_points())); Eigen::VectorXf u_next = - Eigen::VectorXf::Zero(static_cast(mesh.geometry.num_points())); - igneous::ops::DiffusionWorkspace ws; + Eigen::VectorXf::Zero(static_cast(mesh.num_points())); + igneous::ops::diffusion::DiffusionWorkspace ws; for (auto _ : state) { - igneous::ops::apply_markov_transition_steps(mesh, u, steps, u_next, ws); + igneous::ops::diffusion::apply_markov_transition_steps(mesh, u, steps, u_next, ws); u.swap(u_next); benchmark::DoNotOptimize(u.data()); } @@ -166,64 +166,64 @@ static void bench_eigenbasis(benchmark::State &state) { const int basis = static_cast(state.range(1)); for (auto _ : state) { - igneous::ops::compute_eigenbasis(mesh, basis); - benchmark::DoNotOptimize(mesh.topology.eigen_basis.data()); + igneous::ops::diffusion::compute_eigenbasis(mesh, basis); + benchmark::DoNotOptimize(mesh.structure.eigen_basis.data()); } } static void bench_1form_gram(benchmark::State &state) { DiffusionMesh mesh = make_diffusion_cloud(static_cast(state.range(0))); - igneous::ops::compute_eigenbasis(mesh, static_cast(state.range(1))); - igneous::ops::GeometryWorkspace ws; + igneous::ops::diffusion::compute_eigenbasis(mesh, static_cast(state.range(1))); + igneous::ops::diffusion::GeometryWorkspace ws; for (auto _ : state) { - auto G = igneous::ops::compute_1form_gram_matrix(mesh, 0.05f, ws); + auto G = igneous::ops::diffusion::compute_1form_gram_matrix(mesh, 0.05f, ws); benchmark::DoNotOptimize(G.data()); } } static void bench_weak_derivative(benchmark::State &state) { DiffusionMesh mesh = make_diffusion_cloud(static_cast(state.range(0))); - igneous::ops::compute_eigenbasis(mesh, static_cast(state.range(1))); - igneous::ops::HodgeWorkspace ws; + igneous::ops::diffusion::compute_eigenbasis(mesh, static_cast(state.range(1))); + igneous::ops::diffusion::HodgeWorkspace ws; for (auto _ : state) { - auto D = igneous::ops::compute_weak_exterior_derivative(mesh, 0.05f, ws); + auto D = igneous::ops::diffusion::compute_weak_exterior_derivative(mesh, 0.05f, ws); benchmark::DoNotOptimize(D.data()); } } static void bench_curl_energy(benchmark::State &state) { DiffusionMesh mesh = make_diffusion_cloud(static_cast(state.range(0))); - igneous::ops::compute_eigenbasis(mesh, static_cast(state.range(1))); - igneous::ops::HodgeWorkspace ws; + igneous::ops::diffusion::compute_eigenbasis(mesh, static_cast(state.range(1))); + igneous::ops::diffusion::HodgeWorkspace ws; for (auto _ : state) { - auto E = igneous::ops::compute_curl_energy_matrix(mesh, 0.05f, ws); + auto E = igneous::ops::diffusion::compute_curl_energy_matrix(mesh, 0.05f, ws); benchmark::DoNotOptimize(E.data()); } } static void bench_hodge_solve(benchmark::State &state) { DiffusionMesh mesh = make_diffusion_cloud(static_cast(state.range(0))); - igneous::ops::compute_eigenbasis(mesh, static_cast(state.range(1))); + igneous::ops::diffusion::compute_eigenbasis(mesh, static_cast(state.range(1))); - igneous::ops::GeometryWorkspace geom_ws; - igneous::ops::HodgeWorkspace hodge_ws; + igneous::ops::diffusion::GeometryWorkspace geom_ws; + igneous::ops::diffusion::HodgeWorkspace hodge_ws; - const auto G = igneous::ops::compute_1form_gram_matrix(mesh, 0.05f, geom_ws); - const auto D = igneous::ops::compute_weak_exterior_derivative(mesh, 0.05f, hodge_ws); - const auto E = igneous::ops::compute_curl_energy_matrix(mesh, 0.05f, hodge_ws); - const auto L = igneous::ops::compute_hodge_laplacian_matrix(D, E); + const auto G = igneous::ops::diffusion::compute_1form_gram_matrix(mesh, 0.05f, geom_ws); + const auto D = igneous::ops::diffusion::compute_weak_exterior_derivative(mesh, 0.05f, hodge_ws); + const auto E = igneous::ops::diffusion::compute_curl_energy_matrix(mesh, 0.05f, hodge_ws); + const auto L = igneous::ops::diffusion::compute_hodge_laplacian_matrix(D, E); for (auto _ : state) { - auto [evals, evecs] = igneous::ops::compute_hodge_spectrum(L, G); + auto [evals, evecs] = igneous::ops::diffusion::compute_hodge_spectrum(L, G); benchmark::DoNotOptimize(evals.data()); benchmark::DoNotOptimize(evecs.data()); } } -BENCHMARK(bench_mesh_topology_build)->Arg(400); +BENCHMARK(bench_mesh_structure_build)->Arg(400); BENCHMARK(bench_curvature_kernel)->Arg(400); BENCHMARK(bench_flow_kernel)->Arg(400); BENCHMARK(bench_diffusion_build)->Arg(2000); diff --git a/benches/bench_geometry.cpp b/benches/bench_geometry.cpp index 3bdbdfd..757840f 100644 --- a/benches/bench_geometry.cpp +++ b/benches/bench_geometry.cpp @@ -6,20 +6,20 @@ #include #include -#include -#include -#include -#include +#include +#include +#include +#include using namespace igneous; using Clock = std::chrono::high_resolution_clock; using Sig = core::Euclidean3D; -using Mesh = data::Mesh; +using Mesh = data::Space; static Mesh generate_benchmark_mesh(int side_length) { Mesh mesh; mesh.name = "Benchmark_Grid_" + std::to_string(side_length); - mesh.geometry.reserve(static_cast(side_length * side_length)); + mesh.reserve(static_cast(side_length * side_length)); const float scale = 10.0f / static_cast(side_length); for (int y = 0; y < side_length; ++y) { @@ -27,11 +27,11 @@ static Mesh generate_benchmark_mesh(int side_length) { const float px = x * scale; const float py = y * scale; const float pz = std::sin(px) + std::cos(py); - mesh.geometry.push_point({px, py, pz}); + mesh.push_point({px, py, pz}); } } - mesh.topology.faces_to_vertices.reserve( + mesh.structure.faces_to_vertices.reserve( static_cast((side_length - 1) * (side_length - 1) * 6)); for (int y = 0; y < side_length - 1; ++y) { @@ -41,13 +41,13 @@ static Mesh generate_benchmark_mesh(int side_length) { const uint32_t i2 = static_cast((y + 1) * side_length + x); const uint32_t i3 = static_cast((y + 1) * side_length + (x + 1)); - mesh.topology.faces_to_vertices.push_back(i0); - mesh.topology.faces_to_vertices.push_back(i1); - mesh.topology.faces_to_vertices.push_back(i2); + mesh.structure.faces_to_vertices.push_back(i0); + mesh.structure.faces_to_vertices.push_back(i1); + mesh.structure.faces_to_vertices.push_back(i2); - mesh.topology.faces_to_vertices.push_back(i1); - mesh.topology.faces_to_vertices.push_back(i3); - mesh.topology.faces_to_vertices.push_back(i2); + mesh.structure.faces_to_vertices.push_back(i1); + mesh.structure.faces_to_vertices.push_back(i3); + mesh.structure.faces_to_vertices.push_back(i2); } } @@ -58,7 +58,7 @@ struct BenchResult { std::string name; size_t verts = 0; size_t faces = 0; - double t_topology_ms = 0.0; + double t_structure_ms = 0.0; double t_curvature_ms = 0.0; double t_flow_ms = 0.0; double fps = 0.0; @@ -66,37 +66,37 @@ struct BenchResult { static BenchResult run_workload(int grid_size) { auto mesh = generate_benchmark_mesh(grid_size); - const size_t n_verts = mesh.geometry.num_points(); + const size_t n_verts = mesh.num_points(); - mesh.topology.build({n_verts, true}); + mesh.structure.build({n_verts, true}); std::vector H; std::vector K; - ops::CurvatureWorkspace curvature_ws; - ops::FlowWorkspace flow_ws; + ops::dec::CurvatureWorkspace curvature_ws; + ops::dec::FlowWorkspace flow_ws; - ops::compute_curvature_measures(mesh, H, K, curvature_ws); + ops::dec::compute_curvature_measures(mesh, H, K, curvature_ws); - mesh.topology.vertex_face_offsets.clear(); - mesh.topology.vertex_face_data.clear(); - mesh.topology.vertex_neighbor_offsets.clear(); - mesh.topology.vertex_neighbor_data.clear(); + mesh.structure.vertex_face_offsets.clear(); + mesh.structure.vertex_face_data.clear(); + mesh.structure.vertex_neighbor_offsets.clear(); + mesh.structure.vertex_neighbor_data.clear(); const auto t0 = Clock::now(); - mesh.topology.build({n_verts, true}); + mesh.structure.build({n_verts, true}); const auto t1 = Clock::now(); constexpr int iterations = 10; const auto t2 = Clock::now(); for (int i = 0; i < iterations; ++i) { - ops::compute_curvature_measures(mesh, H, K, curvature_ws); + ops::dec::compute_curvature_measures(mesh, H, K, curvature_ws); } const auto t3 = Clock::now(); const auto t4 = Clock::now(); for (int i = 0; i < iterations; ++i) { - ops::integrate_mean_curvature_flow(mesh, 0.01f, flow_ws); + ops::dec::integrate_mean_curvature_flow(mesh, 0.01f, flow_ws); } const auto t5 = Clock::now(); @@ -109,7 +109,7 @@ static BenchResult run_workload(int grid_size) { return { "Grid " + std::to_string(grid_size) + "x" + std::to_string(grid_size), n_verts, - mesh.topology.num_faces(), + mesh.structure.num_faces(), ms_topo, ms_curv, ms_flow, @@ -124,7 +124,7 @@ int main() { std::cout << " IGNEOUS GEOMETRY ENGINE BENCHMARK\n"; std::cout << "==========================================================================\n"; std::cout << std::left << std::setw(15) << "Mesh" << std::setw(12) << "Verts" - << std::setw(12) << "Faces" << std::setw(15) << "Topo (ms)" + << std::setw(12) << "Faces" << std::setw(15) << "Struct (ms)" << std::setw(15) << "Curv (ms)" << std::setw(15) << "Flow (ms)" << std::setw(10) << "Sim FPS" << "\n"; std::cout << "--------------------------------------------------------------------------\n"; @@ -133,7 +133,7 @@ int main() { const auto res = run_workload(size); std::cout << std::left << std::setw(15) << res.name << std::setw(12) << res.verts << std::setw(12) << res.faces << std::fixed - << std::setprecision(3) << std::setw(15) << res.t_topology_ms + << std::setprecision(3) << std::setw(15) << res.t_structure_ms << std::setw(15) << res.t_curvature_ms << std::setw(15) << res.t_flow_ms << std::setw(10) << res.fps << "\n"; } diff --git a/benches/bench_pipelines.cpp b/benches/bench_pipelines.cpp index ee19798..ab58ff7 100644 --- a/benches/bench_pipelines.cpp +++ b/benches/bench_pipelines.cpp @@ -12,7 +12,7 @@ #include #include -#include +#include #include #include #include @@ -20,7 +20,7 @@ #include using MeshSig = igneous::core::Euclidean3D; -using DiffusionMesh = igneous::data::Mesh; +using DiffusionMesh = igneous::data::Space; namespace { struct BenchEnvSetup { @@ -48,13 +48,13 @@ DiffusionMesh make_bunny_geometry() { } igneous::io::load_obj(mesh, bunny.string()); igneous::ops::normalize(mesh); - mesh.topology.clear(); + mesh.structure.clear(); return mesh; } void generate_torus(DiffusionMesh &mesh, size_t n_points, float R, float r) { mesh.clear(); - mesh.geometry.reserve(n_points); + mesh.reserve(n_points); std::mt19937 gen(42); std::uniform_real_distribution dist(0.0f, 6.283185f); @@ -66,15 +66,15 @@ void generate_torus(DiffusionMesh &mesh, size_t n_points, float R, float r) { const float x = (R + r * std::cos(v)) * std::cos(u); const float y = (R + r * std::cos(v)) * std::sin(u); const float z = r * std::sin(v); - mesh.geometry.push_point({x, y, z}); + mesh.push_point({x, y, z}); } } int compute_max_y_vertex(const DiffusionMesh &mesh) { float max_y = -std::numeric_limits::infinity(); int max_y_idx = 0; - for (size_t i = 0; i < mesh.geometry.num_points(); ++i) { - const auto p = mesh.geometry.get_vec3(i); + for (size_t i = 0; i < mesh.num_points(); ++i) { + const auto p = mesh.get_vec3(i); if (p.y > max_y) { max_y = p.y; max_y_idx = static_cast(i); @@ -83,10 +83,10 @@ int compute_max_y_vertex(const DiffusionMesh &mesh) { return max_y_idx; } -void build_diffusion_topology(DiffusionMesh &mesh, float /*bandwidth*/, +void build_diffusion_geometry(DiffusionMesh &mesh, float /*bandwidth*/, int k_neighbors) { - mesh.topology.build({mesh.geometry.x_span(), mesh.geometry.y_span(), - mesh.geometry.z_span(), k_neighbors}); + mesh.structure.build({mesh.x_span(), mesh.y_span(), + mesh.z_span(), k_neighbors}); } void bench_pipeline_diffusion_main(benchmark::State &state) { @@ -102,15 +102,15 @@ void bench_pipeline_diffusion_main(benchmark::State &state) { DiffusionMesh mesh = base_mesh; const int max_y_idx = compute_max_y_vertex(mesh); const int steps = static_cast(state.range(0)); - Eigen::VectorXf u = Eigen::VectorXf::Zero(static_cast(mesh.geometry.num_points())); - Eigen::VectorXf u_next = Eigen::VectorXf::Zero(static_cast(mesh.geometry.num_points())); - igneous::ops::DiffusionWorkspace diffusion_ws; + Eigen::VectorXf u = Eigen::VectorXf::Zero(static_cast(mesh.num_points())); + Eigen::VectorXf u_next = Eigen::VectorXf::Zero(static_cast(mesh.num_points())); + igneous::ops::diffusion::DiffusionWorkspace diffusion_ws; for (auto _ : state) { - build_diffusion_topology(mesh, kBandwidth, kNeighbors); + build_diffusion_geometry(mesh, kBandwidth, kNeighbors); u.setZero(); u[max_y_idx] = 1000.0f; - igneous::ops::apply_markov_transition_steps(mesh, u, steps, u_next, diffusion_ws); + igneous::ops::diffusion::apply_markov_transition_steps(mesh, u, steps, u_next, diffusion_ws); u.swap(u_next); benchmark::DoNotOptimize(u.data()); @@ -129,13 +129,13 @@ void bench_pipeline_spectral_main(benchmark::State &state) { constexpr int kBasis = 16; DiffusionMesh mesh = base_mesh; - igneous::ops::GeometryWorkspace geometry_ws; + igneous::ops::diffusion::GeometryWorkspace geometry_ws; for (auto _ : state) { - build_diffusion_topology(mesh, kBandwidth, kNeighbors); - igneous::ops::compute_eigenbasis(mesh, kBasis); + build_diffusion_geometry(mesh, kBandwidth, kNeighbors); + igneous::ops::diffusion::compute_eigenbasis(mesh, kBasis); const Eigen::MatrixXf G = - igneous::ops::compute_1form_gram_matrix(mesh, kBandwidth, geometry_ws); + igneous::ops::diffusion::compute_1form_gram_matrix(mesh, kBandwidth, geometry_ws); Eigen::SelfAdjointEigenSolver solver(G); benchmark::DoNotOptimize(solver.eigenvalues().data()); } @@ -149,26 +149,26 @@ void bench_pipeline_hodge_main(benchmark::State &state) { DiffusionMesh mesh; generate_torus(mesh, 4000, 2.0f, 0.8f); - igneous::ops::GeometryWorkspace geom_ws; - igneous::ops::HodgeWorkspace hodge_ws; + igneous::ops::diffusion::GeometryWorkspace geom_ws; + igneous::ops::diffusion::HodgeWorkspace hodge_ws; for (auto _ : state) { - build_diffusion_topology(mesh, kBandwidth, kNeighbors); - igneous::ops::compute_eigenbasis(mesh, kBasis); + build_diffusion_geometry(mesh, kBandwidth, kNeighbors); + igneous::ops::diffusion::compute_eigenbasis(mesh, kBasis); const Eigen::MatrixXf G = - igneous::ops::compute_1form_gram_matrix(mesh, kBandwidth, geom_ws); + igneous::ops::diffusion::compute_1form_gram_matrix(mesh, kBandwidth, geom_ws); const Eigen::MatrixXf D = - igneous::ops::compute_weak_exterior_derivative(mesh, kBandwidth, hodge_ws); + igneous::ops::diffusion::compute_weak_exterior_derivative(mesh, kBandwidth, hodge_ws); const Eigen::MatrixXf E = - igneous::ops::compute_curl_energy_matrix(mesh, kBandwidth, hodge_ws); - const Eigen::MatrixXf L = igneous::ops::compute_hodge_laplacian_matrix(D, E); - auto [evals, evecs] = igneous::ops::compute_hodge_spectrum(L, G); + igneous::ops::diffusion::compute_curl_energy_matrix(mesh, kBandwidth, hodge_ws); + const Eigen::MatrixXf L = igneous::ops::diffusion::compute_hodge_laplacian_matrix(D, E); + auto [evals, evecs] = igneous::ops::diffusion::compute_hodge_spectrum(L, G); const Eigen::VectorXf theta_0 = - igneous::ops::compute_circular_coordinates(mesh, evecs.col(0), kBandwidth); + igneous::ops::diffusion::compute_circular_coordinates(mesh, evecs.col(0), kBandwidth); const Eigen::VectorXf theta_1 = - igneous::ops::compute_circular_coordinates(mesh, evecs.col(1), kBandwidth); + igneous::ops::diffusion::compute_circular_coordinates(mesh, evecs.col(1), kBandwidth); benchmark::DoNotOptimize(evals.data()); benchmark::DoNotOptimize(theta_0.data()); @@ -176,15 +176,15 @@ void bench_pipeline_hodge_main(benchmark::State &state) { } } -void bench_hodge_phase_topology(benchmark::State &state) { +void bench_hodge_phase_structure_build(benchmark::State &state) { constexpr float kBandwidth = 0.05f; constexpr int kNeighbors = 32; DiffusionMesh mesh; generate_torus(mesh, 4000, 2.0f, 0.8f); for (auto _ : state) { - build_diffusion_topology(mesh, kBandwidth, kNeighbors); - benchmark::DoNotOptimize(mesh.topology.markov_values.size()); + build_diffusion_geometry(mesh, kBandwidth, kNeighbors); + benchmark::DoNotOptimize(mesh.structure.markov_values.size()); } } @@ -194,11 +194,11 @@ void bench_hodge_phase_eigenbasis(benchmark::State &state) { constexpr int kBasis = 64; DiffusionMesh mesh; generate_torus(mesh, 4000, 2.0f, 0.8f); - build_diffusion_topology(mesh, kBandwidth, kNeighbors); + build_diffusion_geometry(mesh, kBandwidth, kNeighbors); for (auto _ : state) { - igneous::ops::compute_eigenbasis(mesh, kBasis); - benchmark::DoNotOptimize(mesh.topology.eigen_basis.data()); + igneous::ops::diffusion::compute_eigenbasis(mesh, kBasis); + benchmark::DoNotOptimize(mesh.structure.eigen_basis.data()); } } @@ -208,13 +208,13 @@ void bench_hodge_phase_gram(benchmark::State &state) { constexpr int kBasis = 64; DiffusionMesh mesh; generate_torus(mesh, 4000, 2.0f, 0.8f); - build_diffusion_topology(mesh, kBandwidth, kNeighbors); - igneous::ops::compute_eigenbasis(mesh, kBasis); - igneous::ops::GeometryWorkspace geom_ws; + build_diffusion_geometry(mesh, kBandwidth, kNeighbors); + igneous::ops::diffusion::compute_eigenbasis(mesh, kBasis); + igneous::ops::diffusion::GeometryWorkspace geom_ws; for (auto _ : state) { const Eigen::MatrixXf G = - igneous::ops::compute_1form_gram_matrix(mesh, kBandwidth, geom_ws); + igneous::ops::diffusion::compute_1form_gram_matrix(mesh, kBandwidth, geom_ws); benchmark::DoNotOptimize(G.data()); } } @@ -225,13 +225,13 @@ void bench_hodge_phase_weak_derivative(benchmark::State &state) { constexpr int kBasis = 64; DiffusionMesh mesh; generate_torus(mesh, 4000, 2.0f, 0.8f); - build_diffusion_topology(mesh, kBandwidth, kNeighbors); - igneous::ops::compute_eigenbasis(mesh, kBasis); - igneous::ops::HodgeWorkspace hodge_ws; + build_diffusion_geometry(mesh, kBandwidth, kNeighbors); + igneous::ops::diffusion::compute_eigenbasis(mesh, kBasis); + igneous::ops::diffusion::HodgeWorkspace hodge_ws; for (auto _ : state) { const Eigen::MatrixXf D = - igneous::ops::compute_weak_exterior_derivative(mesh, kBandwidth, hodge_ws); + igneous::ops::diffusion::compute_weak_exterior_derivative(mesh, kBandwidth, hodge_ws); benchmark::DoNotOptimize(D.data()); } } @@ -242,13 +242,13 @@ void bench_hodge_phase_curl_energy(benchmark::State &state) { constexpr int kBasis = 64; DiffusionMesh mesh; generate_torus(mesh, 4000, 2.0f, 0.8f); - build_diffusion_topology(mesh, kBandwidth, kNeighbors); - igneous::ops::compute_eigenbasis(mesh, kBasis); - igneous::ops::HodgeWorkspace hodge_ws; + build_diffusion_geometry(mesh, kBandwidth, kNeighbors); + igneous::ops::diffusion::compute_eigenbasis(mesh, kBasis); + igneous::ops::diffusion::HodgeWorkspace hodge_ws; for (auto _ : state) { const Eigen::MatrixXf E = - igneous::ops::compute_curl_energy_matrix(mesh, kBandwidth, hodge_ws); + igneous::ops::diffusion::compute_curl_energy_matrix(mesh, kBandwidth, hodge_ws); benchmark::DoNotOptimize(E.data()); } } @@ -259,21 +259,21 @@ void bench_hodge_phase_solve(benchmark::State &state) { constexpr int kBasis = 64; DiffusionMesh mesh; generate_torus(mesh, 4000, 2.0f, 0.8f); - build_diffusion_topology(mesh, kBandwidth, kNeighbors); - igneous::ops::compute_eigenbasis(mesh, kBasis); + build_diffusion_geometry(mesh, kBandwidth, kNeighbors); + igneous::ops::diffusion::compute_eigenbasis(mesh, kBasis); - igneous::ops::GeometryWorkspace geom_ws; - igneous::ops::HodgeWorkspace hodge_ws; + igneous::ops::diffusion::GeometryWorkspace geom_ws; + igneous::ops::diffusion::HodgeWorkspace hodge_ws; const Eigen::MatrixXf G = - igneous::ops::compute_1form_gram_matrix(mesh, kBandwidth, geom_ws); + igneous::ops::diffusion::compute_1form_gram_matrix(mesh, kBandwidth, geom_ws); const Eigen::MatrixXf D = - igneous::ops::compute_weak_exterior_derivative(mesh, kBandwidth, hodge_ws); + igneous::ops::diffusion::compute_weak_exterior_derivative(mesh, kBandwidth, hodge_ws); const Eigen::MatrixXf E = - igneous::ops::compute_curl_energy_matrix(mesh, kBandwidth, hodge_ws); - const Eigen::MatrixXf L = igneous::ops::compute_hodge_laplacian_matrix(D, E); + igneous::ops::diffusion::compute_curl_energy_matrix(mesh, kBandwidth, hodge_ws); + const Eigen::MatrixXf L = igneous::ops::diffusion::compute_hodge_laplacian_matrix(D, E); for (auto _ : state) { - auto [evals, evecs] = igneous::ops::compute_hodge_spectrum(L, G); + auto [evals, evecs] = igneous::ops::diffusion::compute_hodge_spectrum(L, G); benchmark::DoNotOptimize(evals.data()); benchmark::DoNotOptimize(evecs.data()); } @@ -285,25 +285,25 @@ void bench_hodge_phase_circular(benchmark::State &state) { constexpr int kBasis = 64; DiffusionMesh mesh; generate_torus(mesh, 4000, 2.0f, 0.8f); - build_diffusion_topology(mesh, kBandwidth, kNeighbors); - igneous::ops::compute_eigenbasis(mesh, kBasis); + build_diffusion_geometry(mesh, kBandwidth, kNeighbors); + igneous::ops::diffusion::compute_eigenbasis(mesh, kBasis); - igneous::ops::GeometryWorkspace geom_ws; - igneous::ops::HodgeWorkspace hodge_ws; + igneous::ops::diffusion::GeometryWorkspace geom_ws; + igneous::ops::diffusion::HodgeWorkspace hodge_ws; const Eigen::MatrixXf G = - igneous::ops::compute_1form_gram_matrix(mesh, kBandwidth, geom_ws); + igneous::ops::diffusion::compute_1form_gram_matrix(mesh, kBandwidth, geom_ws); const Eigen::MatrixXf D = - igneous::ops::compute_weak_exterior_derivative(mesh, kBandwidth, hodge_ws); + igneous::ops::diffusion::compute_weak_exterior_derivative(mesh, kBandwidth, hodge_ws); const Eigen::MatrixXf E = - igneous::ops::compute_curl_energy_matrix(mesh, kBandwidth, hodge_ws); - const Eigen::MatrixXf L = igneous::ops::compute_hodge_laplacian_matrix(D, E); - auto [evals, evecs] = igneous::ops::compute_hodge_spectrum(L, G); + igneous::ops::diffusion::compute_curl_energy_matrix(mesh, kBandwidth, hodge_ws); + const Eigen::MatrixXf L = igneous::ops::diffusion::compute_hodge_laplacian_matrix(D, E); + auto [evals, evecs] = igneous::ops::diffusion::compute_hodge_spectrum(L, G); for (auto _ : state) { const Eigen::VectorXf theta_0 = - igneous::ops::compute_circular_coordinates(mesh, evecs.col(0), kBandwidth); + igneous::ops::diffusion::compute_circular_coordinates(mesh, evecs.col(0), kBandwidth); const Eigen::VectorXf theta_1 = - igneous::ops::compute_circular_coordinates(mesh, evecs.col(1), kBandwidth); + igneous::ops::diffusion::compute_circular_coordinates(mesh, evecs.col(1), kBandwidth); benchmark::DoNotOptimize(theta_0.data()); benchmark::DoNotOptimize(theta_1.data()); benchmark::DoNotOptimize(evals.data()); @@ -315,7 +315,7 @@ BENCHMARK(bench_pipeline_diffusion_main)->Arg(20)->Arg(100); BENCHMARK(bench_pipeline_spectral_main); BENCHMARK(bench_pipeline_hodge_main); -BENCHMARK(bench_hodge_phase_topology); +BENCHMARK(bench_hodge_phase_structure_build); BENCHMARK(bench_hodge_phase_eigenbasis); BENCHMARK(bench_hodge_phase_gram); BENCHMARK(bench_hodge_phase_weak_derivative); diff --git a/include/igneous/data/buffers.hpp b/include/igneous/data/buffers.hpp deleted file mode 100644 index 0edb254..0000000 --- a/include/igneous/data/buffers.hpp +++ /dev/null @@ -1,66 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include - -namespace igneous::data { - -template struct GeometryBuffer { - static_assert(Sig::dim >= 3, "GeometryBuffer requires at least 3 dimensions"); - - std::vector x; - std::vector y; - std::vector z; - - [[nodiscard]] size_t num_points() const { return x.size(); } - - void reserve(size_t vertices) { - x.reserve(vertices); - y.reserve(vertices); - z.reserve(vertices); - } - - void resize(size_t vertices) { - x.resize(vertices); - y.resize(vertices); - z.resize(vertices); - } - - void clear() { - x.clear(); - y.clear(); - z.clear(); - } - - [[nodiscard]] core::Vec3 get_vec3(size_t i) const { return {x[i], y[i], z[i]}; } - - void set_vec3(size_t i, const core::Vec3 &v) { - x[i] = v.x; - y[i] = v.y; - z[i] = v.z; - } - - void push_point(const core::Vec3 &v) { - x.push_back(v.x); - y.push_back(v.y); - z.push_back(v.z); - } - - [[nodiscard]] std::span x_span() const { return x; } - [[nodiscard]] std::span y_span() const { return y; } - [[nodiscard]] std::span z_span() const { return z; } - - [[nodiscard]] std::span x_span() { return x; } - [[nodiscard]] std::span y_span() { return y; } - [[nodiscard]] std::span z_span() { return z; } - - [[nodiscard]] std::array, 3> xyz_spans() const { - return {x_span(), y_span(), z_span()}; - } -}; - -} // namespace igneous::data diff --git a/include/igneous/data/mesh.hpp b/include/igneous/data/mesh.hpp deleted file mode 100644 index 48b301f..0000000 --- a/include/igneous/data/mesh.hpp +++ /dev/null @@ -1,37 +0,0 @@ -#pragma once - -#include - -#include -#include - -namespace igneous::data { - -template struct Mesh { - using Signature = Sig; - using TopologyType = Topo; - - data::GeometryBuffer geometry; - Topo topology; - std::string name; - - [[nodiscard]] bool is_valid() const { - if (geometry.num_points() == 0) { - return false; - } - - if constexpr (SurfaceTopology) { - return topology.num_faces() > 0; - } - - return true; - } - - void clear() { - geometry.clear(); - topology.clear(); - name.clear(); - } -}; - -} // namespace igneous::data diff --git a/include/igneous/data/space.hpp b/include/igneous/data/space.hpp new file mode 100644 index 0000000..4456021 --- /dev/null +++ b/include/igneous/data/space.hpp @@ -0,0 +1,86 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace igneous::data { + +template struct Space { + using StructureType = StructureT; + + std::vector x; + std::vector y; + std::vector z; + + StructureT structure; + std::string name; + + [[nodiscard]] size_t num_points() const { return x.size(); } + + void reserve(size_t vertices) { + x.reserve(vertices); + y.reserve(vertices); + z.reserve(vertices); + } + + void resize(size_t vertices) { + x.resize(vertices); + y.resize(vertices); + z.resize(vertices); + } + + [[nodiscard]] core::Vec3 get_vec3(size_t i) const { return {x[i], y[i], z[i]}; } + + void set_vec3(size_t i, const core::Vec3 &v) { + x[i] = v.x; + y[i] = v.y; + z[i] = v.z; + } + + void push_point(const core::Vec3 &v) { + x.push_back(v.x); + y.push_back(v.y); + z.push_back(v.z); + } + + [[nodiscard]] std::span x_span() const { return x; } + [[nodiscard]] std::span y_span() const { return y; } + [[nodiscard]] std::span z_span() const { return z; } + + [[nodiscard]] std::span x_span() { return x; } + [[nodiscard]] std::span y_span() { return y; } + [[nodiscard]] std::span z_span() { return z; } + + [[nodiscard]] std::array, 3> xyz_spans() const { + return {x_span(), y_span(), z_span()}; + } + + [[nodiscard]] bool is_valid() const { + if (num_points() == 0) { + return false; + } + + if constexpr (SurfaceStructure) { + return structure.num_faces() > 0; + } + + return true; + } + + void clear() { + x.clear(); + y.clear(); + z.clear(); + structure.clear(); + name.clear(); + } +}; + +} // namespace igneous::data diff --git a/include/igneous/data/structure.hpp b/include/igneous/data/structure.hpp new file mode 100644 index 0000000..763b6fa --- /dev/null +++ b/include/igneous/data/structure.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace igneous::data { + +template +concept Structure = requires(T &t, const T &ct, uint32_t idx) { + typename T::Input; + { T::DIMENSION } -> std::convertible_to; + { t.build(std::declval()) } -> std::same_as; + { t.clear() } -> std::same_as; + { ct.get_neighborhood(idx) } -> std::convertible_to>; +}; + +template +concept SurfaceStructure = + Structure && requires(T &t, const T &ct, size_t f_idx, size_t v_idx, int corner) { + { ct.num_faces() } -> std::convertible_to; + { ct.get_vertex_for_face(f_idx, corner) } -> std::convertible_to; + { ct.get_faces_for_vertex(v_idx) } -> std::convertible_to>; + { ct.get_vertex_neighbors(v_idx) } -> std::convertible_to>; + { t.faces_to_vertices } -> std::same_as &>; + { ct.faces_to_vertices } -> std::same_as &>; + }; + +} // namespace igneous::data diff --git a/include/igneous/data/topology.hpp b/include/igneous/data/structures/diffusion_geometry.hpp similarity index 63% rename from include/igneous/data/topology.hpp rename to include/igneous/data/structures/diffusion_geometry.hpp index f4c3f5e..e55995d 100644 --- a/include/igneous/data/topology.hpp +++ b/include/igneous/data/structures/diffusion_geometry.hpp @@ -15,305 +15,11 @@ #include #include +#include namespace igneous::data { -template -concept Topology = requires(T &t, const T &ct, uint32_t idx) { - typename T::Input; - { T::DIMENSION } -> std::convertible_to; - { t.build(std::declval()) } -> std::same_as; - { t.clear() } -> std::same_as; - { ct.get_neighborhood(idx) } -> std::convertible_to>; -}; - -template -concept SurfaceTopology = - Topology && requires(T &t, const T &ct, size_t f_idx, size_t v_idx, int corner) { - { ct.num_faces() } -> std::convertible_to; - { ct.get_vertex_for_face(f_idx, corner) } -> std::convertible_to; - { ct.get_faces_for_vertex(v_idx) } -> std::convertible_to>; - { ct.get_vertex_neighbors(v_idx) } -> std::convertible_to>; - { t.faces_to_vertices } -> std::same_as &>; - { ct.faces_to_vertices } -> std::same_as &>; - }; - -struct TriangleTopology { - static constexpr int DIMENSION = 2; - - std::vector faces_to_vertices; - - std::vector face_v0; - std::vector face_v1; - std::vector face_v2; - - std::vector vertex_face_offsets; - std::vector vertex_face_data; - - std::vector vertex_neighbor_offsets; - std::vector vertex_neighbor_data; - - struct Input { - size_t num_vertices = 0; - bool build_vertex_neighbors = true; - }; - - [[nodiscard]] size_t num_faces() const { - if (!face_v0.empty()) { - return face_v0.size(); - } - return faces_to_vertices.size() / 3; - } - - [[nodiscard]] uint32_t get_vertex_for_face(size_t face_idx, int corner) const { - switch (corner) { - case 0: - return face_v0[face_idx]; - case 1: - return face_v1[face_idx]; - default: - return face_v2[face_idx]; - } - } - - [[nodiscard]] std::span get_faces_for_vertex(uint32_t vertex_idx) const { - if (vertex_idx + 1 >= vertex_face_offsets.size()) { - return {}; - } - - const uint32_t begin = vertex_face_offsets[vertex_idx]; - const uint32_t end = vertex_face_offsets[vertex_idx + 1]; - return {&vertex_face_data[begin], end - begin}; - } - - [[nodiscard]] std::span get_vertex_neighbors(uint32_t vertex_idx) const { - if (vertex_idx + 1 >= vertex_neighbor_offsets.size()) { - return {}; - } - - const uint32_t begin = vertex_neighbor_offsets[vertex_idx]; - const uint32_t end = vertex_neighbor_offsets[vertex_idx + 1]; - return {&vertex_neighbor_data[begin], end - begin}; - } - - [[nodiscard]] std::span get_neighborhood(uint32_t vertex_idx) const { - return get_faces_for_vertex(vertex_idx); - } - - void build(Input input) { - const size_t num_vertices = input.num_vertices; - - if (!faces_to_vertices.empty()) { - const size_t n_faces = faces_to_vertices.size() / 3; - face_v0.resize(n_faces); - face_v1.resize(n_faces); - face_v2.resize(n_faces); - - core::parallel_for_index( - 0, static_cast(n_faces), - [&](int face_idx) { - const size_t f = static_cast(face_idx); - const size_t base = f * 3; - face_v0[f] = faces_to_vertices[base + 0]; - face_v1[f] = faces_to_vertices[base + 1]; - face_v2[f] = faces_to_vertices[base + 2]; - }, - 32768); - } - - const size_t n_faces = num_faces(); - - vertex_face_offsets.assign(num_vertices + 1, 0); - for (size_t f = 0; f < n_faces; ++f) { - const uint32_t a = face_v0[f]; - const uint32_t b = face_v1[f]; - const uint32_t c = face_v2[f]; - - if (a < num_vertices) - vertex_face_offsets[a + 1]++; - if (b < num_vertices) - vertex_face_offsets[b + 1]++; - if (c < num_vertices) - vertex_face_offsets[c + 1]++; - } - - for (size_t i = 0; i < num_vertices; ++i) { - vertex_face_offsets[i + 1] += vertex_face_offsets[i]; - } - - vertex_face_data.resize(n_faces * 3); - std::vector face_write_head = vertex_face_offsets; - - for (uint32_t f = 0; f < n_faces; ++f) { - const uint32_t corners[3] = {face_v0[f], face_v1[f], face_v2[f]}; - for (uint32_t v : corners) { - if (v < num_vertices) { - const uint32_t pos = face_write_head[v]++; - vertex_face_data[pos] = f; - } - } - } - - if (!input.build_vertex_neighbors) { - vertex_neighbor_offsets.clear(); - vertex_neighbor_data.clear(); - return; - } - - vertex_neighbor_offsets.assign(num_vertices + 1, 0); - - constexpr size_t kSmallMeshNeighborThreshold = 50000; - if (num_vertices < kSmallMeshNeighborThreshold) { - std::vector seen_neighbor_stamp(num_vertices, 0); - uint32_t stamp = 1; - - for (uint32_t v = 0; v < num_vertices; ++v) { - if (stamp == 0) { - std::fill(seen_neighbor_stamp.begin(), seen_neighbor_stamp.end(), 0); - stamp = 1; - } - - uint32_t count = 0; - const uint32_t face_begin = vertex_face_offsets[v]; - const uint32_t face_end = vertex_face_offsets[v + 1]; - for (uint32_t idx = face_begin; idx < face_end; ++idx) { - const uint32_t f = vertex_face_data[idx]; - const uint32_t corners[3] = {face_v0[f], face_v1[f], face_v2[f]}; - for (uint32_t u : corners) { - if (u >= num_vertices || u == v || seen_neighbor_stamp[u] == stamp) { - continue; - } - seen_neighbor_stamp[u] = stamp; - ++count; - } - } - - vertex_neighbor_offsets[v + 1] = count; - ++stamp; - } - - for (size_t i = 0; i < num_vertices; ++i) { - vertex_neighbor_offsets[i + 1] += vertex_neighbor_offsets[i]; - } - - vertex_neighbor_data.resize(vertex_neighbor_offsets[num_vertices]); - - std::fill(seen_neighbor_stamp.begin(), seen_neighbor_stamp.end(), 0); - stamp = 1; - for (uint32_t v = 0; v < num_vertices; ++v) { - if (stamp == 0) { - std::fill(seen_neighbor_stamp.begin(), seen_neighbor_stamp.end(), 0); - stamp = 1; - } - - uint32_t write = vertex_neighbor_offsets[v]; - const uint32_t face_begin = vertex_face_offsets[v]; - const uint32_t face_end = vertex_face_offsets[v + 1]; - for (uint32_t idx = face_begin; idx < face_end; ++idx) { - const uint32_t f = vertex_face_data[idx]; - const uint32_t corners[3] = {face_v0[f], face_v1[f], face_v2[f]}; - for (uint32_t u : corners) { - if (u >= num_vertices || u == v || seen_neighbor_stamp[u] == stamp) { - continue; - } - seen_neighbor_stamp[u] = stamp; - vertex_neighbor_data[write++] = u; - } - } - ++stamp; - } - return; - } - - const auto gather_unique_neighbors = [&](uint32_t v, std::vector &neighbors) { - neighbors.clear(); - const uint32_t face_begin = vertex_face_offsets[v]; - const uint32_t face_end = vertex_face_offsets[v + 1]; - - const auto try_push = [&](uint32_t u) { - if (u >= num_vertices || u == v) { - return; - } - for (uint32_t existing : neighbors) { - if (existing == u) { - return; - } - } - neighbors.push_back(u); - }; - - const size_t candidate_hint = - static_cast(std::max(1, face_end - face_begin) * 2); - if (neighbors.capacity() < candidate_hint) { - neighbors.reserve(candidate_hint); - } - - for (uint32_t idx = face_begin; idx < face_end; ++idx) { - const uint32_t f = vertex_face_data[idx]; - try_push(face_v0[f]); - try_push(face_v1[f]); - try_push(face_v2[f]); - } - }; - - core::parallel_for_index( - 0, static_cast(num_vertices), - [&](int vertex_idx) { - const uint32_t v = static_cast(vertex_idx); - thread_local std::vector neighbors; - gather_unique_neighbors(v, neighbors); - vertex_neighbor_offsets[static_cast(v) + 1] = - static_cast(neighbors.size()); - }, - 32768); - - for (size_t i = 0; i < num_vertices; ++i) { - vertex_neighbor_offsets[i + 1] += vertex_neighbor_offsets[i]; - } - - vertex_neighbor_data.resize(vertex_neighbor_offsets[num_vertices]); - - core::parallel_for_index( - 0, static_cast(num_vertices), - [&](int vertex_idx) { - const uint32_t v = static_cast(vertex_idx); - thread_local std::vector neighbors; - gather_unique_neighbors(v, neighbors); - - uint32_t write = vertex_neighbor_offsets[v]; - for (uint32_t u : neighbors) { - vertex_neighbor_data[write++] = u; - } - }, - 32768); - } - - void clear() { - faces_to_vertices.clear(); - face_v0.clear(); - face_v1.clear(); - face_v2.clear(); - vertex_face_offsets.clear(); - vertex_face_data.clear(); - vertex_neighbor_offsets.clear(); - vertex_neighbor_data.clear(); - } -}; - -struct PointTopology { - static constexpr int DIMENSION = 0; - - struct Input {}; - - [[nodiscard]] std::span get_neighborhood(uint32_t) const { - return {}; - } - - void build(Input) {} - void clear() {} -}; - -struct DiffusionTopology { +struct DiffusionGeometry { static constexpr int DIMENSION = 0; struct Input { @@ -754,9 +460,7 @@ struct DiffusionTopology { } }; -static_assert(Topology); -static_assert(Topology); -static_assert(SurfaceTopology); -static_assert(!SurfaceTopology); +static_assert(Structure); +static_assert(!SurfaceStructure); } // namespace igneous::data diff --git a/include/igneous/data/structures/discrete_exterior_calculus.hpp b/include/igneous/data/structures/discrete_exterior_calculus.hpp new file mode 100644 index 0000000..4d89a2b --- /dev/null +++ b/include/igneous/data/structures/discrete_exterior_calculus.hpp @@ -0,0 +1,278 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include + +namespace igneous::data { + +struct DiscreteExteriorCalculus { + static constexpr int DIMENSION = 2; + + std::vector faces_to_vertices; + + std::vector face_v0; + std::vector face_v1; + std::vector face_v2; + + std::vector vertex_face_offsets; + std::vector vertex_face_data; + + std::vector vertex_neighbor_offsets; + std::vector vertex_neighbor_data; + + struct Input { + size_t num_vertices = 0; + bool build_vertex_neighbors = true; + }; + + [[nodiscard]] size_t num_faces() const { + if (!face_v0.empty()) { + return face_v0.size(); + } + return faces_to_vertices.size() / 3; + } + + [[nodiscard]] uint32_t get_vertex_for_face(size_t face_idx, int corner) const { + switch (corner) { + case 0: + return face_v0[face_idx]; + case 1: + return face_v1[face_idx]; + default: + return face_v2[face_idx]; + } + } + + [[nodiscard]] std::span get_faces_for_vertex(uint32_t vertex_idx) const { + if (vertex_idx + 1 >= vertex_face_offsets.size()) { + return {}; + } + + const uint32_t begin = vertex_face_offsets[vertex_idx]; + const uint32_t end = vertex_face_offsets[vertex_idx + 1]; + return {&vertex_face_data[begin], end - begin}; + } + + [[nodiscard]] std::span get_vertex_neighbors(uint32_t vertex_idx) const { + if (vertex_idx + 1 >= vertex_neighbor_offsets.size()) { + return {}; + } + + const uint32_t begin = vertex_neighbor_offsets[vertex_idx]; + const uint32_t end = vertex_neighbor_offsets[vertex_idx + 1]; + return {&vertex_neighbor_data[begin], end - begin}; + } + + [[nodiscard]] std::span get_neighborhood(uint32_t vertex_idx) const { + return get_faces_for_vertex(vertex_idx); + } + + void build(Input input) { + const size_t num_vertices = input.num_vertices; + + if (!faces_to_vertices.empty()) { + const size_t n_faces = faces_to_vertices.size() / 3; + face_v0.resize(n_faces); + face_v1.resize(n_faces); + face_v2.resize(n_faces); + + core::parallel_for_index( + 0, static_cast(n_faces), + [&](int face_idx) { + const size_t f = static_cast(face_idx); + const size_t base = f * 3; + face_v0[f] = faces_to_vertices[base + 0]; + face_v1[f] = faces_to_vertices[base + 1]; + face_v2[f] = faces_to_vertices[base + 2]; + }, + 32768); + } + + const size_t n_faces = num_faces(); + + vertex_face_offsets.assign(num_vertices + 1, 0); + for (size_t f = 0; f < n_faces; ++f) { + const uint32_t a = face_v0[f]; + const uint32_t b = face_v1[f]; + const uint32_t c = face_v2[f]; + + if (a < num_vertices) + vertex_face_offsets[a + 1]++; + if (b < num_vertices) + vertex_face_offsets[b + 1]++; + if (c < num_vertices) + vertex_face_offsets[c + 1]++; + } + + for (size_t i = 0; i < num_vertices; ++i) { + vertex_face_offsets[i + 1] += vertex_face_offsets[i]; + } + + vertex_face_data.resize(n_faces * 3); + std::vector face_write_head = vertex_face_offsets; + + for (uint32_t f = 0; f < n_faces; ++f) { + const uint32_t corners[3] = {face_v0[f], face_v1[f], face_v2[f]}; + for (uint32_t v : corners) { + if (v < num_vertices) { + const uint32_t pos = face_write_head[v]++; + vertex_face_data[pos] = f; + } + } + } + + if (!input.build_vertex_neighbors) { + vertex_neighbor_offsets.clear(); + vertex_neighbor_data.clear(); + return; + } + + vertex_neighbor_offsets.assign(num_vertices + 1, 0); + + constexpr size_t kSmallMeshNeighborThreshold = 50000; + if (num_vertices < kSmallMeshNeighborThreshold) { + std::vector seen_neighbor_stamp(num_vertices, 0); + uint32_t stamp = 1; + + for (uint32_t v = 0; v < num_vertices; ++v) { + if (stamp == 0) { + std::fill(seen_neighbor_stamp.begin(), seen_neighbor_stamp.end(), 0); + stamp = 1; + } + + uint32_t count = 0; + const uint32_t face_begin = vertex_face_offsets[v]; + const uint32_t face_end = vertex_face_offsets[v + 1]; + for (uint32_t idx = face_begin; idx < face_end; ++idx) { + const uint32_t f = vertex_face_data[idx]; + const uint32_t corners[3] = {face_v0[f], face_v1[f], face_v2[f]}; + for (uint32_t u : corners) { + if (u >= num_vertices || u == v || seen_neighbor_stamp[u] == stamp) { + continue; + } + seen_neighbor_stamp[u] = stamp; + ++count; + } + } + + vertex_neighbor_offsets[v + 1] = count; + ++stamp; + } + + for (size_t i = 0; i < num_vertices; ++i) { + vertex_neighbor_offsets[i + 1] += vertex_neighbor_offsets[i]; + } + + vertex_neighbor_data.resize(vertex_neighbor_offsets[num_vertices]); + + std::fill(seen_neighbor_stamp.begin(), seen_neighbor_stamp.end(), 0); + stamp = 1; + for (uint32_t v = 0; v < num_vertices; ++v) { + if (stamp == 0) { + std::fill(seen_neighbor_stamp.begin(), seen_neighbor_stamp.end(), 0); + stamp = 1; + } + + uint32_t write = vertex_neighbor_offsets[v]; + const uint32_t face_begin = vertex_face_offsets[v]; + const uint32_t face_end = vertex_face_offsets[v + 1]; + for (uint32_t idx = face_begin; idx < face_end; ++idx) { + const uint32_t f = vertex_face_data[idx]; + const uint32_t corners[3] = {face_v0[f], face_v1[f], face_v2[f]}; + for (uint32_t u : corners) { + if (u >= num_vertices || u == v || seen_neighbor_stamp[u] == stamp) { + continue; + } + seen_neighbor_stamp[u] = stamp; + vertex_neighbor_data[write++] = u; + } + } + ++stamp; + } + return; + } + + const auto gather_unique_neighbors = [&](uint32_t v, std::vector &neighbors) { + neighbors.clear(); + const uint32_t face_begin = vertex_face_offsets[v]; + const uint32_t face_end = vertex_face_offsets[v + 1]; + + const auto try_push = [&](uint32_t u) { + if (u >= num_vertices || u == v) { + return; + } + for (uint32_t existing : neighbors) { + if (existing == u) { + return; + } + } + neighbors.push_back(u); + }; + + const size_t candidate_hint = + static_cast(std::max(1, face_end - face_begin) * 2); + if (neighbors.capacity() < candidate_hint) { + neighbors.reserve(candidate_hint); + } + + for (uint32_t idx = face_begin; idx < face_end; ++idx) { + const uint32_t f = vertex_face_data[idx]; + try_push(face_v0[f]); + try_push(face_v1[f]); + try_push(face_v2[f]); + } + }; + + core::parallel_for_index( + 0, static_cast(num_vertices), + [&](int vertex_idx) { + const uint32_t v = static_cast(vertex_idx); + thread_local std::vector neighbors; + gather_unique_neighbors(v, neighbors); + vertex_neighbor_offsets[static_cast(v) + 1] = + static_cast(neighbors.size()); + }, + 32768); + + for (size_t i = 0; i < num_vertices; ++i) { + vertex_neighbor_offsets[i + 1] += vertex_neighbor_offsets[i]; + } + + vertex_neighbor_data.resize(vertex_neighbor_offsets[num_vertices]); + + core::parallel_for_index( + 0, static_cast(num_vertices), + [&](int vertex_idx) { + const uint32_t v = static_cast(vertex_idx); + thread_local std::vector neighbors; + gather_unique_neighbors(v, neighbors); + + uint32_t write = vertex_neighbor_offsets[v]; + for (uint32_t u : neighbors) { + vertex_neighbor_data[write++] = u; + } + }, + 32768); + } + + void clear() { + faces_to_vertices.clear(); + face_v0.clear(); + face_v1.clear(); + face_v2.clear(); + vertex_face_offsets.clear(); + vertex_face_data.clear(); + vertex_neighbor_offsets.clear(); + vertex_neighbor_data.clear(); + } +}; + +static_assert(Structure); +static_assert(SurfaceStructure); + +} // namespace igneous::data diff --git a/include/igneous/igneous.hpp b/include/igneous/igneous.hpp index 1391380..addb78a 100644 --- a/include/igneous/igneous.hpp +++ b/include/igneous/igneous.hpp @@ -4,15 +4,16 @@ #include #include -#include -#include -#include +#include +#include +#include +#include #include #include -#include -#include +#include +#include #include #include #include diff --git a/include/igneous/io/exporter.hpp b/include/igneous/io/exporter.hpp index fbc05e6..f14e555 100644 --- a/include/igneous/io/exporter.hpp +++ b/include/igneous/io/exporter.hpp @@ -10,12 +10,12 @@ #include #include -#include -#include +#include +#include namespace igneous::io { -using igneous::data::Mesh; +using igneous::data::Space; inline std::tuple get_heatmap_color_bytes(double t) { t = std::clamp(t, 0.0, 1.0); @@ -65,8 +65,8 @@ inline std::pair compute_field_bounds(std::span fie return {mean - (sigma_clip * std_dev), mean + (sigma_clip * std_dev)}; } -template -void export_ply(const Mesh &mesh, std::span field, +template +void export_ply(const Space &mesh, std::span field, const std::string &filename, double sigma_clip = 2.0) { const auto [min_v, max_v] = compute_field_bounds(field, sigma_clip); @@ -75,7 +75,7 @@ void export_ply(const Mesh &mesh, std::span field, return; } - const size_t n_verts = mesh.geometry.num_points(); + const size_t n_verts = mesh.num_points(); file << "ply\n"; file << "format ascii 1.0\n"; @@ -90,7 +90,7 @@ void export_ply(const Mesh &mesh, std::span field, const double denom = std::max(1e-12, max_v - min_v); for (size_t i = 0; i < n_verts; ++i) { - const auto p = mesh.geometry.get_vec3(i); + const auto p = mesh.get_vec3(i); const double val = (i < field.size()) ? static_cast(field[i]) : 0.0; const double t = (val - min_v) / denom; const auto [r, g, b] = get_heatmap_color_bytes(t); @@ -102,15 +102,15 @@ void export_ply(const Mesh &mesh, std::span field, std::cout << "[IO] Exported PLY " << filename << "\n"; } -template -void export_ply(const Mesh &mesh, const std::vector &field, +template +void export_ply(const Space &mesh, const std::vector &field, const std::string &filename, double sigma_clip = 2.0) { export_ply(mesh, std::span(field.data(), field.size()), filename, sigma_clip); } -template -void export_heatmap(const Mesh &mesh, std::span field, +template +void export_heatmap(const Space &mesh, std::span field, const std::string &filename, double sigma_clip = 2.0) { const auto [min_v, max_v] = compute_field_bounds(field, sigma_clip); @@ -119,11 +119,11 @@ void export_heatmap(const Mesh &mesh, std::span field, return; } - const size_t n_verts = mesh.geometry.num_points(); + const size_t n_verts = mesh.num_points(); const double denom = std::max(1e-12, max_v - min_v); for (size_t i = 0; i < n_verts; ++i) { - const auto p = mesh.geometry.get_vec3(i); + const auto p = mesh.get_vec3(i); const double val = (i < field.size()) ? static_cast(field[i]) : 0.0; const double t = (val - min_v) / denom; const auto [r, g, b] = get_heatmap_color_bytes(t); @@ -132,12 +132,12 @@ void export_heatmap(const Mesh &mesh, std::span field, << (b / 255.0f) << "\n"; } - if constexpr (igneous::data::SurfaceTopology) { - const size_t n_faces = mesh.topology.num_faces(); + if constexpr (igneous::data::SurfaceStructure) { + const size_t n_faces = mesh.structure.num_faces(); for (size_t i = 0; i < n_faces; ++i) { - file << "f " << mesh.topology.get_vertex_for_face(i, 0) + 1 << " " - << mesh.topology.get_vertex_for_face(i, 1) + 1 << " " - << mesh.topology.get_vertex_for_face(i, 2) + 1 << "\n"; + file << "f " << mesh.structure.get_vertex_for_face(i, 0) + 1 << " " + << mesh.structure.get_vertex_for_face(i, 1) + 1 << " " + << mesh.structure.get_vertex_for_face(i, 2) + 1 << "\n"; } } else { file << "p"; @@ -153,16 +153,16 @@ void export_heatmap(const Mesh &mesh, std::span field, std::cout << "[IO] Exported OBJ " << filename << "\n"; } -template -void export_heatmap(const Mesh &mesh, +template +void export_heatmap(const Space &mesh, const std::vector &field, const std::string &filename, double sigma_clip = 2.0) { export_heatmap(mesh, std::span(field.data(), field.size()), filename, sigma_clip); } -template -void export_ply_solid(const Mesh &mesh, std::span field, +template +void export_ply_solid(const Space &mesh, std::span field, const std::string &filename, double radius = 0.01, double sigma_clip = 2.0) { const auto [min_v, max_v] = compute_field_bounds(field, sigma_clip); @@ -172,7 +172,7 @@ void export_ply_solid(const Mesh &mesh, std::span field, return; } - const size_t n_points = mesh.geometry.num_points(); + const size_t n_points = mesh.num_points(); const size_t n_verts = n_points * 4; const size_t n_faces = n_points * 4; @@ -194,7 +194,7 @@ void export_ply_solid(const Mesh &mesh, std::span field, const double denom = std::max(1e-12, max_v - min_v); for (size_t i = 0; i < n_points; ++i) { - const auto p = mesh.geometry.get_vec3(i); + const auto p = mesh.get_vec3(i); const double val = (i < field.size()) ? static_cast(field[i]) : 0.0; const double t = (val - min_v) / denom; const auto [r, g, b] = get_heatmap_color_bytes(t); @@ -220,8 +220,8 @@ void export_ply_solid(const Mesh &mesh, std::span field, std::cout << "[IO] Exported Solid PLY " << filename << "\n"; } -template -void export_ply_solid(const Mesh &mesh, +template +void export_ply_solid(const Space &mesh, const std::vector &field, const std::string &filename, double radius = 0.01, double sigma_clip = 2.0) { diff --git a/include/igneous/io/importer.hpp b/include/igneous/io/importer.hpp index d942360..1887e5f 100644 --- a/include/igneous/io/importer.hpp +++ b/include/igneous/io/importer.hpp @@ -4,17 +4,16 @@ #include #include #include -#include -#include -#include +#include +#include namespace igneous::io { -using igneous::data::Mesh; +using igneous::data::Space; -template -void load_obj(Mesh &mesh, const std::string &filename) { +template +void load_obj(Space &mesh, const std::string &filename) { std::ifstream file(filename); if (!file.is_open()) { std::cerr << "Failed to open " << filename << "\n"; @@ -38,33 +37,23 @@ void load_obj(Mesh &mesh, const std::string &filename) { float y = 0.0f; float z = 0.0f; iss >> x >> y >> z; - mesh.geometry.push_point({x, y, z}); + mesh.push_point({x, y, z}); continue; } if (type == "f") { - if constexpr (igneous::data::SurfaceTopology) { + if constexpr (igneous::data::SurfaceStructure) { std::string v_str; while (iss >> v_str) { const size_t slash = v_str.find('/'); const int idx = std::stoi(v_str.substr(0, slash)) - 1; - mesh.topology.faces_to_vertices.push_back(static_cast(idx)); + mesh.structure.faces_to_vertices.push_back(static_cast(idx)); } } } } - - const size_t n_verts = mesh.geometry.num_points(); - - if constexpr (std::is_same_v) { - mesh.topology.build({n_verts, true}); - } else if constexpr (std::is_same_v) { - mesh.topology.build({mesh.geometry.x_span(), mesh.geometry.y_span(), mesh.geometry.z_span()}); - } else { - mesh.topology.build({}); - } - - std::cout << "[IO] Loaded " << filename << " (" << n_verts << " verts)\n"; + std::cout << "[IO] Loaded " << filename << " (" << mesh.num_points() + << " verts)\n"; } } // namespace igneous::io diff --git a/include/igneous/ops/curvature.hpp b/include/igneous/ops/dec/curvature.hpp similarity index 67% rename from include/igneous/ops/curvature.hpp rename to include/igneous/ops/dec/curvature.hpp index 7127dd6..163001c 100644 --- a/include/igneous/ops/curvature.hpp +++ b/include/igneous/ops/dec/curvature.hpp @@ -6,29 +6,26 @@ #include #include -#include #include #include -#include -#include +#include +#include -namespace igneous::ops { +namespace igneous::ops::dec { -template -struct CurvatureWorkspace { +template struct CurvatureWorkspace { std::vector face_normals; }; -template -void compute_curvature_measures(const data::Mesh &mesh, +template +void compute_curvature_measures(const data::Space &space, std::vector &H, std::vector &K, - CurvatureWorkspace &workspace) { - const auto &geometry = mesh.geometry; - const auto &topology = mesh.topology; + CurvatureWorkspace &workspace) { + const auto &structure = space.structure; - const size_t num_verts = geometry.num_points(); - const size_t num_faces = topology.num_faces(); + const size_t num_verts = space.num_points(); + const size_t num_faces = structure.num_faces(); H.assign(num_verts, 0.0f); K.assign(num_verts, 0.0f); @@ -36,14 +33,14 @@ void compute_curvature_measures(const data::Mesh &mesh, workspace.face_normals.resize(num_faces); const auto get_face_vertex = [&](size_t face_idx, int corner) -> uint32_t { - if constexpr (std::is_same_v) { + if constexpr (std::is_same_v) { if (corner == 0) - return topology.face_v0[face_idx]; + return structure.face_v0[face_idx]; if (corner == 1) - return topology.face_v1[face_idx]; - return topology.face_v2[face_idx]; + return structure.face_v1[face_idx]; + return structure.face_v2[face_idx]; } - return topology.get_vertex_for_face(face_idx, corner); + return structure.get_vertex_for_face(face_idx, corner); }; core::parallel_for_index( @@ -54,9 +51,9 @@ void compute_curvature_measures(const data::Mesh &mesh, const uint32_t i1 = get_face_vertex(f, 1); const uint32_t i2 = get_face_vertex(f, 2); - const core::Vec3 p0 = geometry.get_vec3(i0); - const core::Vec3 p1 = geometry.get_vec3(i1); - const core::Vec3 p2 = geometry.get_vec3(i2); + const core::Vec3 p0 = space.get_vec3(i0); + const core::Vec3 p1 = space.get_vec3(i1); + const core::Vec3 p2 = space.get_vec3(i2); workspace.face_normals[f] = (p1 - p0) ^ (p2 - p0); }, @@ -66,7 +63,7 @@ void compute_curvature_measures(const data::Mesh &mesh, 0, static_cast(num_verts), [&](int vertex_idx) { const size_t i = static_cast(vertex_idx); - const auto faces = topology.get_faces_for_vertex(static_cast(i)); + const auto faces = structure.get_faces_for_vertex(static_cast(i)); if (faces.empty()) { return; } @@ -81,7 +78,7 @@ void compute_curvature_measures(const data::Mesh &mesh, core::Vec3 sum_pos{0.0f, 0.0f, 0.0f}; float neighbor_count = 0.0f; - const core::Vec3 p = geometry.get_vec3(i); + const core::Vec3 p = space.get_vec3(i); for (uint32_t f_idx : faces) { const core::Bivec3 fn = workspace.face_normals[f_idx]; @@ -96,14 +93,14 @@ void compute_curvature_measures(const data::Mesh &mesh, core::Vec3 p_a; core::Vec3 p_b; if (i0 == i) { - p_a = geometry.get_vec3(i1); - p_b = geometry.get_vec3(i2); + p_a = space.get_vec3(i1); + p_b = space.get_vec3(i2); } else if (i1 == i) { - p_a = geometry.get_vec3(i2); - p_b = geometry.get_vec3(i0); + p_a = space.get_vec3(i2); + p_b = space.get_vec3(i0); } else { - p_a = geometry.get_vec3(i0); - p_b = geometry.get_vec3(i1); + p_a = space.get_vec3(i0); + p_b = space.get_vec3(i1); } const core::Vec3 u = p_a - p; @@ -140,4 +137,4 @@ void compute_curvature_measures(const data::Mesh &mesh, 128); } -} // namespace igneous::ops +} // namespace igneous::ops::dec diff --git a/include/igneous/ops/flow.hpp b/include/igneous/ops/dec/flow.hpp similarity index 57% rename from include/igneous/ops/flow.hpp rename to include/igneous/ops/dec/flow.hpp index a901e58..17adf11 100644 --- a/include/igneous/ops/flow.hpp +++ b/include/igneous/ops/dec/flow.hpp @@ -1,37 +1,33 @@ #pragma once -#include -#include -#include -#include #include #include -namespace igneous::ops { +#include +#include +#include +#include + +namespace igneous::ops::dec { -template -struct FlowWorkspace { +template struct FlowWorkspace { std::vector displacements; }; -template -void integrate_mean_curvature_flow(data::Mesh &mesh, float dt, - FlowWorkspace &workspace) { - auto &geometry = mesh.geometry; - const auto &topology = mesh.topology; +template +void integrate_mean_curvature_flow(data::Space &space, float dt, + FlowWorkspace &workspace) { + const auto &structure = space.structure; - const size_t num_verts = geometry.num_points(); + const size_t num_verts = space.num_points(); if (workspace.displacements.size() != num_verts) { workspace.displacements.resize(num_verts); } - if constexpr (std::is_same_v) { - const auto &x = geometry.x; - const auto &y = geometry.y; - const auto &z = geometry.z; - const auto &neighbor_offsets = topology.vertex_neighbor_offsets; - const auto &neighbor_data = topology.vertex_neighbor_data; + if constexpr (std::is_same_v) { + const auto &neighbor_offsets = structure.vertex_neighbor_offsets; + const auto &neighbor_data = structure.vertex_neighbor_data; core::parallel_for_index( 0, static_cast(num_verts), @@ -49,14 +45,15 @@ void integrate_mean_curvature_flow(data::Mesh &mesh, float dt, float sz = 0.0f; for (uint32_t idx = begin; idx < end; ++idx) { const uint32_t n_idx = neighbor_data[idx]; - sx += x[n_idx]; - sy += y[n_idx]; - sz += z[n_idx]; + sx += space.x[n_idx]; + sy += space.y[n_idx]; + sz += space.z[n_idx]; } const float inv_count = 1.0f / static_cast(end - begin); - workspace.displacements[i] = {sx * inv_count - x[i], sy * inv_count - y[i], - sz * inv_count - z[i]}; + workspace.displacements[i] = {sx * inv_count - space.x[i], + sy * inv_count - space.y[i], + sz * inv_count - space.z[i]}; }, 131072); @@ -65,9 +62,9 @@ void integrate_mean_curvature_flow(data::Mesh &mesh, float dt, [&](int vertex_idx) { const size_t i = static_cast(vertex_idx); const core::Vec3 &d = workspace.displacements[i]; - geometry.x[i] += d.x * dt; - geometry.y[i] += d.y * dt; - geometry.z[i] += d.z * dt; + space.x[i] += d.x * dt; + space.y[i] += d.y * dt; + space.z[i] += d.z * dt; }, 131072); return; @@ -77,7 +74,7 @@ void integrate_mean_curvature_flow(data::Mesh &mesh, float dt, 0, static_cast(num_verts), [&](int vertex_idx) { const size_t i = static_cast(vertex_idx); - const auto neighbors = topology.get_vertex_neighbors(static_cast(i)); + const auto neighbors = structure.get_vertex_neighbors(static_cast(i)); if (neighbors.empty()) { workspace.displacements[i] = {0.0f, 0.0f, 0.0f}; return; @@ -85,12 +82,12 @@ void integrate_mean_curvature_flow(data::Mesh &mesh, float dt, core::Vec3 sum_neighbors{0.0f, 0.0f, 0.0f}; for (uint32_t n_idx : neighbors) { - sum_neighbors = sum_neighbors + geometry.get_vec3(n_idx); + sum_neighbors = sum_neighbors + space.get_vec3(n_idx); } const float inv_count = 1.0f / static_cast(neighbors.size()); const core::Vec3 average_pos = sum_neighbors * inv_count; - workspace.displacements[i] = average_pos - geometry.get_vec3(i); + workspace.displacements[i] = average_pos - space.get_vec3(i); }, 131072); @@ -98,11 +95,11 @@ void integrate_mean_curvature_flow(data::Mesh &mesh, float dt, 0, static_cast(num_verts), [&](int vertex_idx) { const size_t i = static_cast(vertex_idx); - const core::Vec3 p = geometry.get_vec3(i); + const core::Vec3 p = space.get_vec3(i); const core::Vec3 d = workspace.displacements[i]; - geometry.set_vec3(i, p + d * dt); + space.set_vec3(i, p + d * dt); }, 131072); } -} // namespace igneous::ops +} // namespace igneous::ops::dec diff --git a/include/igneous/ops/diffusion/basis.hpp b/include/igneous/ops/diffusion/basis.hpp index 5a82d28..8d64f20 100644 --- a/include/igneous/ops/diffusion/basis.hpp +++ b/include/igneous/ops/diffusion/basis.hpp @@ -10,7 +10,7 @@ #include #include -namespace igneous::ops { +namespace igneous::ops::diffusion { struct WedgeProductIndexData { std::vector target_indices; @@ -193,4 +193,4 @@ inline WedgeProductIndexData get_wedge_product_indices(int d, int k1, int k2) { return out; } -} // namespace igneous::ops +} // namespace igneous::ops::diffusion diff --git a/include/igneous/ops/diffusion/forms.hpp b/include/igneous/ops/diffusion/forms.hpp index a1b281d..859d656 100644 --- a/include/igneous/ops/diffusion/forms.hpp +++ b/include/igneous/ops/diffusion/forms.hpp @@ -15,7 +15,7 @@ #include #include -namespace igneous::ops { +namespace igneous::ops::diffusion { template struct DiffusionFormWorkspace { std::array coords; @@ -86,7 +86,7 @@ void ensure_gamma_coords(const MeshT &mesh, DiffusionFormWorkspace &works } fill_coordinate_vectors(mesh, workspace.coords); - const int n = static_cast(mesh.geometry.num_points()); + const int n = static_cast(mesh.num_points()); for (int a = 0; a < 3; ++a) { for (int b = a; b < 3; ++b) { workspace.gamma_coords[a][b].resize(n); @@ -103,8 +103,8 @@ void ensure_gamma_coords(const MeshT &mesh, DiffusionFormWorkspace &works template void ensure_gamma_mixed(const MeshT &mesh, int n_coefficients, DiffusionFormWorkspace &workspace) { - const auto &U = mesh.topology.eigen_basis; - const int n = static_cast(mesh.geometry.num_points()); + const auto &U = mesh.structure.eigen_basis; + const int n = static_cast(mesh.num_points()); const int n1 = std::max(1, std::min(n_coefficients, static_cast(U.cols()))); if (workspace.gamma_mixed_cols >= n1 && workspace.gamma_mixed[0].rows() == n) { @@ -156,14 +156,14 @@ template Eigen::MatrixXf compute_kform_gram_matrix(const MeshT &mesh, int k, int n_coefficients, DiffusionFormWorkspace &workspace) { - const auto &U = mesh.topology.eigen_basis; - const auto &mu = mesh.topology.mu; + const auto &U = mesh.structure.eigen_basis; + const auto &mu = mesh.structure.mu; const int n1 = std::max(1, std::min(n_coefficients, static_cast(U.cols()))); const auto compounds = compute_compound_determinants(mesh, k, workspace); const int Ck = std::max(1, compounds.n_components); Eigen::MatrixXf G = Eigen::MatrixXf::Zero(n1 * Ck, n1 * Ck); - const int n = static_cast(mesh.geometry.num_points()); + const int n = static_cast(mesh.num_points()); Eigen::ArrayXf weights(n); if (k == 0) { @@ -217,8 +217,8 @@ Eigen::MatrixXf compute_weak_exterior_derivative( return Eigen::MatrixXf(); } - const auto &U = mesh.topology.eigen_basis; - const auto &mu = mesh.topology.mu; + const auto &U = mesh.structure.eigen_basis; + const auto &mu = mesh.structure.mu; const int n1 = std::max(1, std::min(n_coefficients, static_cast(U.cols()))); ensure_gamma_mixed(mesh, n1, workspace); @@ -242,7 +242,7 @@ Eigen::MatrixXf compute_weak_exterior_derivative( const int Ckp1 = static_cast(kp1.idx_kp1.size()); Eigen::MatrixXf D = Eigen::MatrixXf::Zero(n1 * Ckp1, n1 * Ck); - const int n = static_cast(mesh.geometry.num_points()); + const int n = static_cast(mesh.num_points()); for (int I = 0; I < n1; ++I) { for (int i = 0; i < n1; ++i) { @@ -362,10 +362,10 @@ Eigen::MatrixXf compute_up_laplacian_matrix(const MeshT &mesh, int k, return Eigen::MatrixXf(); } - const auto &U = mesh.topology.eigen_basis; - const auto &mu = mesh.topology.mu; + const auto &U = mesh.structure.eigen_basis; + const auto &mu = mesh.structure.mu; const int n1 = std::max(1, std::min(n_coefficients, static_cast(U.cols()))); - const int n = static_cast(mesh.geometry.num_points()); + const int n = static_cast(mesh.num_points()); ensure_gamma_coords(mesh, workspace); ensure_gamma_mixed(mesh, n1, workspace); @@ -457,7 +457,7 @@ Eigen::MatrixXf compute_down_laplacian_matrix( DiffusionFormWorkspace &workspace) { if (k <= 0) { const int Ck = std::max(1, binomial_coeff(ambient_dim_3d(), std::max(0, k))); - const int n1 = std::max(1, std::min(n_coefficients, static_cast(mesh.topology.eigen_basis.cols()))); + const int n1 = std::max(1, std::min(n_coefficients, static_cast(mesh.structure.eigen_basis.cols()))); return Eigen::MatrixXf::Zero(n1 * Ck, n1 * Ck); } @@ -548,7 +548,7 @@ template Eigen::MatrixXf coefficients_to_pointwise(const MeshT &mesh, const Eigen::VectorXf &coeffs, int k, int n_coefficients) { - const auto &U = mesh.topology.eigen_basis; + const auto &U = mesh.structure.eigen_basis; const int n1 = std::max(1, std::min(n_coefficients, static_cast(U.cols()))); const int Ck = std::max(1, binomial_coeff(ambient_dim_3d(), k)); if (coeffs.size() != n1 * Ck) { @@ -564,8 +564,8 @@ template Eigen::VectorXf project_pointwise_to_coefficients(const MeshT &mesh, const Eigen::MatrixXf &pointwise, int n_coefficients) { - const auto &U = mesh.topology.eigen_basis; - const auto &mu = mesh.topology.mu; + const auto &U = mesh.structure.eigen_basis; + const auto &mu = mesh.structure.mu; const int n1 = std::max(1, std::min(n_coefficients, static_cast(U.cols()))); if (pointwise.rows() != U.rows()) { @@ -607,4 +607,4 @@ pointwise_2form_to_dual_vectors(const Eigen::MatrixXf &pointwise_2form) { return out; } -} // namespace igneous::ops +} // namespace igneous::ops::diffusion diff --git a/include/igneous/ops/diffusion/geometry.hpp b/include/igneous/ops/diffusion/geometry.hpp index 5d280e0..97933e2 100644 --- a/include/igneous/ops/diffusion/geometry.hpp +++ b/include/igneous/ops/diffusion/geometry.hpp @@ -10,9 +10,9 @@ #include #include -#include +#include -namespace igneous::ops { +namespace igneous::ops::diffusion { template struct GeometryWorkspace { std::array coords; @@ -29,13 +29,13 @@ template struct DiffusionWorkspace { template void fill_coordinate_vectors(const MeshT &mesh, std::array &coords) { - const size_t n_verts = mesh.geometry.num_points(); + const size_t n_verts = mesh.num_points(); for (int d = 0; d < 3; ++d) { coords[d].resize(static_cast(n_verts)); } - if constexpr (requires { mesh.topology.immersion_coords; }) { - const auto &immersion = mesh.topology.immersion_coords; + if constexpr (requires { mesh.structure.immersion_coords; }) { + const auto &immersion = mesh.structure.immersion_coords; if (immersion.rows() == static_cast(n_verts) && immersion.cols() >= 3) { for (size_t i = 0; i < n_verts; ++i) { const int idx = static_cast(i); @@ -48,7 +48,7 @@ void fill_coordinate_vectors(const MeshT &mesh, } for (size_t i = 0; i < n_verts; ++i) { - const auto p = mesh.geometry.get_vec3(i); + const auto p = mesh.get_vec3(i); const int idx = static_cast(i); coords[0][idx] = p.x; coords[1][idx] = p.y; @@ -59,13 +59,13 @@ void fill_coordinate_vectors(const MeshT &mesh, template void fill_data_coordinate_vectors(const MeshT &mesh, std::array &coords) { - const size_t n_verts = mesh.geometry.num_points(); + const size_t n_verts = mesh.num_points(); for (int d = 0; d < 3; ++d) { coords[d].resize(static_cast(n_verts)); } for (size_t i = 0; i < n_verts; ++i) { - const auto p = mesh.geometry.get_vec3(i); + const auto p = mesh.get_vec3(i); const int idx = static_cast(i); coords[0][idx] = p.x; coords[1][idx] = p.y; @@ -78,95 +78,84 @@ void carre_du_champ(const MeshT &mesh, Eigen::Ref f, Eigen::Ref h, [[maybe_unused]] float bandwidth, Eigen::Ref gamma_out) { [[maybe_unused]] const int expected_size = - static_cast(mesh.geometry.num_points()); + static_cast(mesh.num_points()); assert(gamma_out.size() == expected_size); gamma_out.setZero(); - if constexpr (requires { - mesh.topology.markov_row_offsets; - mesh.topology.markov_col_indices; - mesh.topology.markov_values; - }) { - const auto &row_offsets = mesh.topology.markov_row_offsets; - const auto &col_indices = mesh.topology.markov_col_indices; - const auto &weights = mesh.topology.markov_values; - - assert(row_offsets.size() == static_cast(expected_size) + 1); - - const float *f_data = f.data(); - const float *h_data = h.data(); - Eigen::VectorXf means_f = Eigen::VectorXf::Zero(expected_size); - Eigen::VectorXf means_h = Eigen::VectorXf::Zero(expected_size); - const bool use_mean_centres = [&]() { - if constexpr (requires { mesh.topology.use_mean_centres; }) { - return mesh.topology.use_mean_centres; - } - return false; - }(); - - if (use_mean_centres) { - for (int i = 0; i < expected_size; ++i) { - const int begin = row_offsets[static_cast(i)]; - const int end = row_offsets[static_cast(i) + 1]; - const int *cols = col_indices.data() + begin; - const float *w = weights.data() + begin; - float mean_f = 0.0f; - float mean_h = 0.0f; - for (int idx = 0; idx < (end - begin); ++idx) { - mean_f += w[idx] * f_data[cols[idx]]; - mean_h += w[idx] * h_data[cols[idx]]; - } - means_f[i] = mean_f; - means_h[i] = mean_h; - } + static_assert(requires { + mesh.structure.markov_row_offsets; + mesh.structure.markov_col_indices; + mesh.structure.markov_values; + }, + "carre_du_champ requires diffusion CSR storage."); + + const auto &row_offsets = mesh.structure.markov_row_offsets; + const auto &col_indices = mesh.structure.markov_col_indices; + const auto &weights = mesh.structure.markov_values; + + assert(row_offsets.size() == static_cast(expected_size) + 1); + + const float *f_data = f.data(); + const float *h_data = h.data(); + Eigen::VectorXf means_f = Eigen::VectorXf::Zero(expected_size); + Eigen::VectorXf means_h = Eigen::VectorXf::Zero(expected_size); + const bool use_mean_centres = [&]() { + if constexpr (requires { mesh.structure.use_mean_centres; }) { + return mesh.structure.use_mean_centres; } + return false; + }(); + if (use_mean_centres) { for (int i = 0; i < expected_size; ++i) { const int begin = row_offsets[static_cast(i)]; const int end = row_offsets[static_cast(i) + 1]; - const int count = end - begin; const int *cols = col_indices.data() + begin; const float *w = weights.data() + begin; - const float center_f = use_mean_centres ? means_f[i] : f_data[i]; - const float center_h = use_mean_centres ? means_h[i] : h_data[i]; - - float acc = 0.0f; - int idx = 0; - for (; idx + 3 < count; idx += 4) { - const int j0 = cols[idx + 0]; - const int j1 = cols[idx + 1]; - const int j2 = cols[idx + 2]; - const int j3 = cols[idx + 3]; - acc += w[idx + 0] * (f_data[j0] - center_f) * (h_data[j0] - center_h); - acc += w[idx + 1] * (f_data[j1] - center_f) * (h_data[j1] - center_h); - acc += w[idx + 2] * (f_data[j2] - center_f) * (h_data[j2] - center_h); - acc += w[idx + 3] * (f_data[j3] - center_f) * (h_data[j3] - center_h); - } - for (; idx < count; ++idx) { - const int j = cols[idx]; - acc += w[idx] * (f_data[j] - center_f) * (h_data[j] - center_h); + float mean_f = 0.0f; + float mean_h = 0.0f; + for (int idx = 0; idx < (end - begin); ++idx) { + mean_f += w[idx] * f_data[cols[idx]]; + mean_h += w[idx] * h_data[cols[idx]]; } + means_f[i] = mean_f; + means_h[i] = mean_h; + } + } - float denom = 2.0f; - if constexpr (requires { mesh.topology.local_bandwidths; }) { - if (mesh.topology.local_bandwidths.size() == expected_size) { - denom = 2.0f * std::max(mesh.topology.local_bandwidths[i], 1e-8f); - } - } - gamma_out[i] = acc / denom; + for (int i = 0; i < expected_size; ++i) { + const int begin = row_offsets[static_cast(i)]; + const int end = row_offsets[static_cast(i) + 1]; + const int count = end - begin; + const int *cols = col_indices.data() + begin; + const float *w = weights.data() + begin; + const float center_f = use_mean_centres ? means_f[i] : f_data[i]; + const float center_h = use_mean_centres ? means_h[i] : h_data[i]; + + float acc = 0.0f; + int idx = 0; + for (; idx + 3 < count; idx += 4) { + const int j0 = cols[idx + 0]; + const int j1 = cols[idx + 1]; + const int j2 = cols[idx + 2]; + const int j3 = cols[idx + 3]; + acc += w[idx + 0] * (f_data[j0] - center_f) * (h_data[j0] - center_h); + acc += w[idx + 1] * (f_data[j1] - center_f) * (h_data[j1] - center_h); + acc += w[idx + 2] * (f_data[j2] - center_f) * (h_data[j2] - center_h); + acc += w[idx + 3] * (f_data[j3] - center_f) * (h_data[j3] - center_h); + } + for (; idx < count; ++idx) { + const int j = cols[idx]; + acc += w[idx] * (f_data[j] - center_f) * (h_data[j] - center_h); } - } else { - const auto &P = mesh.topology.P; - for (int outer = 0; outer < P.outerSize(); ++outer) { - for (typename std::remove_cvref_t::InnerIterator it(P, outer); - it; ++it) { - const int i = it.row(); - const int j = it.col(); - const float w = it.value(); - gamma_out[i] += w * (f[j] - f[i]) * (h[j] - h[i]); + + float denom = 2.0f; + if constexpr (requires { mesh.structure.local_bandwidths; }) { + if (mesh.structure.local_bandwidths.size() == expected_size) { + denom = 2.0f * std::max(mesh.structure.local_bandwidths[i], 1e-8f); } } - gamma_out *= 0.5f; + gamma_out[i] = acc / denom; } } @@ -174,64 +163,63 @@ template void apply_markov_transition(const MeshT &mesh, Eigen::Ref input, Eigen::Ref output) { - const int expected_size = static_cast(mesh.geometry.num_points()); + const int expected_size = static_cast(mesh.num_points()); assert(input.size() == expected_size); assert(output.size() == expected_size); - if constexpr (requires { - mesh.topology.markov_row_offsets; - mesh.topology.markov_col_indices; - mesh.topology.markov_values; - }) { - const auto &row_offsets = mesh.topology.markov_row_offsets; - const auto &col_indices = mesh.topology.markov_col_indices; - const auto &weights = mesh.topology.markov_values; - - assert(row_offsets.size() == static_cast(expected_size) + 1); - assert(input.data() != output.data()); - - const bool use_gpu = - core::compute_backend_from_env() == core::ComputeBackend::Gpu && - (core::gpu::gpu_force_enabled() || expected_size >= core::gpu::gpu_min_rows()); - if (use_gpu) { - if (core::gpu::apply_markov_transition( - static_cast(&mesh.topology), - std::span(row_offsets.data(), row_offsets.size()), - std::span(col_indices.data(), col_indices.size()), - std::span(weights.data(), weights.size()), - std::span(input.data(), static_cast(expected_size)), - std::span(output.data(), static_cast(expected_size)))) { - return; - } + static_assert(requires { + mesh.structure.markov_row_offsets; + mesh.structure.markov_col_indices; + mesh.structure.markov_values; + }, + "apply_markov_transition requires diffusion CSR storage."); + + const auto &row_offsets = mesh.structure.markov_row_offsets; + const auto &col_indices = mesh.structure.markov_col_indices; + const auto &weights = mesh.structure.markov_values; + + assert(row_offsets.size() == static_cast(expected_size) + 1); + assert(input.data() != output.data()); + + const bool use_gpu = + core::compute_backend_from_env() == core::ComputeBackend::Gpu && + (core::gpu::gpu_force_enabled() || expected_size >= core::gpu::gpu_min_rows()); + if (use_gpu) { + if (core::gpu::apply_markov_transition( + static_cast(&mesh.structure), + std::span(row_offsets.data(), row_offsets.size()), + std::span(col_indices.data(), col_indices.size()), + std::span(weights.data(), weights.size()), + std::span(input.data(), static_cast(expected_size)), + std::span(output.data(), static_cast(expected_size)))) { + return; } + } - const float *input_data = input.data(); - float *output_data = output.data(); - - for (int i = 0; i < expected_size; ++i) { - const int begin = row_offsets[static_cast(i)]; - const int end = row_offsets[static_cast(i) + 1]; - const int count = end - begin; - const int *cols = col_indices.data() + begin; - const float *w = weights.data() + begin; - - float acc = 0.0f; - int idx = 0; - for (; idx + 3 < count; idx += 4) { - acc += w[idx + 0] * input_data[cols[idx + 0]]; - acc += w[idx + 1] * input_data[cols[idx + 1]]; - acc += w[idx + 2] * input_data[cols[idx + 2]]; - acc += w[idx + 3] * input_data[cols[idx + 3]]; - } - - for (; idx < count; ++idx) { - acc += w[idx] * input_data[cols[idx]]; - } + const float *input_data = input.data(); + float *output_data = output.data(); + + for (int i = 0; i < expected_size; ++i) { + const int begin = row_offsets[static_cast(i)]; + const int end = row_offsets[static_cast(i) + 1]; + const int count = end - begin; + const int *cols = col_indices.data() + begin; + const float *w = weights.data() + begin; + + float acc = 0.0f; + int idx = 0; + for (; idx + 3 < count; idx += 4) { + acc += w[idx + 0] * input_data[cols[idx + 0]]; + acc += w[idx + 1] * input_data[cols[idx + 1]]; + acc += w[idx + 2] * input_data[cols[idx + 2]]; + acc += w[idx + 3] * input_data[cols[idx + 3]]; + } - output_data[i] = acc; + for (; idx < count; ++idx) { + acc += w[idx] * input_data[cols[idx]]; } - } else { - output.noalias() = mesh.topology.P * input; + + output_data[i] = acc; } } @@ -241,7 +229,7 @@ void apply_markov_transition_steps(const MeshT &mesh, int steps, Eigen::Ref output, DiffusionWorkspace &workspace) { - const int expected_size = static_cast(mesh.geometry.num_points()); + const int expected_size = static_cast(mesh.num_points()); assert(input.size() == expected_size); assert(output.size() == expected_size); @@ -251,13 +239,13 @@ void apply_markov_transition_steps(const MeshT &mesh, } if constexpr (requires { - mesh.topology.markov_row_offsets; - mesh.topology.markov_col_indices; - mesh.topology.markov_values; + mesh.structure.markov_row_offsets; + mesh.structure.markov_col_indices; + mesh.structure.markov_values; }) { - const auto &row_offsets = mesh.topology.markov_row_offsets; - const auto &col_indices = mesh.topology.markov_col_indices; - const auto &weights = mesh.topology.markov_values; + const auto &row_offsets = mesh.structure.markov_row_offsets; + const auto &col_indices = mesh.structure.markov_col_indices; + const auto &weights = mesh.structure.markov_values; assert(row_offsets.size() == static_cast(expected_size) + 1); const long long row_step_work = @@ -269,7 +257,7 @@ void apply_markov_transition_steps(const MeshT &mesh, row_step_work >= core::gpu::gpu_min_row_steps()); if (use_gpu) { if (core::gpu::apply_markov_transition_steps( - static_cast(&mesh.topology), + static_cast(&mesh.structure), std::span(row_offsets.data(), row_offsets.size()), std::span(col_indices.data(), col_indices.size()), std::span(weights.data(), weights.size()), @@ -311,14 +299,14 @@ void apply_markov_transition_steps(const MeshT &mesh, template Eigen::MatrixXf compute_1form_gram_matrix(const MeshT &mesh, float bandwidth, GeometryWorkspace &workspace) { - const int n_basis = mesh.topology.eigen_basis.cols(); + const int n_basis = mesh.structure.eigen_basis.cols(); const int n_total = n_basis * 3; Eigen::MatrixXf G = Eigen::MatrixXf::Zero(n_total, n_total); fill_coordinate_vectors(mesh, workspace.coords); - const int n_verts = static_cast(mesh.geometry.num_points()); + const int n_verts = static_cast(mesh.num_points()); for (int a = 0; a < 3; ++a) { for (int b = a; b < 3; ++b) { workspace.gamma_coords[a][b].resize(n_verts); @@ -330,8 +318,8 @@ Eigen::MatrixXf compute_1form_gram_matrix(const MeshT &mesh, float bandwidth, } } - const auto &U = mesh.topology.eigen_basis; - const auto &mu = mesh.topology.mu; + const auto &U = mesh.structure.eigen_basis; + const auto &mu = mesh.structure.mu; core::parallel_for_index( 0, n_basis, @@ -367,4 +355,4 @@ Eigen::MatrixXf compute_1form_gram_matrix(const MeshT &mesh, float bandwidth) { return compute_1form_gram_matrix(mesh, bandwidth, workspace); } -} // namespace igneous::ops +} // namespace igneous::ops::diffusion diff --git a/include/igneous/ops/diffusion/hodge.hpp b/include/igneous/ops/diffusion/hodge.hpp index c1e7377..18a2cb6 100644 --- a/include/igneous/ops/diffusion/hodge.hpp +++ b/include/igneous/ops/diffusion/hodge.hpp @@ -11,12 +11,12 @@ #include #include -#include +#include #include #include #include -namespace igneous::ops { +namespace igneous::ops::diffusion { template struct HodgeWorkspace { std::array coords; @@ -33,10 +33,10 @@ template Eigen::MatrixXf compute_weak_exterior_derivative(const MeshT &mesh, float bandwidth, HodgeWorkspace &workspace) { - const auto &U = mesh.topology.eigen_basis; - const auto &mu = mesh.topology.mu; + const auto &U = mesh.structure.eigen_basis; + const auto &mu = mesh.structure.mu; const int n0 = U.cols(); - const int n_verts = static_cast(mesh.geometry.num_points()); + const int n_verts = static_cast(mesh.num_points()); Eigen::MatrixXf D = Eigen::MatrixXf::Zero(3 * n0, n0); @@ -83,12 +83,12 @@ Eigen::MatrixXf compute_weak_exterior_derivative(const MeshT &mesh, template Eigen::MatrixXf compute_curl_energy_matrix(const MeshT &mesh, float bandwidth, HodgeWorkspace &workspace) { - const auto &U = mesh.topology.eigen_basis; - const auto &mu = mesh.topology.mu; + const auto &U = mesh.structure.eigen_basis; + const auto &mu = mesh.structure.mu; const auto mu_arr = mu.array(); const int n0 = U.cols(); const int n_basis = 3 * n0; - const int n_verts = static_cast(mesh.geometry.num_points()); + const int n_verts = static_cast(mesh.num_points()); Eigen::MatrixXf E_up = Eigen::MatrixXf::Zero(n_basis, n_basis); @@ -210,11 +210,11 @@ Eigen::VectorXf compute_circular_coordinates(const MeshT &mesh, float lambda = 1.0f, int positive_imag_mode = 0, std::complex *selected_eval = nullptr) { - const auto &U = mesh.topology.eigen_basis; - const auto &mu = mesh.topology.mu; + const auto &U = mesh.structure.eigen_basis; + const auto &mu = mesh.structure.mu; const int n0 = U.cols(); - const size_t n_verts = mesh.geometry.num_points(); + const size_t n_verts = mesh.num_points(); const int n_verts_i = static_cast(n_verts); HodgeWorkspace workspace; @@ -339,4 +339,4 @@ Eigen::VectorXf compute_circular_coordinates(const MeshT &mesh, return theta; } -} // namespace igneous::ops +} // namespace igneous::ops::diffusion diff --git a/include/igneous/ops/diffusion/products.hpp b/include/igneous/ops/diffusion/products.hpp index 5b557c2..6eb1f55 100644 --- a/include/igneous/ops/diffusion/products.hpp +++ b/include/igneous/ops/diffusion/products.hpp @@ -7,7 +7,7 @@ #include #include -namespace igneous::ops { +namespace igneous::ops::diffusion { template Eigen::VectorXf compute_wedge_product_coeffs( @@ -19,7 +19,7 @@ Eigen::VectorXf compute_wedge_product_coeffs( const int k_total = k1 + k2; const int n1 = std::max(1, std::min(n_coefficients, - static_cast(mesh.topology.eigen_basis.cols()))); + static_cast(mesh.structure.eigen_basis.cols()))); if (k_total > d) { return Eigen::VectorXf::Zero(n1); @@ -75,7 +75,7 @@ Eigen::MatrixXf compute_wedge_operator_matrix( const int d = ambient_dim_3d(); const int n1 = std::max(1, std::min(n_coefficients, - static_cast(mesh.topology.eigen_basis.cols()))); + static_cast(mesh.structure.eigen_basis.cols()))); const int in_dim = n1 * std::max(1, binomial_coeff(d, k_right)); const int out_dim = n1 * std::max(1, binomial_coeff(d, k_left + k_right)); @@ -103,4 +103,4 @@ Eigen::MatrixXf compute_wedge_operator_matrix(const MeshT &mesh, n_coefficients, workspace); } -} // namespace igneous::ops +} // namespace igneous::ops::diffusion diff --git a/include/igneous/ops/diffusion/spectral.hpp b/include/igneous/ops/diffusion/spectral.hpp index f6a0c12..5f00d40 100644 --- a/include/igneous/ops/diffusion/spectral.hpp +++ b/include/igneous/ops/diffusion/spectral.hpp @@ -6,11 +6,11 @@ #include #include #include -#include +#include #include #include -namespace igneous::ops { +namespace igneous::ops::diffusion { class MarkovCsrMatProd { public: @@ -226,18 +226,16 @@ void compute_eigenbasis(MeshT &mesh, int n_eigenvectors) { << " eigenfunctions...\n"; } - const int n = [&]() { - if constexpr (requires { mesh.topology.markov_row_offsets; }) { - if (mesh.topology.markov_row_offsets.empty()) { - return 0; - } - return static_cast(mesh.topology.markov_row_offsets.size() - 1); - } else if constexpr (requires { mesh.topology.P; }) { - return static_cast(mesh.topology.P.rows()); - } else { - return 0; - } - }(); + static_assert(requires { + mesh.structure.markov_row_offsets; + mesh.structure.markov_col_indices; + mesh.structure.markov_values; + }, + "compute_eigenbasis requires markov CSR arrays."); + + const int n = mesh.structure.markov_row_offsets.empty() + ? 0 + : static_cast(mesh.structure.markov_row_offsets.size() - 1); const auto solve_with_op = [&](auto &op) { using OpType = std::decay_t; @@ -260,25 +258,25 @@ void compute_eigenbasis(MeshT &mesh, int n_eigenvectors) { nconv = fallback.compute(Spectra::SortRule::LargestReal); if (fallback.info() == Spectra::CompInfo::Successful) { - mesh.topology.eigen_basis = fallback.eigenvectors(nconv).real(); + mesh.structure.eigen_basis = fallback.eigenvectors(nconv).real(); if (verbose) { std::cout << "[Spectral] Converged! Found " << nconv << " eigenvectors. Basis shape: " - << mesh.topology.eigen_basis.rows() << "x" - << mesh.topology.eigen_basis.cols() << "\n"; + << mesh.structure.eigen_basis.rows() << "x" + << mesh.structure.eigen_basis.cols() << "\n"; } return; } } if (eigs.info() == Spectra::CompInfo::Successful) { - mesh.topology.eigen_basis = eigs.eigenvectors(nconv).real(); + mesh.structure.eigen_basis = eigs.eigenvectors(nconv).real(); if (verbose) { std::cout << "[Spectral] Converged! Found " << nconv << " eigenvectors. Basis shape: " - << mesh.topology.eigen_basis.rows() << "x" - << mesh.topology.eigen_basis.cols() << "\n"; + << mesh.structure.eigen_basis.rows() << "x" + << mesh.structure.eigen_basis.cols() << "\n"; } return; } @@ -289,93 +287,79 @@ void compute_eigenbasis(MeshT &mesh, int n_eigenvectors) { }; if (n <= 1) { - mesh.topology.eigen_basis = + mesh.structure.eigen_basis = Eigen::MatrixXf::Ones(std::max(1, n), std::max(1, n)); return; } if constexpr (requires { - mesh.topology.markov_row_offsets; - mesh.topology.markov_col_indices; - mesh.topology.markov_values; + mesh.structure.symmetric_row_offsets; + mesh.structure.symmetric_col_indices; + mesh.structure.symmetric_values; + mesh.structure.symmetric_row_sums; }) { - if constexpr (requires { - mesh.topology.symmetric_row_offsets; - mesh.topology.symmetric_col_indices; - mesh.topology.symmetric_values; - mesh.topology.symmetric_row_sums; - }) { - const auto &sym_row_offsets = mesh.topology.symmetric_row_offsets; - const auto &sym_col_indices = mesh.topology.symmetric_col_indices; - const auto &sym_values = mesh.topology.symmetric_values; - const auto &row_sums = mesh.topology.symmetric_row_sums; - if (!sym_row_offsets.empty() && - static_cast(sym_row_offsets.size()) == n + 1 && - row_sums.size() == n) { - const int k_eval = std::max(1, std::min(n_eigenvectors, std::max(1, n - 1))); - const int full_ncv = std::min(n, std::max(2 * k_eval + 1, 20)); - const bool try_compact = k_eval >= 32; - const int compact_ncv = - try_compact ? std::min(n, std::max(k_eval + 16, 20)) : full_ncv; - - Eigen::VectorXf inv_sqrt_rows(n); - for (int i = 0; i < n; ++i) { - inv_sqrt_rows[i] = 1.0f / std::sqrt(std::max(row_sums[i], 1e-12f)); - } - - NormalizedSymmetricKernelCsrMatProd op(sym_row_offsets, sym_col_indices, - sym_values, inv_sqrt_rows, n); - const auto solve_symmetric = [&](int ncv) -> bool { - Spectra::SymEigsSolver eigs(op, - k_eval, - ncv); - eigs.init(); - const int nconv = eigs.compute(Spectra::SortRule::LargestMagn); - if (eigs.info() != Spectra::CompInfo::Successful || nconv <= 0) { - return false; - } - - Eigen::MatrixXf basis = eigs.eigenvectors(nconv); - basis = basis.array().colwise() * inv_sqrt_rows.array(); - if (std::abs(basis(0, 0)) > 1e-12f) { - basis /= basis(0, 0); - } - mesh.topology.eigen_basis = basis; + const auto &sym_row_offsets = mesh.structure.symmetric_row_offsets; + const auto &sym_col_indices = mesh.structure.symmetric_col_indices; + const auto &sym_values = mesh.structure.symmetric_values; + const auto &row_sums = mesh.structure.symmetric_row_sums; + if (!sym_row_offsets.empty() && + static_cast(sym_row_offsets.size()) == n + 1 && + row_sums.size() == n) { + const int k_eval = std::max(1, std::min(n_eigenvectors, std::max(1, n - 1))); + const int full_ncv = std::min(n, std::max(2 * k_eval + 1, 20)); + const bool try_compact = k_eval >= 32; + const int compact_ncv = + try_compact ? std::min(n, std::max(k_eval + 16, 20)) : full_ncv; + + Eigen::VectorXf inv_sqrt_rows(n); + for (int i = 0; i < n; ++i) { + inv_sqrt_rows[i] = 1.0f / std::sqrt(std::max(row_sums[i], 1e-12f)); + } - if (verbose) { - std::cout << "[Spectral] Converged! Found " << nconv - << " eigenvectors. Basis shape: " - << mesh.topology.eigen_basis.rows() << "x" - << mesh.topology.eigen_basis.cols() << "\n"; - } - return true; - }; + NormalizedSymmetricKernelCsrMatProd op(sym_row_offsets, sym_col_indices, + sym_values, inv_sqrt_rows, n); + const auto solve_symmetric = [&](int ncv) -> bool { + Spectra::SymEigsSolver eigs(op, + k_eval, + ncv); + eigs.init(); + const int nconv = eigs.compute(Spectra::SortRule::LargestMagn); + if (eigs.info() != Spectra::CompInfo::Successful || nconv <= 0) { + return false; + } - if ((try_compact && solve_symmetric(compact_ncv)) || - solve_symmetric(full_ncv)) { - return; + Eigen::MatrixXf basis = eigs.eigenvectors(nconv); + basis = basis.array().colwise() * inv_sqrt_rows.array(); + if (std::abs(basis(0, 0)) > 1e-12f) { + basis /= basis(0, 0); } + mesh.structure.eigen_basis = basis; if (verbose) { - std::cerr << "[Spectral] Symmetric-kernel solve failed, falling back to " - "generic solver.\n"; + std::cout << "[Spectral] Converged! Found " << nconv + << " eigenvectors. Basis shape: " + << mesh.structure.eigen_basis.rows() << "x" + << mesh.structure.eigen_basis.cols() << "\n"; } + return true; + }; + + if ((try_compact && solve_symmetric(compact_ncv)) || + solve_symmetric(full_ncv)) { + return; } - } - MarkovCsrMatProd fallback_op(mesh.topology.markov_row_offsets, - mesh.topology.markov_col_indices, - mesh.topology.markov_values, n); - solve_with_op(fallback_op); - } else if constexpr (requires { mesh.topology.P; }) { - const auto &P = mesh.topology.P; - Spectra::SparseGenMatProd op(P); - solve_with_op(op); - } else { - static_assert( - requires { mesh.topology.P; }, - "compute_eigenbasis requires markov CSR arrays or sparse matrix P."); + if (verbose) { + std::cerr << "[Spectral] Symmetric-kernel solve failed, falling back to " + "generic solver.\n"; + } + } } + + MarkovCsrMatProd fallback_op(mesh.structure.markov_row_offsets, + mesh.structure.markov_col_indices, + mesh.structure.markov_values, n); + solve_with_op(fallback_op); } -} // namespace igneous::ops +} // namespace igneous::ops::diffusion diff --git a/include/igneous/ops/transform.hpp b/include/igneous/ops/transform.hpp index 7397df8..458dd97 100644 --- a/include/igneous/ops/transform.hpp +++ b/include/igneous/ops/transform.hpp @@ -2,13 +2,13 @@ #include #include -#include +#include #include namespace igneous::ops { -template void normalize(data::Mesh &mesh) { - const size_t n_verts = mesh.geometry.num_points(); +template void normalize(data::Space &mesh) { + const size_t n_verts = mesh.num_points(); if (n_verts == 0) { return; } @@ -19,7 +19,7 @@ template void normalize(data::Mesh &mes std::numeric_limits::lowest()}; for (size_t i = 0; i < n_verts; ++i) { - const core::Vec3 p = mesh.geometry.get_vec3(i); + const core::Vec3 p = mesh.get_vec3(i); min_p.x = std::min(min_p.x, p.x); min_p.y = std::min(min_p.y, p.y); min_p.z = std::min(min_p.z, p.z); @@ -35,8 +35,8 @@ template void normalize(data::Mesh &mes const float scale_scalar = (max_dim > 1e-9f) ? (2.0f / max_dim) : 1.0f; for (size_t i = 0; i < n_verts; ++i) { - const core::Vec3 p = mesh.geometry.get_vec3(i); - mesh.geometry.set_vec3(i, (p - center) * scale_scalar); + const core::Vec3 p = mesh.get_vec3(i); + mesh.set_vec3(i, (p - center) * scale_scalar); } } diff --git a/notes/structure_refactor/journal.md b/notes/structure_refactor/journal.md new file mode 100644 index 0000000..fdc6483 --- /dev/null +++ b/notes/structure_refactor/journal.md @@ -0,0 +1,90 @@ +# Structure Refactor Journal + +## Entry 0001 +- Timestamp: 2026-02-15 +- Hypothesis: Hard cutover to structure-first API (`Space` + `Structure`) simplifies data and ops surfaces while preserving behavior. +- Structural Difference Targeted: Replace mesh/topology/buffer model with split structures and flattened space geometry. +- Baseline Verification: + - `ctest --test-dir build --output-on-failure` -> `14/14` passed. +- Decisions: + - `Topology` -> `Structure`, `SurfaceTopology` -> `SurfaceStructure` + - `TriangleTopology` -> `DiscreteExteriorCalculus` + - `DiffusionTopology` -> `DiffusionGeometry` + - `Mesh` -> `Space`, no compatibility aliases + - Remove `PointTopology` and `GeometryBuffer` + - Explicit structure build policy in IO callers + - CLI rename to `diffusion-geometry` +- Status: in_progress + +## Entry 0002 +- Timestamp: 2026-02-15 +- Structural Difference Targeted: Complete hard cutover in data and ops layers. +- Files Added: + - `include/igneous/data/structure.hpp` + - `include/igneous/data/space.hpp` + - `include/igneous/data/structures/discrete_exterior_calculus.hpp` + - `include/igneous/data/structures/diffusion_geometry.hpp` + - `include/igneous/ops/dec/curvature.hpp` + - `include/igneous/ops/dec/flow.hpp` +- Files Removed: + - `include/igneous/data/topology.hpp` + - `include/igneous/data/mesh.hpp` + - `include/igneous/data/buffers.hpp` + - `include/igneous/ops/curvature.hpp` + - `include/igneous/ops/flow.hpp` +- Decisions: + - Keep `igneous::ops` root; structure ops grouped under `ops::dec` and `ops::diffusion`. + - Remove sparse-matrix `structure.P` fallback paths and standardize spectral flow on CSR arrays in `DiffusionGeometry`. +- Verification: + - `cmake --build build --parallel` -> pass. + +## Entry 0003 +- Timestamp: 2026-02-15 +- Structural Difference Targeted: Enforce explicit build policy after load and propagate API cutover through callsites/tests. +- Callsite Policy Updates: + - `io::load_obj` remains load-only. + - DEC and diffusion callers explicitly invoke `space.structure.build(...)` when downstream ops require built structures. +- Key Test Update: + - `tests/test_io_meshes.cpp` now asserts load-only state first, then explicit `structure.build(...)` for DEC and diffusion checks. +- Verification: + - `ctest --test-dir build --output-on-failure` initially failed at `test_io_meshes` due to old implicit-build expectation. + - After explicit-build updates, `ctest --test-dir build --output-on-failure` -> `14/14` passed. + +## Entry 0004 +- Timestamp: 2026-02-15 +- Structural Difference Targeted: Rename topology-oriented CLI and structure tests to final terminology. +- Renames: + - `src/main_diffusion_topology.cpp` -> `src/main_diffusion_geometry.cpp` + - `tests/test_topology_triangle.cpp` -> `tests/test_structure_dec.cpp` + - `tests/test_topology_diffusion.cpp` -> `tests/test_structure_diffusion_geometry.cpp` + - `visualizations/view_main_diffusion_topology.py` -> `visualizations/view_main_diffusion_geometry.py` + - CMake target `igneous-diffusion-topology` -> `igneous-diffusion-geometry` +- Dependent Surfaces Updated: + - `tests/test_diffgeo_cli_outputs.sh` + - `scripts/diffgeo/run_cpp_diffgeo_ops.sh` + - `scripts/diffgeo/run_parity_round.sh` + - `visualizations/README.md` + - `README.md` + - Diffgeo parity helper messaging/scripts under `scripts/diffgeo/` and `tests/`. +- Verification: + - `cmake -S . -B build -G Ninja && cmake --build build --parallel` -> pass. + - `ctest --test-dir build --output-on-failure` -> `14/14` passed. + - Benchmark smoke: + - `IGNEOUS_BENCH_MODE=1 ./build/bench_geometry` -> pass + - `IGNEOUS_BENCH_MODE=1 ./build/bench_dod --benchmark_min_time=0.02s --benchmark_repetitions=1 --benchmark_report_aggregates_only=true` -> pass + - `IGNEOUS_BENCH_MODE=1 ./build/bench_pipelines --benchmark_min_time=0.02s --benchmark_repetitions=1 --benchmark_report_aggregates_only=true` -> pass +- Status: completed + +## Entry 0005 +- Timestamp: 2026-02-15 +- Structural Difference Targeted: Remove remaining non-historical `topology` terminology from active surfaces. +- Updates: + - Renamed benchmark labels/functions to `structure` terminology where applicable. + - Updated `README.md` benchmark list names to match renamed benchmark labels. + - Updated perf comparator metric key `bench_geometry_topology_ms` -> `bench_geometry_structure_ms`. +- Verification: + - `cmake --build build --parallel && ctest --test-dir build --output-on-failure` -> pass. + - Benchmark smoke rerun passed: + - `IGNEOUS_BENCH_MODE=1 ./build/bench_geometry` + - `IGNEOUS_BENCH_MODE=1 ./build/bench_dod --benchmark_min_time=0.02s --benchmark_repetitions=1 --benchmark_report_aggregates_only=true` + - `IGNEOUS_BENCH_MODE=1 ./build/bench_pipelines --benchmark_min_time=0.02s --benchmark_repetitions=1 --benchmark_report_aggregates_only=true` diff --git a/scripts/diffgeo/compare_diffgeo_ops.py b/scripts/diffgeo/compare_diffgeo_ops.py index d1e35a4..af96129 100755 --- a/scripts/diffgeo/compare_diffgeo_ops.py +++ b/scripts/diffgeo/compare_diffgeo_ops.py @@ -314,13 +314,13 @@ def render_markdown(payload: dict[str, Any]) -> str: def main() -> int: - parser = argparse.ArgumentParser(description="Compare diffusion topology parity outputs") + parser = argparse.ArgumentParser(description="Compare diffusion geometry parity outputs") parser.add_argument("--reference-root", required=True) parser.add_argument("--cpp-root", required=True) parser.add_argument("--datasets", default="torus,sphere") parser.add_argument("--output-markdown", required=True) parser.add_argument("--output-json", required=True) - parser.add_argument("--label", default="Diffusion topology parity") + parser.add_argument("--label", default="Diffusion geometry parity") parser.add_argument("--previous-json", default="") parser.add_argument("--gate-improvement", type=float, default=0.20) parser.add_argument("--rcond", type=float, default=1e-10) diff --git a/scripts/diffgeo/run_cpp_diffgeo_ops.sh b/scripts/diffgeo/run_cpp_diffgeo_ops.sh index 4348d41..6cac5e6 100755 --- a/scripts/diffgeo/run_cpp_diffgeo_ops.sh +++ b/scripts/diffgeo/run_cpp_diffgeo_ops.sh @@ -8,7 +8,7 @@ INPUT_CSV="${1:?first arg must be input csv path}" OUTPUT_DIR="${2:?second arg must be output dir path}" BUILD_DIR="${BUILD_DIR:-${ROOT_DIR}/build}" -EXE="${BUILD_DIR}/igneous-diffusion-topology" +EXE="${BUILD_DIR}/igneous-diffusion-geometry" N_BASIS="${N_BASIS:-50}" N_COEFFICIENTS="${N_COEFFICIENTS:-50}" diff --git a/scripts/diffgeo/run_parity_round.sh b/scripts/diffgeo/run_parity_round.sh index 250110e..2b49755 100755 --- a/scripts/diffgeo/run_parity_round.sh +++ b/scripts/diffgeo/run_parity_round.sh @@ -23,7 +23,7 @@ source "${VENV_PATH}/bin/activate" PYTHON_BIN="${VENV_PATH}/bin/python" BUILD_DIR="${BUILD_DIR:-${ROOT_DIR}/build}" -if [[ ! -x "${BUILD_DIR}/igneous-diffusion-topology" ]]; then +if [[ ! -x "${BUILD_DIR}/igneous-diffusion-geometry" ]]; then cmake -S "${ROOT_DIR}" -B "${BUILD_DIR}" cmake --build "${BUILD_DIR}" -j8 fi @@ -97,7 +97,7 @@ COMPARE_ARGS=( --cpp-root "${CPP_ROOT}" --output-markdown "${REPORT_DIR}/parity_report.md" --output-json "${REPORT_DIR}/parity_report.json" - --label "Diffusion topology parity round ${ROUND_TAG}" + --label "Diffusion geometry parity round ${ROUND_TAG}" ) if [[ -n "${PREVIOUS_REPORT_JSON:-}" ]]; then diff --git a/scripts/diffgeo/run_reference_diffgeo_ops.py b/scripts/diffgeo/run_reference_diffgeo_ops.py index 9af9423..b8dd78a 100755 --- a/scripts/diffgeo/run_reference_diffgeo_ops.py +++ b/scripts/diffgeo/run_reference_diffgeo_ops.py @@ -98,7 +98,7 @@ def circular_coordinates(form, lam: float, mode: int): def main() -> None: - parser = argparse.ArgumentParser(description="Run reference diffusion topology ops") + parser = argparse.ArgumentParser(description="Run reference diffusion geometry ops") parser.add_argument("--input-csv", required=True) parser.add_argument("--output-dir", required=True) parser.add_argument("--n-function-basis", type=int, default=50) diff --git a/scripts/perf/compare_against_main.py b/scripts/perf/compare_against_main.py index d710fd4..001e4dc 100755 --- a/scripts/perf/compare_against_main.py +++ b/scripts/perf/compare_against_main.py @@ -118,7 +118,7 @@ def parse_benchmark_text(path: Path) -> Dict[str, float]: flow_ms = float(g.group(4)) frame_ms = topo_ms + curv_ms + flow_ms - rows.setdefault(f"bench_geometry_topology_ms/{grid}", []).append(to_ns(topo_ms, "ms")) + rows.setdefault(f"bench_geometry_structure_ms/{grid}", []).append(to_ns(topo_ms, "ms")) rows.setdefault(f"bench_geometry_curvature_ms/{grid}", []).append(to_ns(curv_ms, "ms")) rows.setdefault(f"bench_geometry_flow_ms/{grid}", []).append(to_ns(flow_ms, "ms")) rows.setdefault(f"bench_geometry_frame_ms/{grid}", []).append(to_ns(frame_ms, "ms")) diff --git a/src/main_diffusion.cpp b/src/main_diffusion.cpp index 7f1b77d..46f57ea 100644 --- a/src/main_diffusion.cpp +++ b/src/main_diffusion.cpp @@ -6,14 +6,14 @@ #include #include -#include +#include #include #include #include #include using namespace igneous; -using DiffusionMesh = data::Mesh; +using DiffusionMesh = data::Space; static std::vector to_std_vector(const Eigen::VectorXf &v) { std::vector out(static_cast(v.size())); @@ -41,27 +41,27 @@ int main(int argc, char **argv) { io::load_obj(mesh, input_path); ops::normalize(mesh); - mesh.topology.build({mesh.geometry.x_span(), mesh.geometry.y_span(), - mesh.geometry.z_span(), 32}); + mesh.structure.build({mesh.x_span(), mesh.y_span(), + mesh.z_span(), 32}); - const int n = static_cast(mesh.geometry.num_points()); + const int n = static_cast(mesh.num_points()); std::cout << "Markov Chain P: " << n << "x" << n << " (" - << mesh.topology.markov_values.size() << " nnz)\n"; + << mesh.structure.markov_values.size() << " nnz)\n"; - auto density_field = to_std_vector(mesh.topology.mu); + auto density_field = to_std_vector(mesh.structure.mu); if (!bench_mode) { io::export_ply_solid(mesh, density_field, output_dir + "/00_density.ply", 0.01); } - const size_t n_verts = mesh.geometry.num_points(); + const size_t n_verts = mesh.num_points(); Eigen::VectorXf u = Eigen::VectorXf::Zero(static_cast(n_verts)); Eigen::VectorXf u_next = Eigen::VectorXf::Zero(static_cast(n_verts)); - ops::DiffusionWorkspace diffusion_ws; + ops::diffusion::DiffusionWorkspace diffusion_ws; int max_y_idx = 0; float max_y = -1e9f; for (size_t i = 0; i < n_verts; ++i) { - const auto p = mesh.geometry.get_vec3(i); + const auto p = mesh.get_vec3(i); if (p.y > max_y) { max_y = p.y; max_y_idx = static_cast(i); @@ -75,11 +75,11 @@ int main(int argc, char **argv) { const int steps = bench_mode ? 20 : 100; if (bench_mode) { - ops::apply_markov_transition_steps(mesh, u, steps, u_next, diffusion_ws); + ops::diffusion::apply_markov_transition_steps(mesh, u, steps, u_next, diffusion_ws); u.swap(u_next); } else { for (int t = 1; t <= steps; ++t) { - ops::apply_markov_transition(mesh, u, u_next); + ops::diffusion::apply_markov_transition(mesh, u, u_next); u.swap(u_next); if (t % 5 == 0) { diff --git a/src/main_diffusion_topology.cpp b/src/main_diffusion_geometry.cpp similarity index 82% rename from src/main_diffusion_topology.cpp rename to src/main_diffusion_geometry.cpp index 000a34c..a015c23 100644 --- a/src/main_diffusion_topology.cpp +++ b/src/main_diffusion_geometry.cpp @@ -11,7 +11,7 @@ #include #include -#include +#include #include #include #include @@ -19,11 +19,11 @@ #include using namespace igneous; -using DiffusionMesh = data::Mesh; +using DiffusionMesh = data::Space; struct Config { std::string input_csv; - std::string output_dir = "output_diffusion_topology"; + std::string output_dir = "output_diffusion_geometry"; size_t n_points = 1000; float major_radius = 2.0f; float minor_radius = 1.0f; @@ -47,7 +47,7 @@ struct Config { static void print_usage() { std::cout - << "Usage: ./build/igneous-diffusion-topology [options]\n" + << "Usage: ./build/igneous-diffusion-geometry [options]\n" << " --input-csv \n" << " --output-dir \n" << " --n-points \n" @@ -167,7 +167,7 @@ static bool parse_args(int argc, char **argv, Config &cfg) { static void generate_torus(DiffusionMesh &mesh, size_t n_points, float major_radius, float minor_radius, uint32_t seed) { mesh.clear(); - mesh.geometry.reserve(n_points); + mesh.reserve(n_points); std::mt19937 gen(seed); std::uniform_real_distribution dist(0.0f, 6.28318530718f); @@ -178,14 +178,14 @@ static void generate_torus(DiffusionMesh &mesh, size_t n_points, float major_rad const float x = (major_radius + minor_radius * std::cos(v)) * std::cos(u); const float y = (major_radius + minor_radius * std::cos(v)) * std::sin(u); const float z = minor_radius * std::sin(v); - mesh.geometry.push_point({x, y, z}); + mesh.push_point({x, y, z}); } } static void generate_sphere(DiffusionMesh &mesh, size_t n_points, float radius, uint32_t seed) { mesh.clear(); - mesh.geometry.reserve(n_points); + mesh.reserve(n_points); std::mt19937 gen(seed); std::uniform_real_distribution u01(0.0f, 1.0f); @@ -196,7 +196,7 @@ static void generate_sphere(DiffusionMesh &mesh, size_t n_points, float radius, const float theta = 2.0f * 3.14159265358979323846f * u; const float phi = std::acos(2.0f * v - 1.0f); const float sin_phi = std::sin(phi); - mesh.geometry.push_point({radius * sin_phi * std::cos(theta), + mesh.push_point({radius * sin_phi * std::cos(theta), radius * sin_phi * std::sin(theta), radius * std::cos(phi)}); } @@ -229,10 +229,10 @@ static bool load_point_cloud_csv(const std::string &filename, DiffusionMesh &mes if (!(iss >> x >> y >> z)) { continue; } - mesh.geometry.push_point({x, y, z}); + mesh.push_point({x, y, z}); } - return mesh.geometry.num_points() > 0; + return mesh.num_points() > 0; } static std::vector to_scalar_field(const Eigen::VectorXf &values) { @@ -251,7 +251,7 @@ static void export_vector_field(const std::string &filename, return; } - const size_t n = mesh.geometry.num_points(); + const size_t n = mesh.num_points(); file << "ply\n"; file << "format ascii 1.0\n"; file << "element vertex " << n << "\n"; @@ -264,7 +264,7 @@ static void export_vector_field(const std::string &filename, file << "end_header\n"; for (size_t i = 0; i < n; ++i) { - const auto p = mesh.geometry.get_vec3(i); + const auto p = mesh.get_vec3(i); const auto v = vectors[i]; file << p.x << " " << p.y << " " << p.z << " " << v.x << " " << v.y << " " << v.z << "\n"; @@ -275,14 +275,14 @@ static std::array, 3> build_gamma_data_immersion(const DiffusionMesh &mesh) { std::array data_coords; std::array immersion_coords; - ops::fill_data_coordinate_vectors(mesh, data_coords); - ops::fill_coordinate_vectors(mesh, immersion_coords); + ops::diffusion::fill_data_coordinate_vectors(mesh, data_coords); + ops::diffusion::fill_coordinate_vectors(mesh, immersion_coords); std::array, 3> gamma{}; for (int a = 0; a < 3; ++a) { for (int b = 0; b < 3; ++b) { - gamma[a][b].resize(static_cast(mesh.geometry.num_points())); - ops::carre_du_champ(mesh, data_coords[a], immersion_coords[b], 0.0f, + gamma[a][b].resize(static_cast(mesh.num_points())); + ops::diffusion::carre_du_champ(mesh, data_coords[a], immersion_coords[b], 0.0f, gamma[a][b]); } } @@ -293,13 +293,13 @@ static std::vector reconstruct_1form_ambient( const DiffusionMesh &mesh, const Eigen::VectorXf &coeffs, int n_coefficients, const std::array, 3> &gamma_data_immersion) { const Eigen::MatrixXf pointwise = - ops::coefficients_to_pointwise(mesh, coeffs, 1, n_coefficients); - std::vector field(mesh.geometry.num_points(), {0.0f, 0.0f, 0.0f}); + ops::diffusion::coefficients_to_pointwise(mesh, coeffs, 1, n_coefficients); + std::vector field(mesh.num_points(), {0.0f, 0.0f, 0.0f}); if (pointwise.rows() == 0 || pointwise.cols() < 3) { return field; } - for (size_t i = 0; i < mesh.geometry.num_points(); ++i) { + for (size_t i = 0; i < mesh.num_points(); ++i) { const int p = static_cast(i); const float vx = gamma_data_immersion[0][0][p] * pointwise(p, 0) + gamma_data_immersion[0][1][p] * pointwise(p, 1) + @@ -319,13 +319,13 @@ static std::vector reconstruct_2form_dual_ambient( const DiffusionMesh &mesh, const Eigen::VectorXf &coeffs, int n_coefficients, const std::array, 3> &gamma_data_immersion) { const Eigen::MatrixXf pointwise = - ops::coefficients_to_pointwise(mesh, coeffs, 2, n_coefficients); - std::vector field(mesh.geometry.num_points(), {0.0f, 0.0f, 0.0f}); + ops::diffusion::coefficients_to_pointwise(mesh, coeffs, 2, n_coefficients); + std::vector field(mesh.num_points(), {0.0f, 0.0f, 0.0f}); if (pointwise.rows() == 0 || pointwise.cols() < 3) { return field; } - for (size_t i = 0; i < mesh.geometry.num_points(); ++i) { + for (size_t i = 0; i < mesh.num_points(); ++i) { const int p = static_cast(i); const float w01 = pointwise(p, 0); const float w02 = pointwise(p, 1); @@ -383,29 +383,29 @@ int main(int argc, char **argv) { generate_torus(mesh, cfg.n_points, cfg.major_radius, cfg.minor_radius, cfg.seed); } - mesh.topology.build({mesh.geometry.x_span(), - mesh.geometry.y_span(), - mesh.geometry.z_span(), + mesh.structure.build({mesh.x_span(), + mesh.y_span(), + mesh.z_span(), cfg.k_neighbors, cfg.knn_bandwidth, cfg.bandwidth_variability, cfg.c, true}); - ops::compute_eigenbasis(mesh, cfg.n_basis); + ops::diffusion::compute_eigenbasis(mesh, cfg.n_basis); const int n_coeff = std::max(1, std::min(cfg.n_coefficients, - static_cast(mesh.topology.eigen_basis.cols()))); + static_cast(mesh.structure.eigen_basis.cols()))); - ops::DiffusionFormWorkspace forms_ws; + ops::diffusion::DiffusionFormWorkspace forms_ws; // 1-forms - const Eigen::MatrixXf G1 = ops::compute_kform_gram_matrix(mesh, 1, n_coeff, forms_ws); + const Eigen::MatrixXf G1 = ops::diffusion::compute_kform_gram_matrix(mesh, 1, n_coeff, forms_ws); const Eigen::MatrixXf down1 = - ops::compute_down_laplacian_matrix(mesh, 1, n_coeff, forms_ws); - const Eigen::MatrixXf up1 = ops::compute_up_laplacian_matrix(mesh, 1, n_coeff, forms_ws); - const Eigen::MatrixXf L1 = ops::assemble_hodge_laplacian_matrix(up1, down1); - auto [evals1, evecs1] = ops::compute_form_spectrum(L1, G1); + ops::diffusion::compute_down_laplacian_matrix(mesh, 1, n_coeff, forms_ws); + const Eigen::MatrixXf up1 = ops::diffusion::compute_up_laplacian_matrix(mesh, 1, n_coeff, forms_ws); + const Eigen::MatrixXf L1 = ops::diffusion::assemble_hodge_laplacian_matrix(up1, down1); + auto [evals1, evecs1] = ops::diffusion::compute_form_spectrum(L1, G1); if (evals1.size() == 0 || evecs1.cols() == 0) { std::cerr << "Failed to compute 1-form spectrum.\n"; @@ -413,7 +413,7 @@ int main(int argc, char **argv) { } const auto harmonic1_idx = - ops::extract_harmonic_mode_indices(evals1, cfg.harmonic_tolerance, 3); + ops::diffusion::extract_harmonic_mode_indices(evals1, cfg.harmonic_tolerance, 3); Eigen::MatrixXf harmonic1_coeffs(evecs1.rows(), static_cast(harmonic1_idx.size())); for (int c = 0; c < static_cast(harmonic1_idx.size()); ++c) { @@ -431,20 +431,20 @@ int main(int argc, char **argv) { const int idx0 = harmonic1_idx.empty() ? 0 : harmonic1_idx[0]; const int idx1 = harmonic1_idx.size() > 1 ? harmonic1_idx[1] : idx0; - const Eigen::VectorXf theta_0 = ops::compute_circular_coordinates( - mesh, pad_1form_coeffs(evecs1.col(idx0), mesh.topology.eigen_basis.cols(), n_coeff), + const Eigen::VectorXf theta_0 = ops::diffusion::compute_circular_coordinates( + mesh, pad_1form_coeffs(evecs1.col(idx0), mesh.structure.eigen_basis.cols(), n_coeff), 0.0f, cfg.circular_lambda, cfg.circular_mode_0, nullptr); - const Eigen::VectorXf theta_1 = ops::compute_circular_coordinates( - mesh, pad_1form_coeffs(evecs1.col(idx1), mesh.topology.eigen_basis.cols(), n_coeff), + const Eigen::VectorXf theta_1 = ops::diffusion::compute_circular_coordinates( + mesh, pad_1form_coeffs(evecs1.col(idx1), mesh.structure.eigen_basis.cols(), n_coeff), 0.0f, cfg.circular_lambda, cfg.circular_mode_1, nullptr); // 2-forms - const Eigen::MatrixXf G2 = ops::compute_kform_gram_matrix(mesh, 2, n_coeff, forms_ws); + const Eigen::MatrixXf G2 = ops::diffusion::compute_kform_gram_matrix(mesh, 2, n_coeff, forms_ws); const Eigen::MatrixXf down2 = - ops::compute_down_laplacian_matrix(mesh, 2, n_coeff, forms_ws); - const Eigen::MatrixXf up2 = ops::compute_up_laplacian_matrix(mesh, 2, n_coeff, forms_ws); - const Eigen::MatrixXf L2 = ops::assemble_hodge_laplacian_matrix(up2, down2); - auto [evals2, evecs2] = ops::compute_form_spectrum(L2, G2); + ops::diffusion::compute_down_laplacian_matrix(mesh, 2, n_coeff, forms_ws); + const Eigen::MatrixXf up2 = ops::diffusion::compute_up_laplacian_matrix(mesh, 2, n_coeff, forms_ws); + const Eigen::MatrixXf L2 = ops::diffusion::assemble_hodge_laplacian_matrix(up2, down2); + auto [evals2, evecs2] = ops::diffusion::compute_form_spectrum(L2, G2); if (evals2.size() == 0 || evecs2.cols() == 0) { std::cerr << "Failed to compute 2-form spectrum.\n"; @@ -452,7 +452,7 @@ int main(int argc, char **argv) { } const auto harmonic2_idx = - ops::extract_harmonic_mode_indices(evals2, cfg.harmonic_tolerance, 3); + ops::diffusion::extract_harmonic_mode_indices(evals2, cfg.harmonic_tolerance, 3); Eigen::MatrixXf harmonic2_coeffs(evecs2.rows(), static_cast(harmonic2_idx.size())); for (int c = 0; c < static_cast(harmonic2_idx.size()); ++c) { @@ -469,7 +469,7 @@ int main(int argc, char **argv) { Eigen::VectorXf wedge_coeffs; if (harmonic1_coeffs.cols() >= 1) { const int rhs_col = harmonic1_coeffs.cols() >= 2 ? 1 : 0; - wedge_coeffs = ops::compute_wedge_product_coeffs( + wedge_coeffs = ops::diffusion::compute_wedge_product_coeffs( mesh, harmonic1_coeffs.col(0), 1, harmonic1_coeffs.col(rhs_col), 1, n_coeff, forms_ws); } else { diff --git a/src/main_hodge.cpp b/src/main_hodge.cpp index dce3c90..b27c6a3 100644 --- a/src/main_hodge.cpp +++ b/src/main_hodge.cpp @@ -11,14 +11,14 @@ #include #include -#include +#include #include #include #include #include using namespace igneous; -using DiffusionMesh = data::Mesh; +using DiffusionMesh = data::Space; struct Config { std::string input_csv; @@ -148,7 +148,7 @@ static bool parse_args(int argc, char **argv, Config &cfg) { static void generate_torus(DiffusionMesh &mesh, size_t n_points, float major_radius, float minor_radius, uint32_t seed) { mesh.clear(); - mesh.geometry.reserve(n_points); + mesh.reserve(n_points); std::mt19937 gen(seed); std::uniform_real_distribution dist(0.0f, 6.28318530718f); @@ -159,7 +159,7 @@ static void generate_torus(DiffusionMesh &mesh, size_t n_points, float major_rad const float x = (major_radius + minor_radius * std::cos(v)) * std::cos(u); const float y = (major_radius + minor_radius * std::cos(v)) * std::sin(u); const float z = minor_radius * std::sin(v); - mesh.geometry.push_point({x, y, z}); + mesh.push_point({x, y, z}); } } @@ -190,18 +190,18 @@ static bool load_point_cloud_csv(const std::string &filename, DiffusionMesh &mes if (!(iss >> x >> y >> z)) { continue; } - mesh.geometry.push_point({x, y, z}); + mesh.push_point({x, y, z}); } - return mesh.geometry.num_points() > 0; + return mesh.num_points() > 0; } static void write_points_csv(const std::string &filename, const DiffusionMesh &mesh) { std::ofstream file(filename); file << "x,y,z\n"; - const size_t n = mesh.geometry.num_points(); + const size_t n = mesh.num_points(); for (size_t i = 0; i < n; ++i) { - const auto p = mesh.geometry.get_vec3(i); + const auto p = mesh.get_vec3(i); file << p.x << "," << p.y << "," << p.z << "\n"; } } @@ -237,20 +237,20 @@ static void write_harmonic_coeffs_csv(const std::string &filename, static std::vector reconstruct_harmonic_ambient(const DiffusionMesh &mesh, const Eigen::VectorXf &coeffs) { - const size_t n_verts = mesh.geometry.num_points(); - const int n_basis = mesh.topology.eigen_basis.cols(); - const auto &U = mesh.topology.eigen_basis; + const size_t n_verts = mesh.num_points(); + const int n_basis = mesh.structure.eigen_basis.cols(); + const auto &U = mesh.structure.eigen_basis; std::array data_coords; std::array immersion_coords; - ops::fill_data_coordinate_vectors(mesh, data_coords); - ops::fill_coordinate_vectors(mesh, immersion_coords); + ops::diffusion::fill_data_coordinate_vectors(mesh, data_coords); + ops::diffusion::fill_coordinate_vectors(mesh, immersion_coords); std::array, 3> gamma_data_imm{}; for (int a = 0; a < 3; ++a) { for (int b = 0; b < 3; ++b) { gamma_data_imm[a][b].resize(static_cast(n_verts)); - ops::carre_du_champ(mesh, data_coords[a], immersion_coords[b], 0.0f, + ops::diffusion::carre_du_champ(mesh, data_coords[a], immersion_coords[b], 0.0f, gamma_data_imm[a][b]); } } @@ -283,14 +283,14 @@ reconstruct_harmonic_ambient(const DiffusionMesh &mesh, static double orientation_score(const DiffusionMesh &mesh, const std::vector &field) { - const size_t n_verts = mesh.geometry.num_points(); + const size_t n_verts = mesh.num_points(); if (n_verts == 0) { return 0.0; } double accum = 0.0; for (size_t i = 0; i < n_verts; ++i) { - const auto p = mesh.geometry.get_vec3(i); + const auto p = mesh.get_vec3(i); const auto v = field[i]; accum += static_cast(p.x) * static_cast(v.x) + static_cast(p.y) * static_cast(v.y) + @@ -321,7 +321,7 @@ static void export_vector_field(const std::string &filename, const DiffusionMesh &mesh, const std::vector &vectors) { std::ofstream file(filename); - const size_t n = mesh.geometry.num_points(); + const size_t n = mesh.num_points(); file << "ply\nformat ascii 1.0\n"; file << "element vertex " << n << "\n"; @@ -330,7 +330,7 @@ static void export_vector_field(const std::string &filename, file << "end_header\n"; for (size_t i = 0; i < n; ++i) { - const auto p = mesh.geometry.get_vec3(i); + const auto p = mesh.get_vec3(i); const auto v = vectors[i]; file << p.x << " " << p.y << " " << p.z << " " << v.x << " " << v.y << " " << v.z << "\n"; @@ -349,9 +349,9 @@ static void write_harmonic_ambient_csv(const std::string &filename, } file << "\n"; - const size_t n = mesh.geometry.num_points(); + const size_t n = mesh.num_points(); for (size_t i = 0; i < n; ++i) { - const auto p = mesh.geometry.get_vec3(i); + const auto p = mesh.get_vec3(i); file << p.x << "," << p.y << "," << p.z; for (size_t form = 0; form < fields.size(); ++form) { const auto v = fields[form][i]; @@ -367,9 +367,9 @@ static void write_circular_csv(const std::string &filename, const Eigen::VectorXf &theta_1) { std::ofstream file(filename); file << "x,y,z,theta_0,theta_1\n"; - const size_t n = mesh.geometry.num_points(); + const size_t n = mesh.num_points(); for (size_t i = 0; i < n; ++i) { - const auto p = mesh.geometry.get_vec3(i); + const auto p = mesh.get_vec3(i); const int idx = static_cast(i); file << p.x << "," << p.y << "," << p.z << "," << theta_0[idx] << "," << theta_1[idx] << "\n"; @@ -409,26 +409,26 @@ int main(int argc, char **argv) { generate_torus(mesh, cfg.n_points, cfg.major_radius, cfg.minor_radius, cfg.seed); } - mesh.topology.build({mesh.geometry.x_span(), - mesh.geometry.y_span(), - mesh.geometry.z_span(), + mesh.structure.build({mesh.x_span(), + mesh.y_span(), + mesh.z_span(), cfg.k_neighbors, cfg.knn_bandwidth, cfg.bandwidth_variability, cfg.c, true}); - ops::compute_eigenbasis(mesh, cfg.n_basis); + ops::diffusion::compute_eigenbasis(mesh, cfg.n_basis); - ops::GeometryWorkspace geom_ws; - const auto G = ops::compute_1form_gram_matrix(mesh, 0.0f, geom_ws); + ops::diffusion::GeometryWorkspace geom_ws; + const auto G = ops::diffusion::compute_1form_gram_matrix(mesh, 0.0f, geom_ws); - ops::HodgeWorkspace hodge_ws; - const auto D_weak = ops::compute_weak_exterior_derivative(mesh, 0.0f, hodge_ws); - const auto E_up = ops::compute_curl_energy_matrix(mesh, 0.0f, hodge_ws); + ops::diffusion::HodgeWorkspace hodge_ws; + const auto D_weak = ops::diffusion::compute_weak_exterior_derivative(mesh, 0.0f, hodge_ws); + const auto E_up = ops::diffusion::compute_curl_energy_matrix(mesh, 0.0f, hodge_ws); - const auto laplacian = ops::compute_hodge_laplacian_matrix(D_weak, E_up); - auto [evals, evecs] = ops::compute_hodge_spectrum(laplacian, G); + const auto laplacian = ops::diffusion::compute_hodge_laplacian_matrix(D_weak, E_up); + auto [evals, evecs] = ops::diffusion::compute_hodge_spectrum(laplacian, G); if (evals.size() == 0 || evecs.cols() < 2) { std::cerr << "Hodge spectrum solve failed.\n"; return 1; @@ -445,10 +445,10 @@ int main(int argc, char **argv) { std::complex selected_eval_0(0.0f, 0.0f); std::complex selected_eval_1(0.0f, 0.0f); - const auto theta_0 = ops::compute_circular_coordinates( + const auto theta_0 = ops::diffusion::compute_circular_coordinates( mesh, evecs.col(0), 0.0f, cfg.circular_lambda, cfg.circular_mode_0, &selected_eval_0); - const auto theta_1 = ops::compute_circular_coordinates( + const auto theta_1 = ops::diffusion::compute_circular_coordinates( mesh, evecs.col(1), 0.0f, cfg.circular_lambda, cfg.circular_mode_1, &selected_eval_1); diff --git a/src/main_mesh.cpp b/src/main_mesh.cpp index 20e5652..3946505 100644 --- a/src/main_mesh.cpp +++ b/src/main_mesh.cpp @@ -1,15 +1,14 @@ #include #include -#include +#include #include #include -#include -#include +#include +#include #include using namespace igneous; -using Sig = core::Euclidean3D; -using Mesh = data::Mesh; +using Space = data::Space; int main(int argc, char **argv) { if (argc < 2) { @@ -19,24 +18,25 @@ int main(int argc, char **argv) { const bool bench_mode = std::getenv("IGNEOUS_BENCH_MODE") != nullptr; const int frame_count = bench_mode ? 10 : 1000; - Mesh mesh; - io::load_obj(mesh, argv[1]); - ops::normalize(mesh); + Space space; + io::load_obj(space, argv[1]); + space.structure.build({space.num_points(), true}); + ops::normalize(space); std::vector H; std::vector K; - ops::CurvatureWorkspace curvature_ws; - ops::FlowWorkspace flow_ws; + ops::dec::CurvatureWorkspace curvature_ws; + ops::dec::FlowWorkspace flow_ws; for (int frame = 0; frame < frame_count; ++frame) { - ops::compute_curvature_measures(mesh, H, K, curvature_ws); + ops::dec::compute_curvature_measures(space, H, K, curvature_ws); if (!bench_mode) { const std::string filename = std::format("output/frame_{:03}.obj", frame); - io::export_heatmap(mesh, H, filename, 2.0); + io::export_heatmap(space, H, filename, 2.0); } - ops::integrate_mean_curvature_flow(mesh, 0.5f, flow_ws); + ops::dec::integrate_mean_curvature_flow(space, 0.5f, flow_ws); } return 0; diff --git a/src/main_point.cpp b/src/main_point.cpp index daf09ef..3d3a8b5 100644 --- a/src/main_point.cpp +++ b/src/main_point.cpp @@ -1,5 +1,5 @@ #include -#include +#include #include #include #include @@ -8,9 +8,9 @@ using namespace igneous; -// 1. Define specific mesh types -using PointCloud = data::Mesh; -using SurfaceMesh = data::Mesh; +// 1. Define specific space types +using PointCloud = data::Space; +using SurfaceSpace = data::Space; int main(int argc, char **argv) { if (argc < 2) { @@ -34,10 +34,10 @@ int main(int argc, char **argv) { ops::normalize(pc); // C. Visualize (Create dummy height field since we can't do curvature) - size_t n_verts = pc.geometry.num_points(); + size_t n_verts = pc.num_points(); std::vector height_field(n_verts); for (size_t i = 0; i < n_verts; ++i) { - height_field[i] = pc.geometry.get_vec3(i).y; // Color by Y-height + height_field[i] = pc.get_vec3(i).y; // Color by Y-height } io::export_ply_solid(pc, height_field, "output_solid_cloud.ply", 0.01); @@ -47,13 +47,14 @@ int main(int argc, char **argv) { // ========================================================= std::cout << "\n--- Testing Surface Workflow ---\n"; - SurfaceMesh surf; + SurfaceSpace surf; io::load_obj(surf, input_path); // Should load faces + surf.structure.build({surf.num_points(), true}); ops::normalize(surf); // Dummy field - std::vector surf_field(surf.geometry.num_points(), 0.0); + std::vector surf_field(surf.num_points(), 0.0); io::export_heatmap(surf, surf_field, "output_surface.obj"); return 0; -} \ No newline at end of file +} diff --git a/src/main_spectral.cpp b/src/main_spectral.cpp index b53f3f7..4289cf3 100644 --- a/src/main_spectral.cpp +++ b/src/main_spectral.cpp @@ -7,7 +7,7 @@ #include #include -#include +#include #include #include #include @@ -15,7 +15,7 @@ #include using namespace igneous; -using DiffusionMesh = data::Mesh; +using DiffusionMesh = data::Space; int main(int argc, char **argv) { if (argc < 2) { @@ -29,14 +29,14 @@ int main(int argc, char **argv) { ops::normalize(mesh); const float bandwidth = 0.005f; - mesh.topology.build({mesh.geometry.x_span(), mesh.geometry.y_span(), - mesh.geometry.z_span(), 32}); + mesh.structure.build({mesh.x_span(), mesh.y_span(), + mesh.z_span(), 32}); const int n_basis = 16; - ops::compute_eigenbasis(mesh, n_basis); + ops::diffusion::compute_eigenbasis(mesh, n_basis); - ops::GeometryWorkspace geometry_ws; - const Eigen::MatrixXf G = ops::compute_1form_gram_matrix(mesh, bandwidth, geometry_ws); + ops::diffusion::GeometryWorkspace geometry_ws; + const Eigen::MatrixXf G = ops::diffusion::compute_1form_gram_matrix(mesh, bandwidth, geometry_ws); Eigen::SelfAdjointEigenSolver solver(G); if (solver.info() == Eigen::Success) { @@ -51,9 +51,9 @@ int main(int argc, char **argv) { std::filesystem::create_directory(out_dir); const int to_export = - std::min(4, static_cast(mesh.topology.eigen_basis.cols())); + std::min(4, static_cast(mesh.structure.eigen_basis.cols())); for (int i = 0; i < to_export; ++i) { - const Eigen::VectorXf phi = mesh.topology.eigen_basis.col(i); + const Eigen::VectorXf phi = mesh.structure.eigen_basis.col(i); std::vector field(static_cast(phi.size())); for (int j = 0; j < phi.size(); ++j) { field[static_cast(j)] = phi[j]; diff --git a/tests/test_diffgeo_cli_outputs.sh b/tests/test_diffgeo_cli_outputs.sh index 084e357..77c7349 100755 --- a/tests/test_diffgeo_cli_outputs.sh +++ b/tests/test_diffgeo_cli_outputs.sh @@ -2,7 +2,7 @@ set -euo pipefail if [[ $# -lt 1 ]]; then - echo "usage: $0 " >&2 + echo "usage: $0 " >&2 exit 2 fi diff --git a/tests/test_diffgeo_parity_optional.sh b/tests/test_diffgeo_parity_optional.sh index 4b88287..5bbc53f 100755 --- a/tests/test_diffgeo_parity_optional.sh +++ b/tests/test_diffgeo_parity_optional.sh @@ -5,7 +5,7 @@ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" REQUIRE_PARITY="${IGNEOUS_REQUIRE_PARITY:-0}" if [[ "${REQUIRE_PARITY}" != "1" ]]; then - echo "Skipping optional diffusion topology parity test: CLI now emits PLY-only outputs" + echo "Skipping optional diffusion geometry parity test: CLI now emits PLY-only outputs" exit 0 fi @@ -32,4 +32,4 @@ if not payload["gates"]["final_pass"]: raise SystemExit("final parity gate failed") PY -echo "optional diffusion topology parity test passed" +echo "optional diffusion geometry parity test passed" diff --git a/tests/test_io_meshes.cpp b/tests/test_io_meshes.cpp index 5c43c53..cefb81a 100644 --- a/tests/test_io_meshes.cpp +++ b/tests/test_io_meshes.cpp @@ -2,8 +2,7 @@ #include #include -#include -#include +#include #include static std::string bunny_path() { @@ -16,28 +15,34 @@ static std::string bunny_path() { return ""; } -TEST_CASE("OBJ import works for triangle, point, and diffusion topologies") { +TEST_CASE("OBJ import is load-only and structures build explicitly") { const std::string path = bunny_path(); REQUIRE(!path.empty()); - using Sig = igneous::core::Euclidean3D; - - igneous::data::Mesh surface_mesh; + igneous::data::Space surface_mesh; igneous::io::load_obj(surface_mesh, path); CHECK(surface_mesh.is_valid()); - CHECK(surface_mesh.geometry.num_points() > 0); - CHECK(surface_mesh.topology.num_faces() > 0); + CHECK(surface_mesh.num_points() > 0); + CHECK(surface_mesh.structure.num_faces() > 0); + CHECK(surface_mesh.structure.vertex_face_offsets.empty()); + surface_mesh.structure.build({surface_mesh.num_points(), true}); + CHECK(surface_mesh.structure.vertex_face_offsets.size() == + surface_mesh.num_points() + 1); - igneous::data::Mesh point_mesh; + igneous::data::Space point_mesh; igneous::io::load_obj(point_mesh, path); CHECK(point_mesh.is_valid()); - CHECK(point_mesh.geometry.num_points() == surface_mesh.geometry.num_points()); + CHECK(point_mesh.num_points() == surface_mesh.num_points()); + CHECK(point_mesh.structure.markov_row_offsets.empty()); - igneous::data::Mesh diffusion_mesh; + igneous::data::Space diffusion_mesh; igneous::io::load_obj(diffusion_mesh, path); CHECK(diffusion_mesh.is_valid()); - CHECK(diffusion_mesh.topology.markov_row_offsets.size() == - diffusion_mesh.geometry.num_points() + 1); - CHECK(diffusion_mesh.topology.markov_values.size() > 0); - CHECK(diffusion_mesh.topology.mu.sum() == doctest::Approx(1.0f).epsilon(1e-3f)); + CHECK(diffusion_mesh.structure.markov_row_offsets.empty()); + diffusion_mesh.structure.build( + {diffusion_mesh.x_span(), diffusion_mesh.y_span(), diffusion_mesh.z_span(), 32}); + CHECK(diffusion_mesh.structure.markov_row_offsets.size() == + diffusion_mesh.num_points() + 1); + CHECK(diffusion_mesh.structure.markov_values.size() > 0); + CHECK(diffusion_mesh.structure.mu.sum() == doctest::Approx(1.0f).epsilon(1e-3f)); } diff --git a/tests/test_ops_curvature_flow.cpp b/tests/test_ops_curvature_flow.cpp index 203e454..afc9b53 100644 --- a/tests/test_ops_curvature_flow.cpp +++ b/tests/test_ops_curvature_flow.cpp @@ -3,21 +3,21 @@ #include #include -#include -#include -#include +#include +#include +#include -static igneous::data::Mesh +static igneous::data::Space make_grid(int side) { - using Mesh = igneous::data::Mesh; + using Mesh = igneous::data::Space; Mesh mesh; - mesh.geometry.reserve(static_cast(side * side)); + mesh.reserve(static_cast(side * side)); for (int y = 0; y < side; ++y) { for (int x = 0; x < side; ++x) { const float fx = static_cast(x) / side; const float fy = static_cast(y) / side; - mesh.geometry.push_point({fx, fy, std::sin(fx) * std::cos(fy)}); + mesh.push_point({fx, fy, std::sin(fx) * std::cos(fy)}); } } @@ -28,27 +28,26 @@ make_grid(int side) { const uint32_t i2 = static_cast((y + 1) * side + x); const uint32_t i3 = static_cast((y + 1) * side + x + 1); - mesh.topology.faces_to_vertices.insert(mesh.topology.faces_to_vertices.end(), {i0, i1, i2, i1, i3, i2}); + mesh.structure.faces_to_vertices.insert(mesh.structure.faces_to_vertices.end(), {i0, i1, i2, i1, i3, i2}); } } - mesh.topology.build({mesh.geometry.num_points(), true}); + mesh.structure.build({mesh.num_points(), true}); return mesh; } TEST_CASE("Curvature and flow kernels produce finite values") { - using Sig = igneous::core::Euclidean3D; auto mesh = make_grid(20); std::vector H; std::vector K; - igneous::ops::CurvatureWorkspace curvature_ws; - igneous::ops::FlowWorkspace flow_ws; + igneous::ops::dec::CurvatureWorkspace curvature_ws; + igneous::ops::dec::FlowWorkspace flow_ws; - igneous::ops::compute_curvature_measures(mesh, H, K, curvature_ws); + igneous::ops::dec::compute_curvature_measures(mesh, H, K, curvature_ws); - CHECK(H.size() == mesh.geometry.num_points()); - CHECK(K.size() == mesh.geometry.num_points()); + CHECK(H.size() == mesh.num_points()); + CHECK(K.size() == mesh.num_points()); for (float v : H) { CHECK(std::isfinite(v)); @@ -57,9 +56,9 @@ TEST_CASE("Curvature and flow kernels produce finite values") { CHECK(std::isfinite(v)); } - const auto p_before = mesh.geometry.get_vec3(0); - igneous::ops::integrate_mean_curvature_flow(mesh, 0.01f, flow_ws); - const auto p_after = mesh.geometry.get_vec3(0); + const auto p_before = mesh.get_vec3(0); + igneous::ops::dec::integrate_mean_curvature_flow(mesh, 0.01f, flow_ws); + const auto p_after = mesh.get_vec3(0); CHECK(std::isfinite(p_after.x)); CHECK(std::isfinite(p_after.y)); diff --git a/tests/test_ops_diffusion_basis.cpp b/tests/test_ops_diffusion_basis.cpp index 4f43dbd..987baaa 100644 --- a/tests/test_ops_diffusion_basis.cpp +++ b/tests/test_ops_diffusion_basis.cpp @@ -6,7 +6,7 @@ #include TEST_CASE("Wedge basis indices match lexicographic combinations") { - const auto idx = igneous::ops::get_wedge_basis_indices(4, 2); + const auto idx = igneous::ops::diffusion::get_wedge_basis_indices(4, 2); REQUIRE(idx.size() == 6); CHECK(idx[0] == std::vector({0, 1})); @@ -18,7 +18,7 @@ TEST_CASE("Wedge basis indices match lexicographic combinations") { } TEST_CASE("kp1 children and signs follow Laplace expansion semantics") { - const auto info = igneous::ops::kp1_children_and_signs(4, 2); + const auto info = igneous::ops::diffusion::kp1_children_and_signs(4, 2); REQUIRE(info.signs.size() == 3); CHECK(info.signs[0] == 1); CHECK(info.signs[1] == -1); @@ -34,14 +34,14 @@ TEST_CASE("kp1 children and signs follow Laplace expansion semantics") { child.push_back(parent[static_cast(c)]); } } - const int expected = igneous::ops::lex_rank_combination(child, 4); + const int expected = igneous::ops::diffusion::lex_rank_combination(child, 4); CHECK(info.children[row][static_cast(r)] == expected); } } } TEST_CASE("Wedge product index mapping produces anti-symmetric split for 1-forms") { - const auto map = igneous::ops::get_wedge_product_indices(3, 1, 1); + const auto map = igneous::ops::diffusion::get_wedge_product_indices(3, 1, 1); REQUIRE(map.n_targets == 3); REQUIRE(map.n_splits == 2); REQUIRE(map.target_indices.size() == 6); diff --git a/tests/test_ops_diffusion_forms.cpp b/tests/test_ops_diffusion_forms.cpp index d73913e..4e0cc00 100644 --- a/tests/test_ops_diffusion_forms.cpp +++ b/tests/test_ops_diffusion_forms.cpp @@ -4,16 +4,16 @@ #include #include -#include +#include #include #include -static igneous::data::Mesh +static igneous::data::Space make_torus_cloud(size_t n_points) { using Mesh = - igneous::data::Mesh; + igneous::data::Space; Mesh mesh; - mesh.geometry.reserve(n_points); + mesh.reserve(n_points); for (size_t i = 0; i < n_points; ++i) { const float u = static_cast(i % 32) / 32.0f * 6.283185f; @@ -21,28 +21,28 @@ make_torus_cloud(size_t n_points) { static_cast(i / 32) / std::max(1, n_points / 32) * 6.283185f; const float R = 2.0f; const float r = 0.8f; - mesh.geometry.push_point( + mesh.push_point( {(R + r * std::cos(v)) * std::cos(u), (R + r * std::cos(v)) * std::sin(u), r * std::sin(v)}); } - mesh.topology.build({mesh.geometry.x_span(), mesh.geometry.y_span(), - mesh.geometry.z_span(), 24}); + mesh.structure.build({mesh.x_span(), mesh.y_span(), + mesh.z_span(), 24}); return mesh; } TEST_CASE("Generic diffusion form operators are finite and shape-consistent") { auto mesh = make_torus_cloud(960); - igneous::ops::compute_eigenbasis(mesh, 48); + igneous::ops::diffusion::compute_eigenbasis(mesh, 48); const int n_coeff = 24; - igneous::ops::DiffusionFormWorkspace ws; + igneous::ops::diffusion::DiffusionFormWorkspace ws; - const auto G1 = igneous::ops::compute_kform_gram_matrix(mesh, 1, n_coeff, ws); - const auto G2 = igneous::ops::compute_kform_gram_matrix(mesh, 2, n_coeff, ws); - const auto D1 = igneous::ops::compute_weak_exterior_derivative(mesh, 1, n_coeff, ws); - const auto up2 = igneous::ops::compute_up_laplacian_matrix(mesh, 2, n_coeff, ws); - const auto down2 = igneous::ops::compute_down_laplacian_matrix(mesh, 2, n_coeff, ws); + const auto G1 = igneous::ops::diffusion::compute_kform_gram_matrix(mesh, 1, n_coeff, ws); + const auto G2 = igneous::ops::diffusion::compute_kform_gram_matrix(mesh, 2, n_coeff, ws); + const auto D1 = igneous::ops::diffusion::compute_weak_exterior_derivative(mesh, 1, n_coeff, ws); + const auto up2 = igneous::ops::diffusion::compute_up_laplacian_matrix(mesh, 2, n_coeff, ws); + const auto down2 = igneous::ops::diffusion::compute_down_laplacian_matrix(mesh, 2, n_coeff, ws); CHECK(G1.rows() == n_coeff * 3); CHECK(G1.cols() == n_coeff * 3); @@ -64,37 +64,37 @@ TEST_CASE("Generic diffusion form operators are finite and shape-consistent") { } } - const auto L2 = igneous::ops::assemble_hodge_laplacian_matrix(up2, down2); - auto [evals2, evecs2] = igneous::ops::compute_form_spectrum(L2, G2); + const auto L2 = igneous::ops::diffusion::assemble_hodge_laplacian_matrix(up2, down2); + auto [evals2, evecs2] = igneous::ops::diffusion::compute_form_spectrum(L2, G2); CHECK(evals2.size() > 0); CHECK(evecs2.cols() > 0); for (int i = 0; i < evals2.size(); ++i) { CHECK(std::isfinite(evals2[i])); } - const auto harmonic_idx = igneous::ops::extract_harmonic_mode_indices(evals2, 1e-2f, 3); + const auto harmonic_idx = igneous::ops::diffusion::extract_harmonic_mode_indices(evals2, 1e-2f, 3); CHECK(!harmonic_idx.empty()); } TEST_CASE("Up-Laplacian(2) entry matches manual Schur-determinant assembly") { auto mesh = make_torus_cloud(640); - igneous::ops::compute_eigenbasis(mesh, 36); + igneous::ops::diffusion::compute_eigenbasis(mesh, 36); const int n_coeff = 16; - igneous::ops::DiffusionFormWorkspace ws; - const auto up2 = igneous::ops::compute_up_laplacian_matrix(mesh, 2, n_coeff, ws); + igneous::ops::diffusion::DiffusionFormWorkspace ws; + const auto up2 = igneous::ops::diffusion::compute_up_laplacian_matrix(mesh, 2, n_coeff, ws); - const auto &U = mesh.topology.eigen_basis; - const auto &mu = mesh.topology.mu; + const auto &U = mesh.structure.eigen_basis; + const auto &mu = mesh.structure.mu; - igneous::ops::ensure_gamma_coords(mesh, ws); - igneous::ops::ensure_gamma_mixed(mesh, n_coeff, ws); - const auto idx2 = igneous::ops::get_wedge_basis_indices(3, 2); + igneous::ops::diffusion::ensure_gamma_coords(mesh, ws); + igneous::ops::diffusion::ensure_gamma_mixed(mesh, n_coeff, ws); + const auto idx2 = igneous::ops::diffusion::get_wedge_basis_indices(3, 2); // Compare entry (i=0,J=0 ; l=0,K=0). float manual = 0.0f; - Eigen::VectorXf gamma_phi_phi(mesh.geometry.num_points()); - igneous::ops::carre_du_champ(mesh, U.col(0), U.col(0), 0.0f, gamma_phi_phi); + Eigen::VectorXf gamma_phi_phi(mesh.num_points()); + igneous::ops::diffusion::carre_du_champ(mesh, U.col(0), U.col(0), 0.0f, gamma_phi_phi); const auto &row_combo = idx2[0]; const auto &col_combo = idx2[0]; @@ -114,7 +114,7 @@ TEST_CASE("Up-Laplacian(2) entry matches manual Schur-determinant assembly") { b_vec[0] = ws.gamma_mixed[col_combo[0]](p, 0); b_vec[1] = ws.gamma_mixed[col_combo[1]](p, 0); - const Eigen::Vector2f solved = igneous::ops::solve_stable_2x2(Dm, c_vec); + const Eigen::Vector2f solved = igneous::ops::diffusion::solve_stable_2x2(Dm, c_vec); const float bDv = b_vec.dot(solved); manual += mu[p] * detD * (gamma_phi_phi[p] - bDv); } diff --git a/tests/test_ops_diffusion_wedge.cpp b/tests/test_ops_diffusion_wedge.cpp index 4680da8..aa542fa 100644 --- a/tests/test_ops_diffusion_wedge.cpp +++ b/tests/test_ops_diffusion_wedge.cpp @@ -5,34 +5,34 @@ #include #include -#include +#include #include #include -static igneous::data::Mesh +static igneous::data::Space make_cloud(size_t n_points) { using Mesh = - igneous::data::Mesh; + igneous::data::Space; Mesh mesh; - mesh.geometry.reserve(n_points); + mesh.reserve(n_points); for (size_t i = 0; i < n_points; ++i) { const float t = static_cast(i) / static_cast(n_points); const float a = t * 6.283185f; const float b = (t * 7.0f) * 6.283185f; - mesh.geometry.push_point({(2.0f + 0.7f * std::cos(b)) * std::cos(a), + mesh.push_point({(2.0f + 0.7f * std::cos(b)) * std::cos(a), (2.0f + 0.7f * std::cos(b)) * std::sin(a), 0.7f * std::sin(b)}); } - mesh.topology.build({mesh.geometry.x_span(), mesh.geometry.y_span(), - mesh.geometry.z_span(), 24}); + mesh.structure.build({mesh.x_span(), mesh.y_span(), + mesh.z_span(), 24}); return mesh; } TEST_CASE("Wedge product for 1-forms is anti-commutative") { auto mesh = make_cloud(720); - igneous::ops::compute_eigenbasis(mesh, 32); + igneous::ops::diffusion::compute_eigenbasis(mesh, 32); const int n_coeff = 16; std::mt19937 rng(123); @@ -45,12 +45,12 @@ TEST_CASE("Wedge product for 1-forms is anti-commutative") { beta[i] = normal(rng); } - igneous::ops::DiffusionFormWorkspace ws; + igneous::ops::diffusion::DiffusionFormWorkspace ws; const Eigen::VectorXf ab = - igneous::ops::compute_wedge_product_coeffs(mesh, alpha, 1, beta, 1, + igneous::ops::diffusion::compute_wedge_product_coeffs(mesh, alpha, 1, beta, 1, n_coeff, ws); const Eigen::VectorXf ba = - igneous::ops::compute_wedge_product_coeffs(mesh, beta, 1, alpha, 1, + igneous::ops::diffusion::compute_wedge_product_coeffs(mesh, beta, 1, alpha, 1, n_coeff, ws); REQUIRE(ab.size() == n_coeff * 3); @@ -63,7 +63,7 @@ TEST_CASE("Wedge product for 1-forms is anti-commutative") { TEST_CASE("Linearized wedge operator matches direct wedge product") { auto mesh = make_cloud(840); - igneous::ops::compute_eigenbasis(mesh, 36); + igneous::ops::diffusion::compute_eigenbasis(mesh, 36); const int n_coeff = 18; std::mt19937 rng(7); @@ -76,11 +76,11 @@ TEST_CASE("Linearized wedge operator matches direct wedge product") { beta[i] = normal(rng); } - igneous::ops::DiffusionFormWorkspace ws; + igneous::ops::diffusion::DiffusionFormWorkspace ws; const Eigen::MatrixXf op = - igneous::ops::compute_wedge_operator_matrix(mesh, alpha, 1, 1, n_coeff, ws); + igneous::ops::diffusion::compute_wedge_operator_matrix(mesh, alpha, 1, 1, n_coeff, ws); const Eigen::VectorXf direct = - igneous::ops::compute_wedge_product_coeffs(mesh, alpha, 1, beta, 1, + igneous::ops::diffusion::compute_wedge_product_coeffs(mesh, alpha, 1, beta, 1, n_coeff, ws); REQUIRE(op.cols() == beta.size()); diff --git a/tests/test_ops_hodge.cpp b/tests/test_ops_hodge.cpp index 8c280f8..cc23134 100644 --- a/tests/test_ops_hodge.cpp +++ b/tests/test_ops_hodge.cpp @@ -3,16 +3,16 @@ #include #include -#include +#include #include #include #include -static igneous::data::Mesh +static igneous::data::Space make_torus_cloud(size_t n_points) { - using Mesh = igneous::data::Mesh; + using Mesh = igneous::data::Space; Mesh mesh; - mesh.geometry.reserve(n_points); + mesh.reserve(n_points); for (size_t i = 0; i < n_points; ++i) { const float u = static_cast(i % 40) / 40.0f * 6.283185f; @@ -22,25 +22,25 @@ make_torus_cloud(size_t n_points) { const float x = (R + r * std::cos(v)) * std::cos(u); const float y = (R + r * std::cos(v)) * std::sin(u); const float z = r * std::sin(v); - mesh.geometry.push_point({x, y, z}); + mesh.push_point({x, y, z}); } - mesh.topology.build({mesh.geometry.x_span(), mesh.geometry.y_span(), - mesh.geometry.z_span(), 24}); + mesh.structure.build({mesh.x_span(), mesh.y_span(), + mesh.z_span(), 24}); return mesh; } TEST_CASE("Hodge operators produce finite spectrum and circular coordinates") { auto mesh = make_torus_cloud(1000); - igneous::ops::compute_eigenbasis(mesh, 50); + igneous::ops::diffusion::compute_eigenbasis(mesh, 50); - igneous::ops::GeometryWorkspace geom_ws; - const auto G = igneous::ops::compute_1form_gram_matrix(mesh, 0.05f, geom_ws); + igneous::ops::diffusion::GeometryWorkspace geom_ws; + const auto G = igneous::ops::diffusion::compute_1form_gram_matrix(mesh, 0.05f, geom_ws); - igneous::ops::HodgeWorkspace hodge_ws; - const auto D = igneous::ops::compute_weak_exterior_derivative(mesh, 0.05f, hodge_ws); - const auto E = igneous::ops::compute_curl_energy_matrix(mesh, 0.05f, hodge_ws); - const auto L = igneous::ops::compute_hodge_laplacian_matrix(D, E); + igneous::ops::diffusion::HodgeWorkspace hodge_ws; + const auto D = igneous::ops::diffusion::compute_weak_exterior_derivative(mesh, 0.05f, hodge_ws); + const auto E = igneous::ops::diffusion::compute_curl_energy_matrix(mesh, 0.05f, hodge_ws); + const auto L = igneous::ops::diffusion::compute_hodge_laplacian_matrix(D, E); CHECK(L.rows() == D.rows()); CHECK(L.cols() == D.rows()); @@ -52,20 +52,20 @@ TEST_CASE("Hodge operators produce finite spectrum and circular coordinates") { } } - auto [evals, evecs] = igneous::ops::compute_hodge_spectrum(L, G); + auto [evals, evecs] = igneous::ops::diffusion::compute_hodge_spectrum(L, G); CHECK(evals.size() > 0); CHECK(evecs.cols() >= 2); for (int i = 0; i < evals.size(); ++i) { CHECK(std::isfinite(evals[i])); } - const auto theta_0 = igneous::ops::compute_circular_coordinates( + const auto theta_0 = igneous::ops::diffusion::compute_circular_coordinates( mesh, evecs.col(0), 0.0f, 1.0f, 0); - const auto theta_1 = igneous::ops::compute_circular_coordinates( + const auto theta_1 = igneous::ops::diffusion::compute_circular_coordinates( mesh, evecs.col(1), 0.0f, 1.0f, 1); const auto check_theta = [&](const Eigen::VectorXf &theta) { - CHECK(theta.size() == static_cast(mesh.geometry.num_points())); + CHECK(theta.size() == static_cast(mesh.num_points())); float mean = 0.0f; for (int i = 0; i < theta.size(); ++i) { diff --git a/tests/test_ops_spectral_geometry.cpp b/tests/test_ops_spectral_geometry.cpp index 6d7d569..6fcd0d3 100644 --- a/tests/test_ops_spectral_geometry.cpp +++ b/tests/test_ops_spectral_geometry.cpp @@ -3,39 +3,39 @@ #include #include -#include +#include #include #include -static igneous::data::Mesh +static igneous::data::Space make_diffusion_cloud(size_t n_points) { - using Mesh = igneous::data::Mesh; + using Mesh = igneous::data::Space; Mesh mesh; - mesh.geometry.reserve(n_points); + mesh.reserve(n_points); for (size_t i = 0; i < n_points; ++i) { const float t = static_cast(i) / static_cast(n_points); - mesh.geometry.push_point({std::cos(t * 6.283185f), std::sin(t * 6.283185f), t}); + mesh.push_point({std::cos(t * 6.283185f), std::sin(t * 6.283185f), t}); } - mesh.topology.build( - {mesh.geometry.x_span(), mesh.geometry.y_span(), mesh.geometry.z_span(), 24}); + mesh.structure.build( + {mesh.x_span(), mesh.y_span(), mesh.z_span(), 24}); return mesh; } TEST_CASE("Spectral basis and Gram matrix are finite and shaped correctly") { auto mesh = make_diffusion_cloud(400); - igneous::ops::compute_eigenbasis(mesh, 8); + igneous::ops::diffusion::compute_eigenbasis(mesh, 8); - CHECK(mesh.topology.eigen_basis.rows() == static_cast(mesh.geometry.num_points())); - CHECK(mesh.topology.eigen_basis.cols() >= 1); + CHECK(mesh.structure.eigen_basis.rows() == static_cast(mesh.num_points())); + CHECK(mesh.structure.eigen_basis.cols() >= 1); - igneous::ops::GeometryWorkspace ws; - const Eigen::MatrixXf G = igneous::ops::compute_1form_gram_matrix(mesh, 0.05f, ws); + igneous::ops::diffusion::GeometryWorkspace ws; + const Eigen::MatrixXf G = igneous::ops::diffusion::compute_1form_gram_matrix(mesh, 0.05f, ws); - CHECK(G.rows() == mesh.topology.eigen_basis.cols() * 3); - CHECK(G.cols() == mesh.topology.eigen_basis.cols() * 3); + CHECK(G.rows() == mesh.structure.eigen_basis.cols() * 3); + CHECK(G.cols() == mesh.structure.eigen_basis.cols() * 3); for (int r = 0; r < G.rows(); ++r) { for (int c = 0; c < G.cols(); ++c) { diff --git a/tests/test_topology_triangle.cpp b/tests/test_structure_dec.cpp similarity index 83% rename from tests/test_topology_triangle.cpp rename to tests/test_structure_dec.cpp index 47d53fe..3bc2d26 100644 --- a/tests/test_topology_triangle.cpp +++ b/tests/test_structure_dec.cpp @@ -2,10 +2,10 @@ #include #include -#include +#include -TEST_CASE("TriangleTopology builds face and neighbor CSR") { - igneous::data::TriangleTopology topo; +TEST_CASE("DiscreteExteriorCalculus builds face and neighbor CSR") { + igneous::data::DiscreteExteriorCalculus topo; topo.faces_to_vertices = { 0, 1, 2, 0, 2, 3, diff --git a/tests/test_topology_diffusion.cpp b/tests/test_structure_diffusion_geometry.cpp similarity index 67% rename from tests/test_topology_diffusion.cpp rename to tests/test_structure_diffusion_geometry.cpp index 7c64fc5..8bb4090 100644 --- a/tests/test_topology_diffusion.cpp +++ b/tests/test_structure_diffusion_geometry.cpp @@ -3,13 +3,12 @@ #include #include -#include -#include -#include +#include +#include #include static Eigen::VectorXf csr_markov_reference( - const igneous::data::DiffusionTopology &topo, + const igneous::data::DiffusionGeometry &topo, Eigen::Ref input) { const int n = static_cast(topo.markov_row_offsets.size()) - 1; Eigen::VectorXf output = Eigen::VectorXf::Zero(n); @@ -29,19 +28,17 @@ static Eigen::VectorXf csr_markov_reference( return output; } -TEST_CASE("DiffusionTopology produces stochastic Markov matrix and valid measure") { - using Sig = igneous::core::Euclidean3D; - - igneous::data::GeometryBuffer geometry; - geometry.reserve(16); +TEST_CASE("DiffusionGeometry produces stochastic Markov matrix and valid measure") { + igneous::data::Space cloud; + cloud.reserve(16); for (int i = 0; i < 16; ++i) { const float t = static_cast(i) / 16.0f; - geometry.push_point({std::cos(t * 6.283185f), std::sin(t * 6.283185f), t}); + cloud.push_point({std::cos(t * 6.283185f), std::sin(t * 6.283185f), t}); } - igneous::data::DiffusionTopology topo; - topo.build({geometry.x_span(), geometry.y_span(), geometry.z_span(), 8}); + igneous::data::DiffusionGeometry topo; + topo.build({cloud.x_span(), cloud.y_span(), cloud.z_span(), 8}); CHECK(topo.markov_row_offsets.size() == 17); CHECK(topo.markov_col_indices.size() == topo.markov_values.size()); @@ -72,27 +69,26 @@ TEST_CASE("DiffusionTopology produces stochastic Markov matrix and valid measure } TEST_CASE("Diffusion CSR markov step matches sparse matrix product") { - using Sig = igneous::core::Euclidean3D; using DiffusionMesh = - igneous::data::Mesh; + igneous::data::Space; DiffusionMesh mesh; - mesh.geometry.reserve(24); + mesh.reserve(24); for (int i = 0; i < 24; ++i) { const float t = static_cast(i) / 24.0f; - mesh.geometry.push_point( + mesh.push_point( {std::cos(t * 6.283185f), std::sin(t * 6.283185f), 0.5f * t}); } - mesh.topology.build({mesh.geometry.x_span(), mesh.geometry.y_span(), - mesh.geometry.z_span(), 8}); + mesh.structure.build({mesh.x_span(), mesh.y_span(), + mesh.z_span(), 8}); - const int n = static_cast(mesh.geometry.num_points()); + const int n = static_cast(mesh.num_points()); Eigen::VectorXf u = Eigen::VectorXf::LinSpaced(n, -1.0f, 1.0f); - Eigen::VectorXf expected = csr_markov_reference(mesh.topology, u); + Eigen::VectorXf expected = csr_markov_reference(mesh.structure, u); Eigen::VectorXf actual = Eigen::VectorXf::Zero(n); - igneous::ops::apply_markov_transition(mesh, u, actual); + igneous::ops::diffusion::apply_markov_transition(mesh, u, actual); for (int i = 0; i < n; ++i) { CHECK(actual[i] == doctest::Approx(expected[i]).epsilon(1e-5f)); @@ -100,35 +96,34 @@ TEST_CASE("Diffusion CSR markov step matches sparse matrix product") { } TEST_CASE("Diffusion multi-step markov matches repeated single steps") { - using Sig = igneous::core::Euclidean3D; using DiffusionMesh = - igneous::data::Mesh; + igneous::data::Space; DiffusionMesh mesh; - mesh.geometry.reserve(24); + mesh.reserve(24); for (int i = 0; i < 24; ++i) { const float t = static_cast(i) / 24.0f; - mesh.geometry.push_point( + mesh.push_point( {std::cos(t * 6.283185f), std::sin(t * 6.283185f), 0.5f * t}); } - mesh.topology.build({mesh.geometry.x_span(), mesh.geometry.y_span(), - mesh.geometry.z_span(), 8}); + mesh.structure.build({mesh.x_span(), mesh.y_span(), + mesh.z_span(), 8}); - const int n = static_cast(mesh.geometry.num_points()); + const int n = static_cast(mesh.num_points()); Eigen::VectorXf u0 = Eigen::VectorXf::LinSpaced(n, -1.0f, 1.0f); constexpr int kSteps = 7; Eigen::VectorXf expected = u0; Eigen::VectorXf tmp = Eigen::VectorXf::Zero(n); for (int step = 0; step < kSteps; ++step) { - igneous::ops::apply_markov_transition(mesh, expected, tmp); + igneous::ops::diffusion::apply_markov_transition(mesh, expected, tmp); expected.swap(tmp); } Eigen::VectorXf actual = Eigen::VectorXf::Zero(n); - igneous::ops::DiffusionWorkspace ws; - igneous::ops::apply_markov_transition_steps(mesh, u0, kSteps, actual, ws); + igneous::ops::diffusion::DiffusionWorkspace ws; + igneous::ops::diffusion::apply_markov_transition_steps(mesh, u0, kSteps, actual, ws); for (int i = 0; i < n; ++i) { CHECK(actual[i] == doctest::Approx(expected[i]).epsilon(1e-5f)); diff --git a/visualizations/README.md b/visualizations/README.md index 8c3b77f..e54b46e 100644 --- a/visualizations/README.md +++ b/visualizations/README.md @@ -44,10 +44,10 @@ python3 visualizations/view_main_spectral.py --run --open python3 visualizations/view_main_hodge.py --run --open ``` -`src/main_diffusion_topology.cpp`: +`src/main_diffusion_geometry.cpp`: ```bash -python3 visualizations/view_main_diffusion_topology.py --run --open +python3 visualizations/view_main_diffusion_geometry.py --run --open ``` Each script writes previews and an `index.md` under `visualizations/results//`. diff --git a/visualizations/view_main_diffusion_topology.py b/visualizations/view_main_diffusion_geometry.py similarity index 88% rename from visualizations/view_main_diffusion_topology.py rename to visualizations/view_main_diffusion_geometry.py index cbab7c0..d4f7005 100755 --- a/visualizations/view_main_diffusion_topology.py +++ b/visualizations/view_main_diffusion_geometry.py @@ -19,16 +19,16 @@ def main() -> int: parser = argparse.ArgumentParser( - description="View outputs from src/main_diffusion_topology.cpp" + description="View outputs from src/main_diffusion_geometry.cpp" ) - parser.add_argument("--run", action="store_true", help="Run igneous-diffusion-topology before rendering") - parser.add_argument("--executable", default="build/igneous-diffusion-topology") - parser.add_argument("--output-dir", default="output_diffusion_topology") + parser.add_argument("--run", action="store_true", help="Run igneous-diffusion-geometry before rendering") + parser.add_argument("--executable", default="build/igneous-diffusion-geometry") + parser.add_argument("--output-dir", default="output_diffusion_geometry") parser.add_argument("--input-csv", default=None) parser.add_argument("--generate-sphere", action="store_true") parser.add_argument("--n-points", type=int, default=None) parser.add_argument("--seed", type=int, default=None) - parser.add_argument("--plots-dir", default="visualizations/results/main_diffusion_topology") + parser.add_argument("--plots-dir", default="visualizations/results/main_diffusion_geometry") parser.add_argument("--open", action="store_true", help="Open generated previews (or raw outputs)") args = parser.parse_args() @@ -52,7 +52,7 @@ def main() -> int: files = sorted(output_dir.glob("*.ply")) if not files: raise SystemExit( - "No diffusion-topology PLY outputs found. Run with --run or verify --output-dir." + "No diffusion-geometry PLY outputs found. Run with --run or verify --output-dir." ) if not has_matplotlib(): @@ -69,7 +69,7 @@ def main() -> int: rows.append((src.name, src, out_png)) index_path = plots_dir / "index.md" - write_index(index_path, "main_diffusion_topology outputs", rows) + write_index(index_path, "main_diffusion_geometry outputs", rows) print(f"[viz] Wrote previews to {plots_dir}") print(f"[viz] Index: {index_path}") From 83d271a869c6f724243ebe694257b36e7f02925e Mon Sep 17 00:00:00 2001 From: Colin Roberts Date: Sun, 15 Feb 2026 07:56:01 -0700 Subject: [PATCH 2/9] docs --- .github/workflows/ci.yml | 49 +++++ .github/workflows/docs.yml | 80 ++++++++ CMakeLists.txt | 23 +++ Makefile | 5 +- README.md | 23 +++ docs/Doxyfile.in | 57 ++++++ docs/mainpage.md | 28 +++ include/igneous/core/algebra.hpp | 101 ++++++---- include/igneous/core/blades.hpp | 71 +++++-- include/igneous/core/gpu.hpp | 50 +++++ include/igneous/core/memory.hpp | 60 ++++-- include/igneous/core/parallel.hpp | 61 ++++++ include/igneous/data/space.hpp | 78 ++++++++ include/igneous/data/structure.hpp | 14 ++ .../data/structures/diffusion_geometry.hpp | 88 ++++++++- .../structures/discrete_exterior_calculus.hpp | 50 +++++ include/igneous/igneous.hpp | 3 + include/igneous/io/exporter.hpp | 65 +++++++ include/igneous/io/importer.hpp | 11 ++ include/igneous/ops/dec/curvature.hpp | 11 ++ include/igneous/ops/dec/flow.hpp | 10 + include/igneous/ops/diffusion/basis.hpp | 59 ++++++ include/igneous/ops/diffusion/forms.hpp | 180 ++++++++++++++++++ include/igneous/ops/diffusion/geometry.hpp | 62 ++++++ include/igneous/ops/diffusion/hodge.hpp | 69 ++++++- include/igneous/ops/diffusion/products.hpp | 42 ++++ include/igneous/ops/diffusion/spectral.hpp | 65 ++++++- include/igneous/ops/transform.hpp | 6 + notes/structure_refactor/journal.md | 32 ++++ 29 files changed, 1375 insertions(+), 78 deletions(-) create mode 100644 .github/workflows/docs.yml create mode 100644 docs/Doxyfile.in create mode 100644 docs/mainpage.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3904f47..9a2039c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -171,3 +171,52 @@ jobs: with: name: compile-commands path: build-debug/compile_commands.json + + docs: + name: API Docs (doxygen) + runs-on: ubuntu-latest + + steps: + - name: Checkout source + uses: actions/checkout@v4 + + - name: Install docs tools + shell: bash + run: | + sudo apt-get update + sudo apt-get install -y doxygen graphviz + + - name: Checkout vcpkg + uses: actions/checkout@v4 + with: + repository: microsoft/vcpkg + path: vcpkg + + - name: Restore vcpkg cache + uses: actions/cache@v4 + with: + path: | + ~/.cache/vcpkg/archives + ${{ github.workspace }}/vcpkg/downloads + key: vcpkg-${{ runner.os }}-x64-linux-${{ hashFiles('vcpkg.json', 'CMakeLists.txt') }} + + - name: Bootstrap vcpkg + shell: bash + run: ./vcpkg/bootstrap-vcpkg.sh + + - name: Configure (Docs) + shell: bash + run: >- + cmake -S . -B build-docs -G Ninja + -DCMAKE_BUILD_TYPE=Release + -DCMAKE_TOOLCHAIN_FILE=${{ github.workspace }}/vcpkg/scripts/buildsystems/vcpkg.cmake + -DVCPKG_TARGET_TRIPLET=x64-linux + -DIGNEOUS_BUILD_DOCS=ON + + - name: Build docs + shell: bash + run: cmake --build build-docs --target docs + + - name: Verify docs output + shell: bash + run: test -f build-docs/docs/html/index.html diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..84cabbc --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,80 @@ +name: Docs + +on: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: docs-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-docs: + name: Build API Docs + runs-on: ubuntu-latest + + steps: + - name: Checkout source + uses: actions/checkout@v4 + + - name: Install docs tools + shell: bash + run: | + sudo apt-get update + sudo apt-get install -y doxygen graphviz + + - name: Checkout vcpkg + uses: actions/checkout@v4 + with: + repository: microsoft/vcpkg + path: vcpkg + + - name: Restore vcpkg cache + uses: actions/cache@v4 + with: + path: | + ~/.cache/vcpkg/archives + ${{ github.workspace }}/vcpkg/downloads + key: vcpkg-${{ runner.os }}-x64-linux-${{ hashFiles('vcpkg.json', 'CMakeLists.txt') }} + + - name: Bootstrap vcpkg + shell: bash + run: ./vcpkg/bootstrap-vcpkg.sh + + - name: Configure (Docs) + shell: bash + run: >- + cmake -S . -B build-docs -G Ninja + -DCMAKE_BUILD_TYPE=Release + -DCMAKE_TOOLCHAIN_FILE=${{ github.workspace }}/vcpkg/scripts/buildsystems/vcpkg.cmake + -DVCPKG_TARGET_TRIPLET=x64-linux + -DIGNEOUS_BUILD_DOCS=ON + + - name: Build docs + shell: bash + run: cmake --build build-docs --target docs + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: build-docs/docs/html + + deploy: + name: Deploy to GitHub Pages + needs: build-docs + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - name: Deploy + id: deployment + uses: actions/deploy-pages@v4 diff --git a/CMakeLists.txt b/CMakeLists.txt index 0278841..de468bd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,6 +5,7 @@ set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) add_compile_options(-Wall -Wextra -Wpedantic) +option(IGNEOUS_BUILD_DOCS "Build Doxygen API documentation target" ON) if(APPLE) enable_language(OBJCXX) @@ -43,6 +44,28 @@ target_link_libraries( Spectra::Spectra igneous_runtime) +if(IGNEOUS_BUILD_DOCS) + find_package(Doxygen QUIET) + if(DOXYGEN_FOUND) + set(IGNEOUS_DOXYFILE_IN ${CMAKE_SOURCE_DIR}/docs/Doxyfile.in) + set(IGNEOUS_DOXYFILE_OUT ${CMAKE_BINARY_DIR}/Doxyfile) + configure_file(${IGNEOUS_DOXYFILE_IN} ${IGNEOUS_DOXYFILE_OUT} @ONLY) + add_custom_target( + docs + COMMAND ${DOXYGEN_EXECUTABLE} ${IGNEOUS_DOXYFILE_OUT} + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + COMMENT "Generating API documentation with Doxygen" + VERBATIM) + else() + message(STATUS "Doxygen not found; 'docs' target will print install guidance") + add_custom_target( + docs + COMMAND ${CMAKE_COMMAND} -E echo "Doxygen not found. Install doxygen (and graphviz optionally), then re-run CMake configure." + COMMENT "API docs unavailable: Doxygen not installed" + VERBATIM) + endif() +endif() + enable_testing() function(add_igneous_test name source) diff --git a/Makefile b/Makefile index 394039c..7ecde34 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all debug release build clean test test-all test-algebra test-structure test-ops bench bench-memory bench-geometry bench-dod bench-deep run-mesh run-diffusion run-spectral run-hodge +.PHONY: all debug release build docs clean test test-all test-algebra test-structure test-ops bench bench-memory bench-geometry bench-dod bench-deep run-mesh run-diffusion run-spectral run-hodge all: build @@ -11,6 +11,9 @@ release: build: cmake --build build +docs: debug + cmake --build build --target docs + clean: rm -rf build diff --git a/README.md b/README.md index 884af75..0e526ad 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,28 @@ cmake --preset default-local -DCMAKE_BUILD_TYPE=Release cmake --build build -j8 ``` +## API Docs + +The repo uses Doxygen-style comments in headers under `include/igneous/`. + +Generate docs: + +```bash +cmake -S . -B build -G Ninja +cmake --build build --target docs +``` + +Output: + +- `build/docs/html/index.html` + +If Doxygen is missing, install it first (for example via Homebrew: `brew install doxygen graphviz`). + +Deployment: + +- GitHub Actions workflow `.github/workflows/docs.yml` builds and deploys docs to GitHub Pages on `main`. +- Published URL pattern: `https://.github.io/igneous/` + ## Run Examples ```bash @@ -190,6 +212,7 @@ Pipeline benchmark groups: ## CI/CD Workflows - `.github/workflows/ci.yml`: Linux/macOS build + tests, sanitizer pass, compile commands artifact. +- `.github/workflows/docs.yml`: Doxygen build and GitHub Pages deployment for API docs. - `.github/workflows/perf-smoke.yml`: PR smoke benchmark report against baseline (report-only). - `.github/workflows/perf-deep.yml`: nightly/manual deep benchmark capture and summary (report-only). - `.github/workflows/release.yml`: tag-triggered `v*` release packaging and GitHub Release asset publish. diff --git a/docs/Doxyfile.in b/docs/Doxyfile.in new file mode 100644 index 0000000..55fab9b --- /dev/null +++ b/docs/Doxyfile.in @@ -0,0 +1,57 @@ +PROJECT_NAME = "igneous" +PROJECT_BRIEF = "Structure-first geometry and diffusion operators" +OUTPUT_DIRECTORY = @CMAKE_BINARY_DIR@/docs +CREATE_SUBDIRS = NO +ALLOW_UNICODE_NAMES = NO +OUTPUT_LANGUAGE = English + +INPUT = @CMAKE_SOURCE_DIR@/include @CMAKE_SOURCE_DIR@/README.md @CMAKE_SOURCE_DIR@/docs/mainpage.md +FILE_PATTERNS = *.hpp *.md +RECURSIVE = YES +USE_MDFILE_AS_MAINPAGE = @CMAKE_SOURCE_DIR@/docs/mainpage.md + +EXTRACT_ALL = NO +EXTRACT_PRIVATE = YES +EXTRACT_STATIC = YES +EXTRACT_LOCAL_CLASSES = YES +EXTRACT_LOCAL_METHODS = YES +HIDE_UNDOC_MEMBERS = NO +HIDE_UNDOC_CLASSES = NO + +JAVADOC_AUTOBRIEF = YES +QT_AUTOBRIEF = NO +MULTILINE_CPP_IS_BRIEF = YES +INHERIT_DOCS = YES +INLINE_INFO = YES +SORT_MEMBER_DOCS = YES + +WARNINGS = YES +WARN_IF_UNDOCUMENTED = YES +WARN_IF_DOC_ERROR = YES +WARN_NO_PARAMDOC = NO + +GENERATE_HTML = YES +HTML_OUTPUT = html +GENERATE_TREEVIEW = YES +FULL_SIDEBAR = YES + +GENERATE_LATEX = NO +GENERATE_XML = NO +GENERATE_MAN = NO + +SOURCE_BROWSER = YES +INLINE_SOURCES = NO +REFERENCED_BY_RELATION = YES +REFERENCES_RELATION = YES + +HAVE_DOT = YES +DOT_IMAGE_FORMAT = svg +DOT_GRAPH_MAX_NODES = 80 +CALL_GRAPH = NO +CALLER_GRAPH = NO +CLASS_DIAGRAMS = YES + +FULL_PATH_NAMES = NO +STRIP_FROM_PATH = @CMAKE_SOURCE_DIR@ + +QUIET = NO diff --git a/docs/mainpage.md b/docs/mainpage.md new file mode 100644 index 0000000..9f8b8fa --- /dev/null +++ b/docs/mainpage.md @@ -0,0 +1,28 @@ +# igneous C++ API Docs + +This site is generated from Doxygen comments in the C++ headers. + +## Scope + +- Public API in `include/igneous/` +- Internal/private implementation notes where available +- Concepts, structures, spaces, and operators + +## Build + +From the repo root: + +```bash +cmake -S . -B build -G Ninja +cmake --build build --target docs +``` + +The generated HTML docs are written to: + +- `build/docs/html/index.html` + +## Style in This Repo + +- Use `///` or `/** ... */` comments for declarations. +- Document templates/concepts, workspaces, and helper functions. +- Include private/internal field docs for complex stateful types. diff --git a/include/igneous/core/algebra.hpp b/include/igneous/core/algebra.hpp index cf3ec79..d09052c 100644 --- a/include/igneous/core/algebra.hpp +++ b/include/igneous/core/algebra.hpp @@ -7,9 +7,7 @@ namespace igneous::core { -// ======================================================================== -// 1. SIGNATURE & METRIC -// ======================================================================== +/// \brief Clifford signature descriptor (`p`,`q`,`r`). template requires(P >= 0) && (Q >= 0) && (R >= 0) struct Signature { @@ -24,7 +22,11 @@ using Euclidean3D = Signature<3, 0>; using PGA = Signature<3, 0, 1>; using CGA = Signature<4, 1>; -// Metric Helper +/** + * \brief Metric coefficient for basis direction `index`. + * \param index Basis vector index. + * \return `+1`, `-1`, or `0` depending on signature axis type. + */ template constexpr int get_basis_metric(int index) { if (index < Sig::p) return 1; @@ -33,7 +35,12 @@ template constexpr int get_basis_metric(int index) { return 0; } -// Computes the sign/metric for basis blade multiplication a * b +/** + * \brief Sign and metric factor for geometric product of basis blades. + * \param a Left blade bitmap. + * \param b Right blade bitmap. + * \return Multiplicative sign/metric coefficient. + */ template constexpr int geometric_product_sign(unsigned int a, unsigned int b) { int sign = 1; @@ -55,23 +62,17 @@ constexpr int geometric_product_sign(unsigned int a, unsigned int b) { return sign; } -// Concept for valid signatures +/// \brief Concept for valid signature-like types. template concept IsSignature = requires { { T::p } -> std::convertible_to; { T::dim } -> std::convertible_to; }; -// Forward declaration +/// \brief Forward declaration of multivector type. template struct Multivector; -// ======================================================================== -// 2. KERNEL INTERFACE ( The "Engine" ) -// ======================================================================== -// This struct defines HOW we multiply. -// The default implementation uses the generic compile-time unroll. -// We will specialize this for Euclidean3D, PGA, CGA. - +/// \brief Product-kernel policy object used by `Multivector`. template struct AlgebraKernels { using MV = Multivector; @@ -115,22 +116,28 @@ template struct AlgebraKernels { return result; } - // --- PUBLIC API --- + /** + * \brief Geometric product. + * \param a Left operand. + * \param b Right operand. + * \return Product multivector. + */ static constexpr MV geometric_product(const MV &a, const MV &b) { return product_generic(a, b, std::make_index_sequence{}); } + /** + * \brief Exterior/wedge product. + * \param a Left operand. + * \param b Right operand. + * \return Wedge-product multivector. + */ static constexpr MV wedge_product(const MV &a, const MV &b) { return product_generic(a, b, std::make_index_sequence{}); } }; -// ======================================================================== -// 3. SPECIALIZATION: EUCLIDEAN 3D (Cl(3,0)) -// ======================================================================== -// Hand-unrolled kernel for maximum performance. -// Eliminates all loops and complex template instantiation depth. - +/// \brief Hand-unrolled `Cl(3,0)` kernel specialization. template struct AlgebraKernels { using MV = Multivector; @@ -214,17 +221,20 @@ template struct AlgebraKernels { } }; -// ======================================================================== -// 4. MULTIVECTOR -// ======================================================================== +/// \brief Fixed-size multivector value type. template struct Multivector { static constexpr size_t Size = Sig::size; alignas(xsimd::default_arch::alignment()) std::array data; - // Constructors + /// \brief Zero-initialized multivector. constexpr Multivector() : data{0} {} - // Helpers + /** + * \brief Build a multivector with one basis blade set to `scale`. + * \param bitmap Blade bitmap index. + * \param scale Coefficient value. + * \return Newly constructed multivector. + */ static constexpr Multivector from_blade(unsigned int bitmap, Field scale) { Multivector mv; if (bitmap < Sig::size) @@ -232,19 +242,42 @@ template struct Multivector { return mv; } + /** + * \brief Immutable component access. + * \param i Component index. + * \return Component value. + */ constexpr Field operator[](size_t i) const { return data[i]; } + /** + * \brief Mutable component access. + * \param i Component index. + * \return Mutable reference to component. + */ constexpr Field &operator[](size_t i) { return data[i]; } - // --- ARITHMETIC OPERATORS (Delegating to Kernel) --- - + /** + * \brief Geometric product. + * \param other Right-hand operand. + * \return Product multivector. + */ constexpr Multivector operator*(const Multivector &other) const { return AlgebraKernels::geometric_product(*this, other); } + /** + * \brief Wedge product. + * \param other Right-hand operand. + * \return Wedge-product multivector. + */ constexpr Multivector operator^(const Multivector &other) const { return AlgebraKernels::wedge_product(*this, other); } + /** + * \brief Component-wise addition. + * \param other Right-hand operand. + * \return Sum multivector. + */ constexpr Multivector operator+(const Multivector &other) const { Multivector res; for (size_t i = 0; i < Size; ++i) @@ -252,6 +285,11 @@ template struct Multivector { return res; } + /** + * \brief Component-wise subtraction. + * \param other Right-hand operand. + * \return Difference multivector. + */ constexpr Multivector operator-(const Multivector &other) const { Multivector res; for (size_t i = 0; i < Size; ++i) @@ -260,10 +298,9 @@ template struct Multivector { } }; -// ======================================================================== -// 5. WIDE TYPES -// ======================================================================== +/// \brief SIMD packet scalar used by wide multivectors. using Packet = xsimd::batch; +/// \brief Multivector whose scalar components are SIMD packets. template using WideMultivector = Multivector; -} // namespace igneous::core \ No newline at end of file +} // namespace igneous::core diff --git a/include/igneous/core/blades.hpp b/include/igneous/core/blades.hpp index 69d50b3..aca5b0c 100644 --- a/include/igneous/core/blades.hpp +++ b/include/igneous/core/blades.hpp @@ -3,45 +3,76 @@ namespace igneous::core { -// A pure 3-component Bivector (xy, yz, zx) -// Used for Cross Products / Areas +/// \brief 3D bivector with `(xy, yz, zx)` components. struct Bivec3 { - float xy, yz, - zx; // e12, e23, e31 (Order varies by convention, sticking to xy, yz, zx) + /// \brief XY plane component (`e12`). + float xy; + /// \brief YZ plane component (`e23`). + float yz; + /// \brief ZX plane component (`e31`). + float zx; - // Magnitude Squared: |B|^2 = -B*B (technically). - // For Euclidean, it's just sum of squares. + /** + * \brief Squared Euclidean magnitude. + * \return Squared norm. + */ float norm_sq() const { return xy * xy + yz * yz + zx * zx; } + /** + * \brief Euclidean magnitude. + * \return Norm. + */ float norm() const { return std::sqrt(norm_sq()); } }; -// A pure 3-component Vector (x, y, z) +/// \brief Lightweight 3D vector utility for geometry kernels. struct Vec3 { - float x, y, z; + /// \brief X coordinate. + float x; + /// \brief Y coordinate. + float y; + /// \brief Z coordinate. + float z; - // Addition + /** + * \brief Vector addition. + * \param o Right-hand operand. + * \return Sum vector. + */ Vec3 operator+(const Vec3 &o) const { return {x + o.x, y + o.y, z + o.z}; } + /** + * \brief Vector subtraction. + * \param o Right-hand operand. + * \return Difference vector. + */ Vec3 operator-(const Vec3 &o) const { return {x - o.x, y - o.y, z - o.z}; } - // Scalar Mult + /** + * \brief Scalar multiplication. + * \param s Scalar factor. + * \return Scaled vector. + */ Vec3 operator*(float s) const { return {x * s, y * s, z * s}; } - // Dot Product (Inner Product) -> Scalar - // Note: In GA, a|b is the scalar part of ab + /** + * \brief Dot product. + * \param o Right-hand operand. + * \return Dot product scalar. + */ float dot(const Vec3 &o) const { return x * o.x + y * o.y + z * o.z; } - // Wedge Product (Outer Product) -> Bivector - // This is the "Cross Product" logic + /** + * \brief Exterior product producing a bivector. + * \param o Right-hand operand. + * \return Bivector wedge product. + */ Bivec3 operator^(const Vec3 &o) const { return { - x * o.y - y * o.x, // xy (e12) - y * o.z - z * o.y, // yz (e23) - z * o.x - x * o.z // zx (e31/e13 sign depends on basis) - // Note: Standard Euclidean cross product result corresponds to dual of - // wedge. e1^e2 = e12. + x * o.y - y * o.x, + y * o.z - z * o.y, + z * o.x - x * o.z }; } }; -} // namespace igneous::core \ No newline at end of file +} // namespace igneous::core diff --git a/include/igneous/core/gpu.hpp b/include/igneous/core/gpu.hpp index c86f909..99b7391 100644 --- a/include/igneous/core/gpu.hpp +++ b/include/igneous/core/gpu.hpp @@ -8,6 +8,10 @@ namespace igneous::core::gpu { +/** + * \brief Whether `IGNEOUS_GPU_FORCE` requests unconditional GPU attempts. + * \return `true` when force-offload is enabled. + */ inline bool gpu_force_enabled() { const char *raw = std::getenv("IGNEOUS_GPU_FORCE"); if (raw == nullptr) { @@ -20,6 +24,10 @@ inline bool gpu_force_enabled() { return value == "1" || value == "true" || value == "yes" || value == "on"; } +/** + * \brief Minimum row count for GPU offload (`IGNEOUS_GPU_MIN_ROWS`). + * \return Row threshold. + */ inline int gpu_min_rows() { const char *raw = std::getenv("IGNEOUS_GPU_MIN_ROWS"); if (raw != nullptr) { @@ -31,6 +39,10 @@ inline int gpu_min_rows() { return 8192; } +/** + * \brief Minimum `rows * steps` for multi-step GPU offload. + * \return Work threshold. + */ inline long long gpu_min_row_steps() { const char *raw = std::getenv("IGNEOUS_GPU_MIN_ROW_STEPS"); if (raw != nullptr) { @@ -42,10 +54,25 @@ inline long long gpu_min_row_steps() { return 200000; } +/// \brief Report whether a GPU backend is available at runtime. [[nodiscard]] bool available(); +/** + * \brief Invalidate cached GPU resources associated with `cache_key`. + * \param cache_key Cache identifier (typically structure address). + */ void invalidate_markov_cache(const void *cache_key); +/** + * \brief GPU Markov single-step transition. + * \param cache_key Cache identifier. + * \param row_offsets CSR row offsets. + * \param col_indices CSR column indices. + * \param weights CSR values. + * \param input Input scalar field. + * \param output Output scalar field. + * \return `true` when GPU path executed successfully. + */ [[nodiscard]] bool apply_markov_transition(const void *cache_key, std::span row_offsets, std::span col_indices, @@ -53,11 +80,34 @@ void invalidate_markov_cache(const void *cache_key); std::span input, std::span output); +/** + * \brief GPU Markov multi-step transition. + * \param cache_key Cache identifier. + * \param row_offsets CSR row offsets. + * \param col_indices CSR column indices. + * \param weights CSR values. + * \param input Input scalar field. + * \param steps Number of repeated transitions. + * \param output Output scalar field. + * \return `true` when GPU path executed successfully. + */ [[nodiscard]] bool apply_markov_transition_steps( const void *cache_key, std::span row_offsets, std::span col_indices, std::span weights, std::span input, int steps, std::span output); +/** + * \brief GPU carré du champ evaluation over CSR diffusion graph. + * \param cache_key Cache identifier. + * \param row_offsets CSR row offsets. + * \param col_indices CSR column indices. + * \param weights CSR values. + * \param f First scalar field. + * \param h Second scalar field. + * \param inv_2t Scaling factor. + * \param output Output gamma field. + * \return `true` when GPU path executed successfully. + */ [[nodiscard]] bool carre_du_champ(const void *cache_key, std::span row_offsets, std::span col_indices, diff --git a/include/igneous/core/memory.hpp b/include/igneous/core/memory.hpp index 4d40cce..4cd83fd 100644 --- a/include/igneous/core/memory.hpp +++ b/include/igneous/core/memory.hpp @@ -6,31 +6,52 @@ namespace igneous::core { -// A simple linear allocator (Arena). -// It grabs a big chunk of memory upfront and hands it out sequentially. -// Deallocation only happens when the entire arena is reset or destroyed. +/** + * \brief Monotonic arena allocator backed by a contiguous byte buffer. + * + * Individual deallocation is intentionally unsupported; callers reset the arena + * when all allocations can be discarded together. + */ class MemoryArena : public std::pmr::memory_resource { private: - std::vector buffer; // The backing storage - std::byte *ptr = nullptr; // Current allocation pointer - std::size_t offset = 0; // Current offset in bytes + /// \brief Owned backing storage. + std::vector buffer; + /// \brief Base pointer into `buffer`. + std::byte *ptr = nullptr; + /// \brief Current linear allocation offset in bytes. + std::size_t offset = 0; public: - // Reserve a big block of memory (default 1MB for now) + /** + * \brief Construct arena with fixed capacity (`1 MiB` default). + * \param size_bytes Backing buffer size in bytes. + */ explicit MemoryArena(std::size_t size_bytes = 1024 * 1024) { buffer.resize(size_bytes); ptr = buffer.data(); } - // Reset the arena (wipe all allocations instantly) + /// \brief Reset all allocations in constant time. void reset() { offset = 0; } - // Get usage statistics + /** + * \brief Number of bytes currently allocated from this arena. + * \return Used bytes. + */ std::size_t used_bytes() const { return offset; } + /** + * \brief Total arena capacity in bytes. + * \return Total bytes. + */ std::size_t total_bytes() const { return buffer.size(); } protected: - // Implementation of do_allocate (required by std::pmr) + /** + * \brief `std::pmr` allocation entry point. + * \param bytes Requested size in bytes. + * \param alignment Requested alignment. + * \return Pointer to allocated storage. + */ void *do_allocate(std::size_t bytes, std::size_t alignment) override { // Calculate padding needed for alignment std::size_t padding = 0; @@ -45,7 +66,7 @@ class MemoryArena : public std::pmr::memory_resource { } if (offset + padding + bytes > buffer.size()) { - throw std::bad_alloc(); // Arena is full! + throw std::bad_alloc(); } void *result = ptr + offset + padding; @@ -53,21 +74,28 @@ class MemoryArena : public std::pmr::memory_resource { return result; } - // Linear allocators don't support individual deallocation. - // We just ignore calls to deallocate. + /** + * \brief No-op for monotonic allocation strategy. + * \param p Ignored. + * \param bytes Ignored. + * \param alignment Ignored. + */ void do_deallocate(void *p, std::size_t bytes, std::size_t alignment) override { - // No-op. Memory is freed when Arena is reset or destroyed. (void)p; (void)bytes; (void)alignment; } - // Comparison (two arenas are equal if they are the same object) + /** + * \brief Memory resources compare equal only by object identity. + * \param other Resource to compare against. + * \return `true` if both references point to the same object. + */ bool do_is_equal(const std::pmr::memory_resource &other) const noexcept override { return this == &other; } }; -} // namespace igneous::core \ No newline at end of file +} // namespace igneous::core diff --git a/include/igneous/core/parallel.hpp b/include/igneous/core/parallel.hpp index 9876f1f..22cf778 100644 --- a/include/igneous/core/parallel.hpp +++ b/include/igneous/core/parallel.hpp @@ -13,12 +13,17 @@ namespace igneous::core { +/// \brief Runtime compute backend choice. enum class ComputeBackend { Cpu, CpuParallel, Gpu }; +/** + * \brief Parse compute backend from `IGNEOUS_BACKEND`. + * \return Selected backend enum value. + */ inline ComputeBackend compute_backend_from_env() { const char *raw = std::getenv("IGNEOUS_BACKEND"); if (raw == nullptr) { @@ -39,6 +44,10 @@ inline ComputeBackend compute_backend_from_env() { return ComputeBackend::CpuParallel; } +/** + * \brief Desired worker count from env or hardware fallback. + * \return Worker count used by parallel loops. + */ inline int compute_thread_count() { const char *raw = std::getenv("IGNEOUS_NUM_THREADS"); if (raw != nullptr) { @@ -55,6 +64,10 @@ inline int compute_thread_count() { return static_cast(hw); } +/** + * \brief Hardware concurrency with safe fallback to `1`. + * \return Hardware thread count. + */ inline int hardware_thread_count() { const unsigned hw = std::thread::hardware_concurrency(); if (hw == 0) { @@ -63,8 +76,18 @@ inline int hardware_thread_count() { return static_cast(hw); } +/** + * \brief Reusable worker pool for index-based parallel loops. + * + * The caller thread participates in work execution; worker threads consume + * chunks from a shared atomic index. + */ class ParallelWorkerPool { public: + /** + * \brief Construct pool with up to `max_workers - 1` background threads. + * \param max_workers Total participants including caller thread. + */ explicit ParallelWorkerPool(int max_workers) : max_workers_(std::max(0, max_workers - 1)) { workers_.reserve(static_cast(max_workers_)); @@ -73,6 +96,7 @@ class ParallelWorkerPool { } } + /// \brief Stop workers and release pool resources. ~ParallelWorkerPool() { { std::lock_guard lock(mutex_); @@ -88,6 +112,13 @@ class ParallelWorkerPool { ParallelWorkerPool(const ParallelWorkerPool &) = delete; ParallelWorkerPool &operator=(const ParallelWorkerPool &) = delete; + /** + * \brief Execute `[begin, end)` with up to `requested_workers` participants. + * \param begin Inclusive loop start. + * \param end Exclusive loop end. + * \param requested_workers Requested total participants. + * \param fn Loop body receiving index `i`. + */ template void run(int begin, int end, int requested_workers, Fn &&fn) { if (end <= begin) { @@ -123,12 +154,14 @@ class ParallelWorkerPool { } private: + /// \brief Block until all participants complete the current generation. void wait_for_task_completion() { std::unique_lock lock(mutex_); cv_done_.wait(lock, [this]() { return remaining_.load(std::memory_order_acquire) == 0; }); job_ = nullptr; } + /// \brief Worker thread body waiting on generation changes. void worker_loop(int worker_idx) { uint64_t seen_generation = 0; while (true) { @@ -147,6 +180,7 @@ class ParallelWorkerPool { } } + /// \brief Consume chunks for the active generation. void run_chunks() { while (true) { const int chunk_begin = next_.fetch_add(grain_, std::memory_order_relaxed); @@ -165,30 +199,57 @@ class ParallelWorkerPool { } } + /// \brief Maximum number of background worker threads. int max_workers_ = 0; + /// \brief Background worker threads. std::vector workers_; + /// \brief Synchronizes control-plane state. std::mutex mutex_; + /// \brief Signals new work generation. std::condition_variable cv_work_; + /// \brief Signals generation completion. std::condition_variable cv_done_; + /// \brief Requests worker shutdown. bool stop_ = false; + /// \brief Monotonic work-generation counter. uint64_t generation_ = 0; + /// \brief Number of worker threads participating in current run. int active_workers_ = 0; + /// \brief Current loop begin index. int begin_ = 0; + /// \brief Current loop end index. int end_ = 0; + /// \brief Chunk size used by workers. int grain_ = 1; + /// \brief Next unclaimed index for dynamic chunking. std::atomic next_{0}; + /// \brief Remaining participants for completion signaling. std::atomic remaining_{0}; + /// \brief Active work function. std::function job_; }; +/** + * \brief Singleton worker pool used by `parallel_for_index`. + * \return Process-wide worker pool instance. + */ inline ParallelWorkerPool ¶llel_worker_pool() { static ParallelWorkerPool pool(hardware_thread_count()); return pool; } +/** + * \brief Execute an integer index loop potentially in parallel. + * + * Parallel execution is gated by backend configuration and loop size. + * \param begin Inclusive loop start. + * \param end Exclusive loop end. + * \param fn Loop body receiving index `i`. + * \param min_parallel_range Minimum range length to enable parallel execution. + */ template void parallel_for_index(int begin, int end, Fn &&fn, int min_parallel_range = 32) { if (end <= begin) { diff --git a/include/igneous/data/space.hpp b/include/igneous/data/space.hpp index 4456021..708351f 100644 --- a/include/igneous/data/space.hpp +++ b/include/igneous/data/space.hpp @@ -12,56 +12,133 @@ namespace igneous::data { +/** + * \brief Geometry container parameterized by a single structure type. + * + * `Space` owns: + * - flattened SoA geometry (`x`, `y`, `z`) + * - one structure instance (`structure`) + * - optional user-facing name (`name`) + * + * The structure is intentionally explicit and built by callers when needed. + */ template struct Space { + /// \brief Alias for the instantiated structure type. using StructureType = StructureT; + /// \brief X coordinates for all points. std::vector x; + /// \brief Y coordinates for all points. std::vector y; + /// \brief Z coordinates for all points. std::vector z; + /// \brief Structure data associated with this geometry. StructureT structure; + /// \brief Optional descriptive identifier. std::string name; + /** + * \brief Number of stored points. + * \return Number of points in the geometry arrays. + */ [[nodiscard]] size_t num_points() const { return x.size(); } + /** + * \brief Reserve geometry capacity for `vertices` points. + * \param vertices Number of points to reserve capacity for. + */ void reserve(size_t vertices) { x.reserve(vertices); y.reserve(vertices); z.reserve(vertices); } + /** + * \brief Resize geometry arrays to `vertices` points. + * \param vertices New point count. + */ void resize(size_t vertices) { x.resize(vertices); y.resize(vertices); z.resize(vertices); } + /** + * \brief Read a 3D point as `core::Vec3`. + * \param i Point index. + * \return Point coordinates at index `i`. + */ [[nodiscard]] core::Vec3 get_vec3(size_t i) const { return {x[i], y[i], z[i]}; } + /** + * \brief Overwrite a 3D point from `core::Vec3`. + * \param i Point index. + * \param v Replacement coordinate value. + */ void set_vec3(size_t i, const core::Vec3 &v) { x[i] = v.x; y[i] = v.y; z[i] = v.z; } + /** + * \brief Append a new point to all coordinate channels. + * \param v Point to append. + */ void push_point(const core::Vec3 &v) { x.push_back(v.x); y.push_back(v.y); z.push_back(v.z); } + /** + * \brief Immutable view of X coordinates. + * \return Span over `x`. + */ [[nodiscard]] std::span x_span() const { return x; } + /** + * \brief Immutable view of Y coordinates. + * \return Span over `y`. + */ [[nodiscard]] std::span y_span() const { return y; } + /** + * \brief Immutable view of Z coordinates. + * \return Span over `z`. + */ [[nodiscard]] std::span z_span() const { return z; } + /** + * \brief Mutable view of X coordinates. + * \return Mutable span over `x`. + */ [[nodiscard]] std::span x_span() { return x; } + /** + * \brief Mutable view of Y coordinates. + * \return Mutable span over `y`. + */ [[nodiscard]] std::span y_span() { return y; } + /** + * \brief Mutable view of Z coordinates. + * \return Mutable span over `z`. + */ [[nodiscard]] std::span z_span() { return z; } + /** + * \brief Immutable grouped axis spans in X/Y/Z order. + * \return Array `{x_span(), y_span(), z_span()}`. + */ [[nodiscard]] std::array, 3> xyz_spans() const { return {x_span(), y_span(), z_span()}; } + /** + * \brief Lightweight validity check for geometry + structure presence. + * + * This does not verify deep structure invariants; it checks whether geometry + * is non-empty and, for surface structures, faces exist. + * \return `true` when geometry/structure satisfy minimal checks. + */ [[nodiscard]] bool is_valid() const { if (num_points() == 0) { return false; @@ -74,6 +151,7 @@ template struct Space { return true; } + /// \brief Clear geometry, structure, and name. void clear() { x.clear(); y.clear(); diff --git a/include/igneous/data/structure.hpp b/include/igneous/data/structure.hpp index 763b6fa..16f96dd 100644 --- a/include/igneous/data/structure.hpp +++ b/include/igneous/data/structure.hpp @@ -8,6 +8,15 @@ namespace igneous::data { +/** + * \brief Concept for a structure object carried by `data::Space`. + * + * A valid structure type must define: + * - a build input type (`T::Input`) + * - a topological dimension (`T::DIMENSION`) + * - explicit lifecycle methods (`build`, `clear`) + * - a neighborhood query (`get_neighborhood`) + */ template concept Structure = requires(T &t, const T &ct, uint32_t idx) { typename T::Input; @@ -17,6 +26,11 @@ concept Structure = requires(T &t, const T &ct, uint32_t idx) { { ct.get_neighborhood(idx) } -> std::convertible_to>; }; +/** + * \brief Refinement of `Structure` for surface/triangle-based connectivity. + * + * Surface structures expose face and vertex adjacency needed by DEC operators. + */ template concept SurfaceStructure = Structure && requires(T &t, const T &ct, size_t f_idx, size_t v_idx, int corner) { diff --git a/include/igneous/data/structures/diffusion_geometry.hpp b/include/igneous/data/structures/diffusion_geometry.hpp index e55995d..b567f58 100644 --- a/include/igneous/data/structures/diffusion_geometry.hpp +++ b/include/igneous/data/structures/diffusion_geometry.hpp @@ -19,53 +19,96 @@ namespace igneous::data { +/** + * \brief Point-cloud diffusion structure with kNN Markov geometry. + * + * This structure builds: + * - row-stochastic Markov CSR (`markov_*`) + * - symmetric kernel CSR (`symmetric_*`) for spectral solves + * - stationary density (`mu`) + * - auxiliary geometric fields used by downstream operators + */ struct DiffusionGeometry { + /// \brief Dimension marker used by the `Structure` concept. static constexpr int DIMENSION = 0; + /// \brief Parameters for diffusion graph construction. struct Input { + /// \brief X coordinates of points. std::span x; + /// \brief Y coordinates of points. std::span y; + /// \brief Z coordinates of points. std::span z; + /// \brief Number of nearest neighbors per row. int k_neighbors = 32; + /// \brief Number of neighbors used for local bandwidth estimation. int knn_bandwidth = 8; + /// \brief Density-adaptive bandwidth exponent. float bandwidth_variability = -0.5f; + /// \brief Drift tuning parameter from the reference construction. float c = 0.0f; + /// \brief Center `carre_du_champ` with row means instead of self values. bool use_mean_centres = true; }; + /// \brief Stationary measure induced by the symmetric kernel. Eigen::VectorXf mu; + /// \brief Spectral basis produced by diffusion eigensolves. Eigen::MatrixXf eigen_basis; + /// \brief Per-point local diffusion timescale. Eigen::VectorXf local_bandwidths; + /// \brief Row sums of the symmetric kernel. Eigen::VectorXf symmetric_row_sums; + /// \brief Markov-averaged embedding coordinates. Eigen::MatrixXf immersion_coords; + /// \brief Runtime toggle for centered `carre_du_champ`. bool use_mean_centres = true; + /// \brief Effective k used by the latest build. int knn_k = 0; - // Dense kNN row storage used by the reference-style diffusion build. + /// \brief Dense kNN column indices (`n * k`). std::vector knn_indices; + /// \brief Dense kNN distances (`n * k`). std::vector knn_distances; + /// \brief Dense row-stochastic kernel values (`n * k`). std::vector knn_kernel; - // Direct CSR view for hot diffusion operators. + /// \brief CSR row offsets for Markov transition matrix. std::vector markov_row_offsets; + /// \brief CSR column indices for Markov transition matrix. std::vector markov_col_indices; + /// \brief CSR values for Markov transition matrix. std::vector markov_values; - // Symmetric kernel used to reproduce reference spectral basis construction. + /// \brief CSR row offsets for symmetric kernel matrix. std::vector symmetric_row_offsets; + /// \brief CSR column indices for symmetric kernel matrix. std::vector symmetric_col_indices; + /// \brief CSR values for symmetric kernel matrix. std::vector symmetric_values; + /** + * \brief Number of points represented by the CSR graph. + * \return Row count inferred from `markov_row_offsets`. + */ [[nodiscard]] size_t num_primitives() const { if (markov_row_offsets.empty()) { return 0; } return markov_row_offsets.size() - 1; } - [[nodiscard]] std::span get_neighborhood(uint32_t) const { + /** + * \brief Neighborhood accessor required by `data::Structure`. + * \param vertex_idx Unused. + * \return Empty span (diffusion neighborhoods are held in CSR arrays). + */ + [[nodiscard]] std::span get_neighborhood(uint32_t vertex_idx) const { + (void)vertex_idx; return {}; } + /// \brief Clear all diffusion data and invalidate any GPU-side cache. void clear() { core::gpu::invalidate_markov_cache(this); mu.resize(0); @@ -86,13 +129,19 @@ struct DiffusionGeometry { symmetric_values.clear(); } + /// \brief nanoflann adaptor over the SoA coordinate spans. struct PointCloudAdaptor { + /// \brief X coordinate channel. std::span x; + /// \brief Y coordinate channel. std::span y; + /// \brief Z coordinate channel. std::span z; + /// \brief Number of points available to the k-d tree. [[nodiscard]] size_t kdtree_get_point_count() const { return x.size(); } + /// \brief Coordinate accessor required by nanoflann. [[nodiscard]] float kdtree_get_pt(size_t idx, size_t dim) const { if (dim == 0) { return x[idx]; @@ -103,13 +152,19 @@ struct DiffusionGeometry { return z[idx]; } + /// \brief Bounding box callback (unused for this adaptor). template bool kdtree_get_bbox(BBOX &) const { return false; } }; + /// \brief nanoflann 3D L2 tree type. using KDTree = nanoflann::KDTreeSingleIndexAdaptor< nanoflann::L2_Simple_Adaptor, PointCloudAdaptor, 3>; + /** + * \brief Candidate epsilon sweep used during kernel tuning. + * \return Monotonic list of epsilon values. + */ static std::vector build_epsilons() { std::vector epsilons; epsilons.reserve(80); @@ -119,6 +174,14 @@ struct DiffusionGeometry { return epsilons; } + /** + * \brief Tune kernel scale and intrinsic dimension surrogate. + * \param entries Squared-distance-like kernel arguments. + * \param n Number of points. + * \param k Number of neighbors per point. + * \param epsilons Candidate kernel scales. + * \return Pair `(epsilon, dim_surrogate)`. + */ static std::pair tune_kernel(const std::vector &entries, int n, int k, const std::vector &epsilons) { @@ -159,6 +222,14 @@ struct DiffusionGeometry { return {epsilon, dim}; } + /** + * \brief Compute per-point local bandwidth estimates from kNN distances. + * \param nbr_distances Dense kNN distance matrix in row-major flat storage. + * \param n Number of points. + * \param k Number of neighbors per point. + * \param knn_bandwidth Number of neighbors used for RMS estimate. + * \param bandwidths_out Output vector of local bandwidths. + */ static void compute_local_bandwidths(const std::vector &nbr_distances, int n, int k, int knn_bandwidth, Eigen::VectorXf &bandwidths_out) { @@ -182,6 +253,11 @@ struct DiffusionGeometry { } } + /** + * \brief Median helper used for robust bandwidth normalization. + * \param values Input values (consumed by value). + * \return Median value (or `1.0f` when empty). + */ static float vector_median(std::vector values) { if (values.empty()) { return 1.0f; @@ -192,6 +268,10 @@ struct DiffusionGeometry { return values[mid]; } + /** + * \brief Build diffusion CSR structures from point coordinates. + * \param input Build parameters and coordinate spans. + */ void build(Input input) { core::gpu::invalidate_markov_cache(this); const size_t n_verts = input.x.size(); diff --git a/include/igneous/data/structures/discrete_exterior_calculus.hpp b/include/igneous/data/structures/discrete_exterior_calculus.hpp index 4d89a2b..3bb6054 100644 --- a/include/igneous/data/structures/discrete_exterior_calculus.hpp +++ b/include/igneous/data/structures/discrete_exterior_calculus.hpp @@ -10,26 +10,48 @@ namespace igneous::data { +/** + * \brief Triangle-surface structure used by DEC-style operators. + * + * Connectivity is stored in CSR-style arrays for face incidence and + * vertex-neighbor traversal. + */ struct DiscreteExteriorCalculus { + /// \brief Topological dimension of this structure. static constexpr int DIMENSION = 2; + /// \brief Input triangle index buffer (`[f0v0, f0v1, f0v2, ...]`). std::vector faces_to_vertices; + /// \brief Split face corner arrays for cache-friendly face traversal. std::vector face_v0; + /// \brief Split face corner arrays for cache-friendly face traversal. std::vector face_v1; + /// \brief Split face corner arrays for cache-friendly face traversal. std::vector face_v2; + /// \brief CSR offsets for face incidence per vertex. std::vector vertex_face_offsets; + /// \brief CSR payload of face indices per vertex. std::vector vertex_face_data; + /// \brief CSR offsets for 1-ring neighbors per vertex. std::vector vertex_neighbor_offsets; + /// \brief CSR payload of neighbor vertex indices. std::vector vertex_neighbor_data; + /// \brief Parameters used by `build`. struct Input { + /// \brief Number of vertices in the associated geometry. size_t num_vertices = 0; + /// \brief Whether to build vertex-neighbor CSR in addition to face incidence. bool build_vertex_neighbors = true; }; + /** + * \brief Number of triangles in the structure. + * \return Triangle count. + */ [[nodiscard]] size_t num_faces() const { if (!face_v0.empty()) { return face_v0.size(); @@ -37,6 +59,12 @@ struct DiscreteExteriorCalculus { return faces_to_vertices.size() / 3; } + /** + * \brief Return the vertex index for a face corner (`corner` in `[0,2]`). + * \param face_idx Face index. + * \param corner Corner index (`0`, `1`, or `2`). + * \return Vertex index for the requested corner. + */ [[nodiscard]] uint32_t get_vertex_for_face(size_t face_idx, int corner) const { switch (corner) { case 0: @@ -48,6 +76,11 @@ struct DiscreteExteriorCalculus { } } + /** + * \brief Faces incident to vertex `vertex_idx`. + * \param vertex_idx Vertex index. + * \return Span of incident face indices. + */ [[nodiscard]] std::span get_faces_for_vertex(uint32_t vertex_idx) const { if (vertex_idx + 1 >= vertex_face_offsets.size()) { return {}; @@ -58,6 +91,11 @@ struct DiscreteExteriorCalculus { return {&vertex_face_data[begin], end - begin}; } + /** + * \brief 1-ring neighbors for vertex `vertex_idx`. + * \param vertex_idx Vertex index. + * \return Span of neighboring vertex indices. + */ [[nodiscard]] std::span get_vertex_neighbors(uint32_t vertex_idx) const { if (vertex_idx + 1 >= vertex_neighbor_offsets.size()) { return {}; @@ -68,10 +106,21 @@ struct DiscreteExteriorCalculus { return {&vertex_neighbor_data[begin], end - begin}; } + /** + * \brief Generic neighborhood accessor required by `data::Structure`. + * \param vertex_idx Vertex index. + * \return Same data as `get_faces_for_vertex(vertex_idx)`. + */ [[nodiscard]] std::span get_neighborhood(uint32_t vertex_idx) const { return get_faces_for_vertex(vertex_idx); } + /** + * \brief Build CSR structures from `faces_to_vertices`. + * + * If `face_v*` arrays are already provided, they are reused. + * \param input Build configuration and vertex count. + */ void build(Input input) { const size_t num_vertices = input.num_vertices; @@ -260,6 +309,7 @@ struct DiscreteExteriorCalculus { 32768); } + /// \brief Drop all connectivity storage. void clear() { faces_to_vertices.clear(); face_v0.clear(); diff --git a/include/igneous/igneous.hpp b/include/igneous/igneous.hpp index addb78a..3335397 100644 --- a/include/igneous/igneous.hpp +++ b/include/igneous/igneous.hpp @@ -1,5 +1,8 @@ #pragma once +/// \file +/// \brief Umbrella header for the public igneous API. + #include #include #include diff --git a/include/igneous/io/exporter.hpp b/include/igneous/io/exporter.hpp index f14e555..56efa2a 100644 --- a/include/igneous/io/exporter.hpp +++ b/include/igneous/io/exporter.hpp @@ -17,6 +17,11 @@ namespace igneous::io { using igneous::data::Space; +/** + * \brief Map normalized scalar `t` in `[0,1]` to an RGB heatmap color. + * \param t Normalized scalar value. + * \return RGB tuple as 8-bit channels. + */ inline std::tuple get_heatmap_color_bytes(double t) { t = std::clamp(t, 0.0, 1.0); float r = 0.0f; @@ -42,6 +47,14 @@ inline std::tuple get_heatmap_color_bytes(double t) { }; } +/** + * \brief Compute robust color bounds via mean +/- `sigma_clip` * stddev. + * + * Non-finite values are skipped. + * \param field Scalar field values. + * \param sigma_clip Number of standard deviations used for clipping. + * \return Pair `(min_value, max_value)` used for color normalization. + */ template inline std::pair compute_field_bounds(std::span field, double sigma_clip) { double sum = 0.0; @@ -65,6 +78,13 @@ inline std::pair compute_field_bounds(std::span fie return {mean - (sigma_clip * std_dev), mean + (sigma_clip * std_dev)}; } +/** + * \brief Export a scalar field as colored point vertices in ASCII PLY. + * \param mesh Input space. + * \param field Scalar field values. + * \param filename Output PLY file path. + * \param sigma_clip Sigma clipping for color normalization. + */ template void export_ply(const Space &mesh, std::span field, const std::string &filename, double sigma_clip = 2.0) { @@ -102,6 +122,13 @@ void export_ply(const Space &mesh, std::span field, std::cout << "[IO] Exported PLY " << filename << "\n"; } +/** + * \brief `std::vector` overload for `export_ply`. + * \param mesh Input space. + * \param field Scalar field values. + * \param filename Output PLY file path. + * \param sigma_clip Sigma clipping for color normalization. + */ template void export_ply(const Space &mesh, const std::vector &field, const std::string &filename, double sigma_clip = 2.0) { @@ -109,6 +136,17 @@ void export_ply(const Space &mesh, const std::vector &field, sigma_clip); } +/** + * \brief Export a heatmap OBJ. + * + * Surface structures emit triangle faces; non-surface structures emit OBJ point + * records (`p`) referencing all vertices. + * + * \param mesh Input space. + * \param field Scalar field values. + * \param filename Output OBJ file path. + * \param sigma_clip Sigma clipping for color normalization. + */ template void export_heatmap(const Space &mesh, std::span field, const std::string &filename, double sigma_clip = 2.0) { @@ -153,6 +191,13 @@ void export_heatmap(const Space &mesh, std::span field, std::cout << "[IO] Exported OBJ " << filename << "\n"; } +/** + * \brief `std::vector` overload for `export_heatmap`. + * \param mesh Input space. + * \param field Scalar field values. + * \param filename Output OBJ file path. + * \param sigma_clip Sigma clipping for color normalization. + */ template void export_heatmap(const Space &mesh, const std::vector &field, @@ -161,6 +206,18 @@ void export_heatmap(const Space &mesh, filename, sigma_clip); } +/** + * \brief Export each point as a small tetrahedron in a colored PLY mesh. + * + * This is useful for visualizing point clouds in tools that primarily render + * polygonal meshes. + * + * \param mesh Input space. + * \param field Scalar field values. + * \param filename Output PLY file path. + * \param radius Tetrahedron scale per point. + * \param sigma_clip Sigma clipping for color normalization. + */ template void export_ply_solid(const Space &mesh, std::span field, const std::string &filename, double radius = 0.01, @@ -220,6 +277,14 @@ void export_ply_solid(const Space &mesh, std::span fiel std::cout << "[IO] Exported Solid PLY " << filename << "\n"; } +/** + * \brief `std::vector` overload for `export_ply_solid`. + * \param mesh Input space. + * \param field Scalar field values. + * \param filename Output PLY file path. + * \param radius Tetrahedron scale per point. + * \param sigma_clip Sigma clipping for color normalization. + */ template void export_ply_solid(const Space &mesh, const std::vector &field, diff --git a/include/igneous/io/importer.hpp b/include/igneous/io/importer.hpp index 1887e5f..090e4a9 100644 --- a/include/igneous/io/importer.hpp +++ b/include/igneous/io/importer.hpp @@ -12,6 +12,17 @@ namespace igneous::io { using igneous::data::Space; +/** + * \brief Load geometry/connectivity from an OBJ file into a `Space`. + * + * This function is intentionally load-only: + * - point positions are parsed from `v` records + * - surface faces are parsed from `f` records when `StructureT` is a surface structure + * - no `structure.build(...)` call is performed + * + * \param mesh Destination space to overwrite. + * \param filename OBJ file path. + */ template void load_obj(Space &mesh, const std::string &filename) { std::ifstream file(filename); diff --git a/include/igneous/ops/dec/curvature.hpp b/include/igneous/ops/dec/curvature.hpp index 163001c..17a0584 100644 --- a/include/igneous/ops/dec/curvature.hpp +++ b/include/igneous/ops/dec/curvature.hpp @@ -13,10 +13,21 @@ namespace igneous::ops::dec { +/// \brief Scratch storage reused by curvature evaluation. template struct CurvatureWorkspace { + /// \brief Unnormalized per-face bivector normals. std::vector face_normals; }; +/** + * \brief Compute mean (`H`) and Gaussian (`K`) curvature approximations. + * + * Uses angle-defect and neighborhood Laplacian estimates on triangle surfaces. + * \param space Input surface space. + * \param H Output mean-curvature-like scalar values per vertex. + * \param K Output Gaussian-curvature-like scalar values per vertex. + * \param workspace Reused temporary buffers. + */ template void compute_curvature_measures(const data::Space &space, std::vector &H, diff --git a/include/igneous/ops/dec/flow.hpp b/include/igneous/ops/dec/flow.hpp index 17adf11..c1112c0 100644 --- a/include/igneous/ops/dec/flow.hpp +++ b/include/igneous/ops/dec/flow.hpp @@ -10,10 +10,20 @@ namespace igneous::ops::dec { +/// \brief Scratch storage reused by curvature-flow integration. template struct FlowWorkspace { + /// \brief Per-vertex displacement computed from local neighborhood averaging. std::vector displacements; }; +/** + * \brief Advance one explicit mean-curvature flow step. + * + * The update is a neighborhood average displacement scaled by `dt`. + * \param space Input/output surface space. + * \param dt Time step scale. + * \param workspace Reused temporary buffers. + */ template void integrate_mean_curvature_flow(data::Space &space, float dt, FlowWorkspace &workspace) { diff --git a/include/igneous/ops/diffusion/basis.hpp b/include/igneous/ops/diffusion/basis.hpp index 8d64f20..652b341 100644 --- a/include/igneous/ops/diffusion/basis.hpp +++ b/include/igneous/ops/diffusion/basis.hpp @@ -12,22 +12,40 @@ namespace igneous::ops::diffusion { +/// \brief Dense lookup tables for wedge-product index expansion. struct WedgeProductIndexData { + /// \brief Output component index for each wedge term. std::vector target_indices; + /// \brief Left operand component index for each wedge term. std::vector left_indices; + /// \brief Right operand component index for each wedge term. std::vector right_indices; + /// \brief Orientation sign for each wedge term. std::vector signs; + /// \brief Number of target basis components. int n_targets = 0; + /// \brief Number of left/right split patterns. int n_splits = 0; }; +/// \brief Child-index tables used by weak exterior derivative assembly. struct Kp1ChildrenAndSignsData { + /// \brief Basis indices for k-forms. std::vector> idx_k; + /// \brief Basis indices for (k+1)-forms. std::vector> idx_kp1; + /// \brief Child mapping from each (k+1)-index to k-indices. std::vector> children; + /// \brief Alternating signs applied during assembly. std::vector signs; }; +/** + * \brief Binomial coefficient helper (`n choose k`). + * \param n Universe size. + * \param k Selection size. + * \return Binomial coefficient value. + */ inline int binomial_coeff(int n, int k) { if (k < 0 || k > n) { return 0; @@ -43,6 +61,14 @@ inline int binomial_coeff(int n, int k) { return static_cast(result); } +/** + * \brief Recursive generator for lexicographic combinations. + * \param n Universe size. + * \param k Selection size. + * \param start First candidate value. + * \param current Current partial combination. + * \param out Destination list of combinations. + */ inline void combinations_recursive(int n, int k, int start, std::vector ¤t, std::vector> &out) { @@ -59,6 +85,12 @@ inline void combinations_recursive(int n, int k, int start, } } +/** + * \brief Enumerate ordered wedge-basis index combinations for degree `k`. + * \param d Ambient dimension. + * \param k Exterior degree. + * \return Lexicographically ordered basis-index combinations. + */ inline std::vector> get_wedge_basis_indices(int d, int k) { if (k < 0 || k > d) { return {}; @@ -76,6 +108,12 @@ inline std::vector> get_wedge_basis_indices(int d, int k) { return out; } +/** + * \brief Lexicographic rank of a sorted combination. + * \param comb Sorted combination indices. + * \param d Ambient dimension. + * \return Zero-based rank among all `k`-combinations. + */ inline int lex_rank_combination(const std::vector &comb, int d) { const int k = static_cast(comb.size()); if (k == 0) { @@ -94,6 +132,12 @@ inline int lex_rank_combination(const std::vector &comb, int d) { return rank; } +/** + * \brief Precompute child indices and signs for `d_k` style expansions. + * \param d Ambient dimension. + * \param k Exterior degree. + * \return Tables used to assemble `(k+1)` from `k` components. + */ inline Kp1ChildrenAndSignsData kp1_children_and_signs(int d, int k) { if (k < 1 || k > d - 1) { throw std::invalid_argument("kp1_children_and_signs requires 1 <= k <= d-1"); @@ -126,6 +170,12 @@ inline Kp1ChildrenAndSignsData kp1_children_and_signs(int d, int k) { return out; } +/** + * \brief Return complement indices of `subset` in `[0, total)`. + * \param total Universe size. + * \param subset Sorted subset indices. + * \return Sorted complement indices. + */ inline std::vector complement_indices(int total, const std::vector &subset) { std::vector out; out.reserve(static_cast(total - static_cast(subset.size()))); @@ -140,6 +190,15 @@ inline std::vector complement_indices(int total, const std::vector &su return out; } +/** + * \brief Precompute sparse mapping from `(k1,k2)` components to `k1+k2` wedge components. + * + * The output drives fast pointwise wedge assembly in diffusion-form operators. + * \param d Ambient dimension. + * \param k1 Left exterior degree. + * \param k2 Right exterior degree. + * \return Index/sign mapping for wedge assembly. + */ inline WedgeProductIndexData get_wedge_product_indices(int d, int k1, int k2) { WedgeProductIndexData out; const int k_total = k1 + k2; diff --git a/include/igneous/ops/diffusion/forms.hpp b/include/igneous/ops/diffusion/forms.hpp index 859d656..d6d6a85 100644 --- a/include/igneous/ops/diffusion/forms.hpp +++ b/include/igneous/ops/diffusion/forms.hpp @@ -17,24 +17,46 @@ namespace igneous::ops::diffusion { +/// \brief Shared scratch buffers for diffusion k-form operators. template struct DiffusionFormWorkspace { + /// \brief Coordinate vectors used for gamma evaluations. std::array coords; + /// \brief `Gamma(x_a, x_b)` caches. std::array, 3> gamma_coords; + /// \brief `Gamma(x_a, phi_i)` matrix cache per axis. std::array gamma_mixed; + /// \brief Generic temporary gamma vector. Eigen::VectorXf gamma_tmp; + /// \brief Number of columns currently available in `gamma_mixed`. int gamma_mixed_cols = 0; + /// \brief Whether `gamma_coords` has been initialized. bool gamma_coords_ready = false; }; +/// \brief Precomputed compound-minor determinants for metric pullbacks. struct CompoundDeterminantData { + /// \brief Basis index combinations for the target degree. std::vector> indices; + /// \brief Determinant vectors for each basis-pair minor. std::vector values; + /// \brief Degree (`k`) these determinants correspond to. int degree = 0; + /// \brief Number of basis components for this degree. int n_components = 0; }; +/** + * \brief Ambient coordinate dimension used by current diffusion form operators. + * \return Ambient dimension (`3`). + */ inline int ambient_dim_3d() { return 3; } +/** + * \brief Determinant of a tiny dense matrix packed row-major in `data`. + * \param data Matrix values in row-major order. + * \param k Matrix dimension. + * \return Determinant value. + */ inline float determinant_small(const float *data, int k) { if (k == 0) { return 1.0f; @@ -59,6 +81,13 @@ inline float determinant_small(const float *data, int k) { return a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g); } +/** + * \brief Build per-point minors of `Gamma(x,x)` for selected row/column subsets. + * \param gamma_coords Precomputed coordinate gamma tensors. + * \param rows Row-index subset. + * \param cols Column-index subset. + * \return Vector of determinant values per point. + */ inline Eigen::VectorXf build_minor_det_vector( const std::array, 3> &gamma_coords, const std::vector &rows, const std::vector &cols) { @@ -79,6 +108,11 @@ inline Eigen::VectorXf build_minor_det_vector( return out; } +/** + * \brief Ensure `workspace.gamma_coords` is initialized. + * \param mesh Input diffusion space. + * \param workspace Scratch workspace to populate. + */ template void ensure_gamma_coords(const MeshT &mesh, DiffusionFormWorkspace &workspace) { if (workspace.gamma_coords_ready) { @@ -100,6 +134,12 @@ void ensure_gamma_coords(const MeshT &mesh, DiffusionFormWorkspace &works workspace.gamma_coords_ready = true; } +/** + * \brief Ensure mixed coordinate/basis gamma caches have at least `n_coefficients` columns. + * \param mesh Input diffusion space. + * \param n_coefficients Required basis column count. + * \param workspace Scratch workspace to populate. + */ template void ensure_gamma_mixed(const MeshT &mesh, int n_coefficients, DiffusionFormWorkspace &workspace) { @@ -125,6 +165,13 @@ void ensure_gamma_mixed(const MeshT &mesh, int n_coefficients, workspace.gamma_mixed_cols = n1; } +/** + * \brief Compute all compound determinant vectors for k-form metric pullbacks. + * \param mesh Input diffusion space. + * \param k Exterior degree. + * \param workspace Scratch workspace. + * \return Precomputed compound determinant data. + */ template CompoundDeterminantData compute_compound_determinants( const MeshT &mesh, int k, DiffusionFormWorkspace &workspace) { @@ -152,6 +199,14 @@ CompoundDeterminantData compute_compound_determinants( return out; } +/** + * \brief Assemble Gram matrix for k-form basis coefficients. + * \param mesh Input diffusion space. + * \param k Exterior degree. + * \param n_coefficients Basis truncation size. + * \param workspace Scratch workspace. + * \return Symmetric Gram matrix for k-forms. + */ template Eigen::MatrixXf compute_kform_gram_matrix(const MeshT &mesh, int k, int n_coefficients, @@ -202,6 +257,13 @@ Eigen::MatrixXf compute_kform_gram_matrix(const MeshT &mesh, int k, return G; } +/** + * \brief Convenience overload for k-form Gram assembly. + * \param mesh Input diffusion space. + * \param k Exterior degree. + * \param n_coefficients Basis truncation size. + * \return Symmetric Gram matrix for k-forms. + */ template Eigen::MatrixXf compute_kform_gram_matrix(const MeshT &mesh, int k, int n_coefficients) { @@ -209,6 +271,14 @@ Eigen::MatrixXf compute_kform_gram_matrix(const MeshT &mesh, int k, return compute_kform_gram_matrix(mesh, k, n_coefficients, workspace); } +/** + * \brief Assemble weak exterior derivative matrix `d_k` in coefficient space. + * \param mesh Input diffusion space. + * \param k Exterior degree. + * \param n_coefficients Basis truncation size. + * \param workspace Scratch workspace. + * \return Weak derivative matrix. + */ template Eigen::MatrixXf compute_weak_exterior_derivative( const MeshT &mesh, int k, int n_coefficients, @@ -273,6 +343,13 @@ Eigen::MatrixXf compute_weak_exterior_derivative( return D; } +/** + * \brief Convenience overload for weak exterior derivative assembly. + * \param mesh Input diffusion space. + * \param k Exterior degree. + * \param n_coefficients Basis truncation size. + * \return Weak derivative matrix. + */ template Eigen::MatrixXf compute_weak_exterior_derivative(const MeshT &mesh, int k, int n_coefficients) { @@ -280,9 +357,23 @@ Eigen::MatrixXf compute_weak_exterior_derivative(const MeshT &mesh, int k, return compute_weak_exterior_derivative(mesh, k, n_coefficients, workspace); } +/** + * \brief Forward declaration of symmetric pseudo-inverse helper. + * \param matrix Symmetric matrix. + * \param rcond Relative conditioning threshold. + * \return Pseudo-inverse matrix. + */ inline Eigen::MatrixXf pseudo_inverse_symmetric(const Eigen::MatrixXf &matrix, float rcond); +/** + * \brief Assemble codifferential matrix `delta_k` from `d_{k-1}` and mass. + * \param mesh Input diffusion space. + * \param k Exterior degree. + * \param n_coefficients Basis truncation size. + * \param workspace Scratch workspace. + * \return Codifferential matrix. + */ template Eigen::MatrixXf compute_codifferential_matrix( const MeshT &mesh, int k, int n_coefficients, @@ -299,6 +390,13 @@ Eigen::MatrixXf compute_codifferential_matrix( return G_prev_inv * D_prev.transpose(); } +/** + * \brief Convenience overload for codifferential assembly. + * \param mesh Input diffusion space. + * \param k Exterior degree. + * \param n_coefficients Basis truncation size. + * \return Codifferential matrix. + */ template Eigen::MatrixXf compute_codifferential_matrix(const MeshT &mesh, int k, int n_coefficients) { @@ -306,6 +404,12 @@ Eigen::MatrixXf compute_codifferential_matrix(const MeshT &mesh, int k, return compute_codifferential_matrix(mesh, k, n_coefficients, workspace); } +/** + * \brief Numerically stable solve for small 2x2 systems with SVD fallback. + * \param A Input `2x2` matrix. + * \param b Right-hand side vector. + * \return Solution vector. + */ inline Eigen::Vector2f solve_stable_2x2(const Eigen::Matrix2f &A, const Eigen::Vector2f &b) { const float det = A(0, 0) * A(1, 1) - A(0, 1) * A(1, 0); @@ -328,6 +432,12 @@ inline Eigen::Vector2f solve_stable_2x2(const Eigen::Matrix2f &A, return svd.matrixV() * inv_s.asDiagonal() * svd.matrixU().transpose() * b; } +/** + * \brief Pseudo-inverse for symmetric matrices using eigenvalue thresholding. + * \param matrix Input matrix. + * \param rcond Relative threshold for inverting eigenvalues. + * \return Pseudo-inverse matrix. + */ inline Eigen::MatrixXf pseudo_inverse_symmetric(const Eigen::MatrixXf &matrix, float rcond = 1e-5f) { if (matrix.rows() == 0 || matrix.cols() == 0) { @@ -354,6 +464,14 @@ inline Eigen::MatrixXf pseudo_inverse_symmetric(const Eigen::MatrixXf &matrix, return evecs * inv.asDiagonal() * evecs.transpose(); } +/** + * \brief Assemble up-Laplacian contribution for k-forms. + * \param mesh Input diffusion space. + * \param k Exterior degree. + * \param n_coefficients Basis truncation size. + * \param workspace Scratch workspace. + * \return Up-Laplacian matrix. + */ template Eigen::MatrixXf compute_up_laplacian_matrix(const MeshT &mesh, int k, int n_coefficients, @@ -444,6 +562,13 @@ Eigen::MatrixXf compute_up_laplacian_matrix(const MeshT &mesh, int k, return L; } +/** + * \brief Convenience overload for up-Laplacian assembly. + * \param mesh Input diffusion space. + * \param k Exterior degree. + * \param n_coefficients Basis truncation size. + * \return Up-Laplacian matrix. + */ template Eigen::MatrixXf compute_up_laplacian_matrix(const MeshT &mesh, int k, int n_coefficients) { @@ -451,6 +576,14 @@ Eigen::MatrixXf compute_up_laplacian_matrix(const MeshT &mesh, int k, return compute_up_laplacian_matrix(mesh, k, n_coefficients, workspace); } +/** + * \brief Assemble down-Laplacian contribution for k-forms. + * \param mesh Input diffusion space. + * \param k Exterior degree. + * \param n_coefficients Basis truncation size. + * \param workspace Scratch workspace. + * \return Down-Laplacian matrix. + */ template Eigen::MatrixXf compute_down_laplacian_matrix( const MeshT &mesh, int k, int n_coefficients, @@ -470,6 +603,13 @@ Eigen::MatrixXf compute_down_laplacian_matrix( return D_prev * G_prev_inv * D_prev.transpose(); } +/** + * \brief Convenience overload for down-Laplacian assembly. + * \param mesh Input diffusion space. + * \param k Exterior degree. + * \param n_coefficients Basis truncation size. + * \return Down-Laplacian matrix. + */ template Eigen::MatrixXf compute_down_laplacian_matrix(const MeshT &mesh, int k, int n_coefficients) { @@ -477,12 +617,25 @@ Eigen::MatrixXf compute_down_laplacian_matrix(const MeshT &mesh, int k, return compute_down_laplacian_matrix(mesh, k, n_coefficients, workspace); } +/** + * \brief Sum up and down contributions into a Hodge Laplacian. + * \param up Up-Laplacian contribution. + * \param down Down-Laplacian contribution. + * \return Combined Hodge Laplacian. + */ inline Eigen::MatrixXf assemble_hodge_laplacian_matrix(const Eigen::MatrixXf &up, const Eigen::MatrixXf &down) { return up + down; } +/** + * \brief Solve generalized eigenproblem for form Laplacian and mass matrix. + * \param laplacian Form Laplacian matrix. + * \param mass_matrix Form mass matrix. + * \param rcond Relative threshold for mass-space regularization. + * \return Pair `(eigenvalues, eigenvectors)`. + */ inline std::pair compute_form_spectrum(const Eigen::MatrixXf &laplacian, const Eigen::MatrixXf &mass_matrix, @@ -523,6 +676,13 @@ compute_form_spectrum(const Eigen::MatrixXf &laplacian, return std::make_pair(solver.eigenvalues(), phi * solver.eigenvectors()); } +/** + * \brief Extract near-zero eigenvalue indices as harmonic modes. + * \param evals Eigenvalue vector. + * \param tolerance Absolute threshold used to classify harmonic modes. + * \param max_modes Maximum number of modes to return. + * \return Harmonic mode indices. + */ inline std::vector extract_harmonic_mode_indices(const Eigen::VectorXf &evals, float tolerance = 1e-3f, int max_modes = 3) { @@ -544,6 +704,14 @@ inline std::vector extract_harmonic_mode_indices(const Eigen::VectorXf &eva return out; } +/** + * \brief Expand flattened coefficient vector to pointwise k-form components. + * \param mesh Input diffusion space. + * \param coeffs Flattened coefficient vector. + * \param k Exterior degree. + * \param n_coefficients Basis truncation size. + * \return Pointwise matrix (`n_points x C(k)`). + */ template Eigen::MatrixXf coefficients_to_pointwise(const MeshT &mesh, const Eigen::VectorXf &coeffs, int k, @@ -560,6 +728,13 @@ Eigen::MatrixXf coefficients_to_pointwise(const MeshT &mesh, return pointwise; } +/** + * \brief Project pointwise components back to coefficient vectors. + * \param mesh Input diffusion space. + * \param pointwise Pointwise form component matrix. + * \param n_coefficients Basis truncation size. + * \return Flattened coefficient vector. + */ template Eigen::VectorXf project_pointwise_to_coefficients(const MeshT &mesh, const Eigen::MatrixXf &pointwise, @@ -590,6 +765,11 @@ Eigen::VectorXf project_pointwise_to_coefficients(const MeshT &mesh, return flattened; } +/** + * \brief Convert ambient 2-form components `(w01,w02,w12)` to dual 3D vectors. + * \param pointwise_2form Pointwise 2-form matrix. + * \return Per-point dual vector field. + */ inline std::vector pointwise_2form_to_dual_vectors(const Eigen::MatrixXf &pointwise_2form) { std::vector out(static_cast(pointwise_2form.rows()), diff --git a/include/igneous/ops/diffusion/geometry.hpp b/include/igneous/ops/diffusion/geometry.hpp index 97933e2..69d4de6 100644 --- a/include/igneous/ops/diffusion/geometry.hpp +++ b/include/igneous/ops/diffusion/geometry.hpp @@ -14,18 +14,31 @@ namespace igneous::ops::diffusion { +/// \brief Reusable buffers for diffusion geometry operators. template struct GeometryWorkspace { + /// \brief Coordinate vectors used in gamma evaluations. std::array coords; + /// \brief Pairwise `carre_du_champ` values for coordinates. std::array, 3> gamma_coords; + /// \brief Temporary weighted basis product storage. Eigen::VectorXf weights; + /// \brief General temporary gamma output vector. Eigen::VectorXf gamma_tmp; }; +/// \brief Ping-pong buffers for repeated Markov transitions. template struct DiffusionWorkspace { + /// \brief Source vector for the current transition step. Eigen::VectorXf ping; + /// \brief Destination vector for the current transition step. Eigen::VectorXf pong; }; +/** + * \brief Fill ambient coordinate vectors (prefers immersion coordinates when available). + * \param mesh Input diffusion space. + * \param coords Output coordinate vectors (`x,y,z`). + */ template void fill_coordinate_vectors(const MeshT &mesh, std::array &coords) { @@ -56,6 +69,11 @@ void fill_coordinate_vectors(const MeshT &mesh, } } +/** + * \brief Fill coordinate vectors from raw input geometry (`Space::x/y/z`). + * \param mesh Input space. + * \param coords Output coordinate vectors (`x,y,z`). + */ template void fill_data_coordinate_vectors(const MeshT &mesh, std::array &coords) { @@ -73,6 +91,16 @@ void fill_data_coordinate_vectors(const MeshT &mesh, } } +/** + * \brief Compute diffusion carré du champ: `Gamma(f, h)`. + * + * The implementation consumes CSR Markov rows and optional local bandwidths. + * \param mesh Input diffusion space. + * \param f First scalar field. + * \param h Second scalar field. + * \param bandwidth Legacy bandwidth parameter (unused in CSR path). + * \param gamma_out Output vector of pointwise gamma values. + */ template void carre_du_champ(const MeshT &mesh, Eigen::Ref f, Eigen::Ref h, [[maybe_unused]] float bandwidth, @@ -159,6 +187,12 @@ void carre_du_champ(const MeshT &mesh, Eigen::Ref f, } } +/** + * \brief Apply one Markov transition `output = P * input`. + * \param mesh Input diffusion space. + * \param input Input scalar vector. + * \param output Output scalar vector. + */ template void apply_markov_transition(const MeshT &mesh, Eigen::Ref input, @@ -223,6 +257,14 @@ void apply_markov_transition(const MeshT &mesh, } } +/** + * \brief Apply `steps` Markov transitions using a caller-provided workspace. + * \param mesh Input diffusion space. + * \param input Input scalar vector. + * \param steps Number of repeated transitions. + * \param output Output scalar vector. + * \param workspace Scratch buffers reused across calls. + */ template void apply_markov_transition_steps(const MeshT &mesh, Eigen::Ref input, @@ -287,6 +329,13 @@ void apply_markov_transition_steps(const MeshT &mesh, output = *src; } +/** + * \brief Convenience overload that allocates a temporary workspace. + * \param mesh Input diffusion space. + * \param input Input scalar vector. + * \param steps Number of repeated transitions. + * \param output Output scalar vector. + */ template void apply_markov_transition_steps(const MeshT &mesh, Eigen::Ref input, @@ -296,6 +345,13 @@ void apply_markov_transition_steps(const MeshT &mesh, apply_markov_transition_steps(mesh, input, steps, output, workspace); } +/** + * \brief Assemble the diffusion 1-form Gram matrix. + * \param mesh Input diffusion space. + * \param bandwidth Diffusion bandwidth parameter. + * \param workspace Scratch buffers reused across calls. + * \return Symmetric 1-form Gram matrix. + */ template Eigen::MatrixXf compute_1form_gram_matrix(const MeshT &mesh, float bandwidth, GeometryWorkspace &workspace) { @@ -349,6 +405,12 @@ Eigen::MatrixXf compute_1form_gram_matrix(const MeshT &mesh, float bandwidth, return G; } +/** + * \brief Convenience overload for 1-form Gram assembly. + * \param mesh Input diffusion space. + * \param bandwidth Diffusion bandwidth parameter. + * \return Symmetric 1-form Gram matrix. + */ template Eigen::MatrixXf compute_1form_gram_matrix(const MeshT &mesh, float bandwidth) { GeometryWorkspace workspace; diff --git a/include/igneous/ops/diffusion/hodge.hpp b/include/igneous/ops/diffusion/hodge.hpp index 18a2cb6..5329eb9 100644 --- a/include/igneous/ops/diffusion/hodge.hpp +++ b/include/igneous/ops/diffusion/hodge.hpp @@ -18,17 +18,33 @@ namespace igneous::ops::diffusion { +/// \brief Reusable buffers for diffusion-Hodge assembly. template struct HodgeWorkspace { + /// \brief Coordinate vectors used in gamma evaluations. std::array coords; - std::array gamma_x_phi_mat; // [3] each [n_verts x n0] - Eigen::MatrixXf weighted_u; // [n_verts x n0] - std::vector> gamma_x_phi; // [3][n0] - std::vector> gamma_phi_x; // [n0][3] + /// \brief `Gamma(x_a, phi_i)` matrix cache for each axis. + std::array gamma_x_phi_mat; + /// \brief Basis weighted by stationary measure. + Eigen::MatrixXf weighted_u; + /// \brief Reserved for mixed gamma layouts. + std::vector> gamma_x_phi; + /// \brief `Gamma(phi_i, x_a)` cache per mode and axis. + std::vector> gamma_phi_x; + /// \brief `Gamma(x_a, x_b)` caches. std::array, 3> gamma_xx; + /// \brief Temporary `Gamma(phi_i, phi_j)` buffer. Eigen::VectorXf gamma_phi_phi; + /// \brief Generic temporary weight vector. Eigen::VectorXf weight; }; +/** + * \brief Assemble weak exterior derivative matrix for 1-forms. + * \param mesh Input diffusion space. + * \param bandwidth Diffusion bandwidth parameter. + * \param workspace Scratch buffers. + * \return Weak derivative matrix. + */ template Eigen::MatrixXf compute_weak_exterior_derivative(const MeshT &mesh, float bandwidth, @@ -73,6 +89,12 @@ Eigen::MatrixXf compute_weak_exterior_derivative(const MeshT &mesh, return D; } +/** + * \brief Convenience overload for weak exterior derivative assembly. + * \param mesh Input diffusion space. + * \param bandwidth Diffusion bandwidth parameter. + * \return Weak derivative matrix. + */ template Eigen::MatrixXf compute_weak_exterior_derivative(const MeshT &mesh, float bandwidth) { @@ -80,6 +102,13 @@ Eigen::MatrixXf compute_weak_exterior_derivative(const MeshT &mesh, return compute_weak_exterior_derivative(mesh, bandwidth, workspace); } +/** + * \brief Assemble curl-energy term used in diffusion Hodge Laplacian. + * \param mesh Input diffusion space. + * \param bandwidth Diffusion bandwidth parameter. + * \param workspace Scratch buffers. + * \return Curl-energy matrix. + */ template Eigen::MatrixXf compute_curl_energy_matrix(const MeshT &mesh, float bandwidth, HodgeWorkspace &workspace) { @@ -149,18 +178,37 @@ Eigen::MatrixXf compute_curl_energy_matrix(const MeshT &mesh, float bandwidth, return E_up; } +/** + * \brief Convenience overload for curl-energy assembly. + * \param mesh Input diffusion space. + * \param bandwidth Diffusion bandwidth parameter. + * \return Curl-energy matrix. + */ template Eigen::MatrixXf compute_curl_energy_matrix(const MeshT &mesh, float bandwidth) { HodgeWorkspace workspace; return compute_curl_energy_matrix(mesh, bandwidth, workspace); } +/** + * \brief Combine down and up terms into a Hodge Laplacian. + * \param D_weak Weak derivative matrix. + * \param E_up Curl-energy matrix. + * \return Hodge Laplacian matrix. + */ inline Eigen::MatrixXf compute_hodge_laplacian_matrix( const Eigen::MatrixXf &D_weak, const Eigen::MatrixXf &E_up) { const Eigen::MatrixXf L_down = D_weak * D_weak.transpose(); return L_down + E_up; } +/** + * \brief Solve generalized Hodge eigenproblem `(L, G)` with regularization cutoff. + * \param laplacian Hodge Laplacian matrix. + * \param mass_matrix Gram/mass matrix. + * \param rcond Eigenvalue threshold for regularization. + * \return Pair `(eigenvalues, eigenvectors)`. + */ inline std::pair compute_hodge_spectrum(const Eigen::MatrixXf &laplacian, const Eigen::MatrixXf &mass_matrix, @@ -203,6 +251,19 @@ compute_hodge_spectrum(const Eigen::MatrixXf &laplacian, return std::make_pair(evals, evecs); } +/** + * \brief Compute scalar circular coordinates from a harmonic 1-form mode. + * + * This solves a generalized eigenproblem for an advection-diffusion operator + * built from `alpha_coeffs`. + * \param mesh Input diffusion space. + * \param alpha_coeffs Harmonic 1-form coefficients. + * \param bandwidth Diffusion bandwidth parameter. + * \param lambda Diffusion regularization weight. + * \param positive_imag_mode Which positive-imaginary mode to choose. + * \param selected_eval Optional output for selected complex eigenvalue. + * \return Angle field in `[0, 2*pi)`. + */ template Eigen::VectorXf compute_circular_coordinates(const MeshT &mesh, const Eigen::VectorXf &alpha_coeffs, diff --git a/include/igneous/ops/diffusion/products.hpp b/include/igneous/ops/diffusion/products.hpp index 6eb1f55..2212627 100644 --- a/include/igneous/ops/diffusion/products.hpp +++ b/include/igneous/ops/diffusion/products.hpp @@ -9,6 +9,17 @@ namespace igneous::ops::diffusion { +/** + * \brief Compute coefficients of `alpha ^ beta` in diffusion-form basis. + * \param mesh Input diffusion space. + * \param alpha_coeffs Left operand coefficients. + * \param k1 Left exterior degree. + * \param beta_coeffs Right operand coefficients. + * \param k2 Right exterior degree. + * \param n_coefficients Basis truncation size. + * \param workspace Scratch buffers. + * \return Flattened coefficient vector of the wedge product. + */ template Eigen::VectorXf compute_wedge_product_coeffs( const MeshT &mesh, const Eigen::VectorXf &alpha_coeffs, int k1, @@ -56,6 +67,16 @@ Eigen::VectorXf compute_wedge_product_coeffs( return project_pointwise_to_coefficients(mesh, wedge_pw, n1); } +/** + * \brief Convenience overload for wedge coefficient assembly. + * \param mesh Input diffusion space. + * \param alpha_coeffs Left operand coefficients. + * \param k1 Left exterior degree. + * \param beta_coeffs Right operand coefficients. + * \param k2 Right exterior degree. + * \param n_coefficients Basis truncation size. + * \return Flattened coefficient vector of the wedge product. + */ template Eigen::VectorXf compute_wedge_product_coeffs(const MeshT &mesh, const Eigen::VectorXf &alpha_coeffs, @@ -68,6 +89,18 @@ Eigen::VectorXf compute_wedge_product_coeffs(const MeshT &mesh, n_coefficients, workspace); } +/** + * \brief Build the linear operator matrix for left wedge multiplication. + * + * Returns `W` such that `W * beta_coeffs` approximates `alpha ^ beta`. + * \param mesh Input diffusion space. + * \param alpha_coeffs Fixed left operand coefficients. + * \param k_left Left exterior degree. + * \param k_right Right exterior degree. + * \param n_coefficients Basis truncation size. + * \param workspace Scratch buffers. + * \return Dense linear operator matrix. + */ template Eigen::MatrixXf compute_wedge_operator_matrix( const MeshT &mesh, const Eigen::VectorXf &alpha_coeffs, int k_left, @@ -93,6 +126,15 @@ Eigen::MatrixXf compute_wedge_operator_matrix( return op; } +/** + * \brief Convenience overload for wedge operator assembly. + * \param mesh Input diffusion space. + * \param alpha_coeffs Fixed left operand coefficients. + * \param k_left Left exterior degree. + * \param k_right Right exterior degree. + * \param n_coefficients Basis truncation size. + * \return Dense linear operator matrix. + */ template Eigen::MatrixXf compute_wedge_operator_matrix(const MeshT &mesh, const Eigen::VectorXf &alpha_coeffs, diff --git a/include/igneous/ops/diffusion/spectral.hpp b/include/igneous/ops/diffusion/spectral.hpp index 5f00d40..641445e 100644 --- a/include/igneous/ops/diffusion/spectral.hpp +++ b/include/igneous/ops/diffusion/spectral.hpp @@ -12,19 +12,32 @@ namespace igneous::ops::diffusion { +/** + * \brief Spectra-compatible matrix product wrapper over Markov CSR data. + * + * Implements the matrix-operation interface expected by Spectra. + */ class MarkovCsrMatProd { public: using Scalar = float; + /// \brief Construct operator view over CSR arrays. MarkovCsrMatProd(const std::vector &row_offsets, const std::vector &col_indices, const std::vector &values, int n) : row_offsets_(row_offsets), col_indices_(col_indices), values_(values), n_(n) {} + /// \return Matrix row count. [[nodiscard]] int rows() const { return n_; } + /// \return Matrix column count. [[nodiscard]] int cols() const { return n_; } + /** + * \brief Apply matrix-vector product. + * \param x_in Input vector. + * \param y_out Output vector. + */ void perform_op(const Scalar *x_in, Scalar *y_out) const { core::parallel_for_index( 0, n_, @@ -52,16 +65,22 @@ class MarkovCsrMatProd { } private: + /// \brief CSR row offsets. const std::vector &row_offsets_; + /// \brief CSR column indices. const std::vector &col_indices_; + /// \brief CSR values. const std::vector &values_; + /// \brief Matrix dimension. int n_ = 0; }; +/// \brief Spectra operator for the symmetrized Markov transform. class MarkovSymmetricCsrMatProd { public: using Scalar = float; + /// \brief Construct normalized symmetric Markov operator. MarkovSymmetricCsrMatProd(const std::vector &row_offsets, const std::vector &col_indices, const std::vector &values, @@ -70,9 +89,16 @@ class MarkovSymmetricCsrMatProd { : row_offsets_(row_offsets), col_indices_(col_indices), values_(values), sqrt_mu_(sqrt_mu), inv_sqrt_mu_(inv_sqrt_mu), n_(n) {} + /// \return Matrix row count. [[nodiscard]] int rows() const { return n_; } + /// \return Matrix column count. [[nodiscard]] int cols() const { return n_; } + /** + * \brief Apply matrix-vector product. + * \param x_in Input vector. + * \param y_out Output vector. + */ void perform_op(const Scalar *x_in, Scalar *y_out) const { const float *sqrt_mu_data = sqrt_mu_.data(); const float *inv_sqrt_mu_data = inv_sqrt_mu_.data(); @@ -108,27 +134,42 @@ class MarkovSymmetricCsrMatProd { } private: + /// \brief CSR row offsets. const std::vector &row_offsets_; + /// \brief CSR column indices. const std::vector &col_indices_; + /// \brief CSR values. const std::vector &values_; + /// \brief `sqrt(mu)` scaling. const Eigen::VectorXf &sqrt_mu_; + /// \brief `1/sqrt(mu)` scaling. const Eigen::VectorXf &inv_sqrt_mu_; + /// \brief Matrix dimension. int n_ = 0; }; +/// \brief Spectra operator over the symmetric kernel CSR matrix. class SymmetricKernelCsrMatProd { public: using Scalar = float; + /// \brief Construct operator view over symmetric-kernel CSR arrays. SymmetricKernelCsrMatProd(const std::vector &row_offsets, const std::vector &col_indices, const std::vector &values, int n) : row_offsets_(row_offsets), col_indices_(col_indices), values_(values), n_(n) {} + /// \return Matrix row count. [[nodiscard]] int rows() const { return n_; } + /// \return Matrix column count. [[nodiscard]] int cols() const { return n_; } + /** + * \brief Apply matrix-vector product. + * \param x_in Input vector. + * \param y_out Output vector. + */ void perform_op(const Scalar *x_in, Scalar *y_out) const { core::parallel_for_index( 0, n_, @@ -156,16 +197,22 @@ class SymmetricKernelCsrMatProd { } private: + /// \brief CSR row offsets. const std::vector &row_offsets_; + /// \brief CSR column indices. const std::vector &col_indices_; + /// \brief CSR values. const std::vector &values_; + /// \brief Matrix dimension. int n_ = 0; }; +/// \brief Spectra operator for normalized symmetric-kernel eigensolve. class NormalizedSymmetricKernelCsrMatProd { public: using Scalar = float; + /// \brief Construct normalized symmetric-kernel operator. NormalizedSymmetricKernelCsrMatProd(const std::vector &row_offsets, const std::vector &col_indices, const std::vector &values, @@ -174,9 +221,16 @@ class NormalizedSymmetricKernelCsrMatProd { : row_offsets_(row_offsets), col_indices_(col_indices), values_(values), inv_sqrt_rows_(inv_sqrt_rows), n_(n) {} + /// \return Matrix row count. [[nodiscard]] int rows() const { return n_; } + /// \return Matrix column count. [[nodiscard]] int cols() const { return n_; } + /** + * \brief Apply matrix-vector product. + * \param x_in Input vector. + * \param y_out Output vector. + */ void perform_op(const Scalar *x_in, Scalar *y_out) const { const float *inv_sqrt_data = inv_sqrt_rows_.data(); core::parallel_for_index( @@ -210,14 +264,23 @@ class NormalizedSymmetricKernelCsrMatProd { } private: + /// \brief CSR row offsets. const std::vector &row_offsets_; + /// \brief CSR column indices. const std::vector &col_indices_; + /// \brief CSR values. const std::vector &values_; + /// \brief Inverse square-root row scaling. const Eigen::VectorXf &inv_sqrt_rows_; + /// \brief Matrix dimension. int n_ = 0; }; -// Computes the first k eigenvectors of the Markov Chain P. +/** + * \brief Compute a diffusion eigenbasis from Markov CSR data. + * \param mesh Input diffusion space. `mesh.structure.eigen_basis` is overwritten. + * \param n_eigenvectors Number of eigenvectors requested. + */ template void compute_eigenbasis(MeshT &mesh, int n_eigenvectors) { const bool verbose = std::getenv("IGNEOUS_BENCH_MODE") == nullptr; diff --git a/include/igneous/ops/transform.hpp b/include/igneous/ops/transform.hpp index 458dd97..4e5a53f 100644 --- a/include/igneous/ops/transform.hpp +++ b/include/igneous/ops/transform.hpp @@ -7,6 +7,12 @@ namespace igneous::ops { +/** + * \brief Normalize geometry to a unit box centered at the origin. + * + * The largest axis extent is mapped to length `2.0`. + * \param mesh Input/output space geometry. + */ template void normalize(data::Space &mesh) { const size_t n_verts = mesh.num_points(); if (n_verts == 0) { diff --git a/notes/structure_refactor/journal.md b/notes/structure_refactor/journal.md index fdc6483..be10d26 100644 --- a/notes/structure_refactor/journal.md +++ b/notes/structure_refactor/journal.md @@ -88,3 +88,35 @@ - `IGNEOUS_BENCH_MODE=1 ./build/bench_geometry` - `IGNEOUS_BENCH_MODE=1 ./build/bench_dod --benchmark_min_time=0.02s --benchmark_repetitions=1 --benchmark_report_aggregates_only=true` - `IGNEOUS_BENCH_MODE=1 ./build/bench_pipelines --benchmark_min_time=0.02s --benchmark_repetitions=1 --benchmark_report_aggregates_only=true` + +## Entry 0006 +- Timestamp: 2026-02-15 +- Structural Difference Targeted: Improve API discoverability with inline C++ docs and generated reference docs. +- Documentation Additions: + - Added Doxygen comments across `include/igneous` headers, including internal/private state fields for key workspace and runtime classes. + - Added docs generation tooling: + - `docs/Doxyfile.in` + - `docs/mainpage.md` + - CMake `docs` target (when Doxygen is available) + - `Makefile` `docs` shortcut + - Added README docs section with generation instructions and output path. +- Decisions: + - Use Doxygen as the canonical C++ API doc generator (closest to Rust `cargo doc` workflow). + - Keep extraction of private members enabled in docs config (`EXTRACT_PRIVATE=YES`) to match maintenance needs. + +## Entry 0007 +- Timestamp: 2026-02-15 +- Structural Difference Targeted: Add docs quality checks to CI and deploy generated docs. +- CI Additions: + - Added `docs` job to `.github/workflows/ci.yml`: + - installs Doxygen + Graphviz + - configures CMake with `IGNEOUS_BUILD_DOCS=ON` + - builds `docs` target + - verifies `build-docs/docs/html/index.html` exists +- Deployment Additions: + - Added `.github/workflows/docs.yml`: + - triggers on `push` to `main` and manual dispatch + - builds docs artifact + - deploys to GitHub Pages using `actions/deploy-pages` +- Documentation: + - Added README note for docs deployment URL pattern. From d373dcba89f488125d971234ead97c1062da4ba8 Mon Sep 17 00:00:00 2001 From: Colin Roberts Date: Sun, 15 Feb 2026 08:31:42 -0700 Subject: [PATCH 3/9] chore(dev): add clang lint/format tooling and CI jobs --- .clang-format | 19 ++++++++ .clang-tidy | 44 +++++++++++++++++++ .github/workflows/ci.yml | 63 ++++++++++++++++++++++++++ Makefile | 14 +++++- README.md | 23 ++++++++++ notes/structure_refactor/journal.md | 22 ++++++++++ scripts/dev/format.sh | 61 ++++++++++++++++++++++++++ scripts/dev/lint.sh | 68 +++++++++++++++++++++++++++++ 8 files changed, 313 insertions(+), 1 deletion(-) create mode 100644 .clang-format create mode 100644 .clang-tidy create mode 100755 scripts/dev/format.sh create mode 100755 scripts/dev/lint.sh diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..1c4695a --- /dev/null +++ b/.clang-format @@ -0,0 +1,19 @@ +--- +BasedOnStyle: LLVM +Standard: Latest +IndentWidth: 2 +TabWidth: 2 +UseTab: Never +ColumnLimit: 100 +ContinuationIndentWidth: 4 +PointerAlignment: Left +ReferenceAlignment: Left +BreakBeforeBraces: Attach +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +NamespaceIndentation: None +SortIncludes: true +IncludeBlocks: Preserve +ReflowComments: false +... diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..fa84169 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,44 @@ +--- +Checks: > + -*, + bugprone-branch-clone, + bugprone-copy-constructor-init, + bugprone-dangling-handle, + bugprone-fold-init-type, + bugprone-forward-declaration-namespace, + bugprone-inaccurate-erase, + bugprone-infinite-loop, + bugprone-macro-parentheses, + bugprone-misplaced-operator-in-strlen-in-alloc, + bugprone-move-forwarding-reference, + bugprone-multiple-statement-macro, + bugprone-sizeof-container, + bugprone-sizeof-expression, + bugprone-string-constructor, + bugprone-string-integer-assignment, + bugprone-suspicious-enum-usage, + bugprone-suspicious-include, + bugprone-suspicious-memory-comparison, + bugprone-suspicious-memset-usage, + bugprone-unchecked-optional-access, + bugprone-undefined-memory-manipulation, + bugprone-use-after-move, + performance-for-range-copy, + performance-inefficient-string-concatenation, + performance-inefficient-vector-operation, + performance-move-const-arg, + performance-no-automatic-move, + modernize-use-nullptr, + modernize-use-override, + modernize-use-using, + readability-const-return-type, + readability-duplicate-include, + readability-redundant-control-flow, + readability-simplify-boolean-expr +WarningsAsErrors: "" +HeaderFilterRegex: "^(.*(/include/igneous/|/src/).*)$" +FormatStyle: file +CheckOptions: + - key: modernize-use-nullptr.NullMacros + value: "NULL" +... diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a2039c..68f62f7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -172,6 +172,69 @@ jobs: name: compile-commands path: build-debug/compile_commands.json + lint: + name: Lint (clang-tidy) + runs-on: ubuntu-latest + + steps: + - name: Checkout source + uses: actions/checkout@v4 + + - name: Install lint tools + shell: bash + run: | + sudo apt-get update + sudo apt-get install -y clang-tidy ripgrep + + - name: Checkout vcpkg + uses: actions/checkout@v4 + with: + repository: microsoft/vcpkg + path: vcpkg + + - name: Restore vcpkg cache + uses: actions/cache@v4 + with: + path: | + ~/.cache/vcpkg/archives + ${{ github.workspace }}/vcpkg/downloads + key: vcpkg-${{ runner.os }}-x64-linux-${{ hashFiles('vcpkg.json', 'CMakeLists.txt') }} + + - name: Bootstrap vcpkg + shell: bash + run: ./vcpkg/bootstrap-vcpkg.sh + + - name: Configure (Lint) + shell: bash + run: >- + cmake -S . -B build-lint -G Ninja + -DCMAKE_BUILD_TYPE=Debug + -DCMAKE_TOOLCHAIN_FILE=${{ github.workspace }}/vcpkg/scripts/buildsystems/vcpkg.cmake + -DVCPKG_TARGET_TRIPLET=x64-linux + -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + + - name: Run clang-tidy (all) + shell: bash + run: IGNEOUS_LINT_SCOPE=all ./scripts/dev/lint.sh build-lint + + format-check: + name: Format Check (clang-format) + runs-on: ubuntu-latest + + steps: + - name: Checkout source + uses: actions/checkout@v4 + + - name: Install format tools + shell: bash + run: | + sudo apt-get update + sudo apt-get install -y clang-format ripgrep + + - name: Run clang-format check + shell: bash + run: ./scripts/dev/format.sh --check + docs: name: API Docs (doxygen) runs-on: ubuntu-latest diff --git a/Makefile b/Makefile index 7ecde34..fa1424b 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all debug release build docs clean test test-all test-algebra test-structure test-ops bench bench-memory bench-geometry bench-dod bench-deep run-mesh run-diffusion run-spectral run-hodge +.PHONY: all debug release build docs lint lint-all format format-check clean test test-all test-algebra test-structure test-ops bench bench-memory bench-geometry bench-dod bench-deep run-mesh run-diffusion run-spectral run-hodge all: build @@ -14,6 +14,18 @@ build: docs: debug cmake --build build --target docs +lint: debug + ./scripts/dev/lint.sh build + +lint-all: debug + IGNEOUS_LINT_SCOPE=all ./scripts/dev/lint.sh build + +format: + ./scripts/dev/format.sh apply + +format-check: + ./scripts/dev/format.sh --check + clean: rm -rf build diff --git a/README.md b/README.md index 0e526ad..0e8e89e 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,29 @@ Deployment: - GitHub Actions workflow `.github/workflows/docs.yml` builds and deploys docs to GitHub Pages on `main`. - Published URL pattern: `https://.github.io/igneous/` +## Lint and Format + +This repo uses `clang-tidy` (lint) and `clang-format` (formatter) with config files at: + +- `.clang-tidy` +- `.clang-format` + +Run locally: + +```bash +make lint +make lint-all +make format +make format-check +``` + +Notes: + +- `make lint` requires `build/compile_commands.json` and will run `cmake --preset default-local` via `make debug` first. +- `make lint` runs a fast/default lint pass on C++ translation units under `src/`. +- `make lint-all` extends lint coverage to `tests/` and `benches/` as well. +- Tool binaries searched in `PATH`: `clang-tidy` and `clang-format` (version-suffixed variants are supported). + ## Run Examples ```bash diff --git a/notes/structure_refactor/journal.md b/notes/structure_refactor/journal.md index be10d26..a29ee71 100644 --- a/notes/structure_refactor/journal.md +++ b/notes/structure_refactor/journal.md @@ -120,3 +120,25 @@ - deploys to GitHub Pages using `actions/deploy-pages` - Documentation: - Added README note for docs deployment URL pattern. + +## Entry 0008 +- Timestamp: 2026-02-15 +- Structural Difference Targeted: Add pedantic static analysis and formatting workflow for maintainability. +- Tooling Additions: + - Added `.clang-tidy` with a practical pedantic checks profile (`bugprone`, `performance`, selected `modernize/readability`). + - Added `.clang-format` as repo-wide formatter policy. + - Added helper scripts: + - `scripts/dev/lint.sh` + - `scripts/dev/format.sh` + - Added `Makefile` targets: + - `make lint` + - `make lint-all` + - `make format` + - `make format-check` +- Documentation: + - Added lint/format usage section to `README.md`. +- Verification: + - `make lint` -> pass; reports actionable warnings from current source. + - `make lint-all` -> pass; reports additional warnings across tests/benches. + - `make format-check` -> fail on existing formatting drift, as expected before first repo-wide `make format`. + - `ctest --test-dir build --output-on-failure` -> `14/14` passed after tooling integration. diff --git a/scripts/dev/format.sh b/scripts/dev/format.sh new file mode 100755 index 0000000..be8e2eb --- /dev/null +++ b/scripts/dev/format.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +MODE="${1:-apply}" + +pick_tool() { + local tool + for tool in "$@"; do + if command -v "${tool}" >/dev/null 2>&1; then + command -v "${tool}" + return 0 + fi + done + return 1 +} + +CLANG_FORMAT_BIN="$(pick_tool clang-format clang-format-19 clang-format-18 clang-format-17)" || { + echo "error: clang-format not found in PATH." + exit 1 +} + +mapfile -t SOURCE_FILES < <( + cd "${ROOT_DIR}" && + rg --files include src tests benches | rg '\.(hpp|h|hh|cpp|cc|cxx|mm)$' +) + +if [[ ${#SOURCE_FILES[@]} -eq 0 ]]; then + echo "No C++ source files found under include/src/tests/benches." + exit 0 +fi + +cd "${ROOT_DIR}" +if ! "${CLANG_FORMAT_BIN}" --style=file --dump-config >/dev/null 2>&1; then + echo "error: unable to parse .clang-format." + exit 1 +fi + +if [[ "${MODE}" == "--check" ]]; then + status=0 + declare -a NEEDS_FORMAT=() + for file in "${SOURCE_FILES[@]}"; do + if ! diff -q "${file}" <("${CLANG_FORMAT_BIN}" "${file}") >/dev/null; then + NEEDS_FORMAT+=("${file}") + status=1 + fi + done + if [[ ${status} -ne 0 ]]; then + echo "The following files are not clang-formatted:" + printf ' %s\n' "${NEEDS_FORMAT[@]}" + echo "Run 'make format' to apply formatting." + fi + exit "${status}" +fi + +if [[ "${MODE}" != "apply" ]]; then + echo "error: unsupported mode '${MODE}'. Use 'apply' or '--check'." + exit 1 +fi + +"${CLANG_FORMAT_BIN}" -i "${SOURCE_FILES[@]}" diff --git a/scripts/dev/lint.sh b/scripts/dev/lint.sh new file mode 100755 index 0000000..7500aac --- /dev/null +++ b/scripts/dev/lint.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +BUILD_DIR="${1:-${ROOT_DIR}/build}" +LINT_SCOPE="${IGNEOUS_LINT_SCOPE:-src}" + +pick_tool() { + local tool + for tool in "$@"; do + if command -v "${tool}" >/dev/null 2>&1; then + command -v "${tool}" + return 0 + fi + done + return 1 +} + +if [[ ! -f "${BUILD_DIR}/compile_commands.json" ]]; then + echo "error: ${BUILD_DIR}/compile_commands.json not found." + echo "hint: run 'cmake --preset default-local' first." + exit 1 +fi + +CLANG_TIDY_BIN="$(pick_tool clang-tidy clang-tidy-19 clang-tidy-18 clang-tidy-17)" || { + echo "error: clang-tidy not found in PATH." + exit 1 +} + +SEARCH_DIRS=() +case "${LINT_SCOPE}" in + src) + SEARCH_DIRS=(src) + ;; + all) + SEARCH_DIRS=(src tests benches) + ;; + *) + echo "error: unsupported IGNEOUS_LINT_SCOPE='${LINT_SCOPE}'. Use 'src' or 'all'." + exit 1 + ;; +esac + +mapfile -t TRANSLATION_UNITS < <( + cd "${ROOT_DIR}" && + rg --files "${SEARCH_DIRS[@]}" | rg '\.(cpp|cc|cxx)$' +) + +if [[ ${#TRANSLATION_UNITS[@]} -eq 0 ]]; then + echo "No translation units found under src/tests/benches." + exit 0 +fi + +cd "${ROOT_DIR}" +status=0 +EXTRA_ARGS=() +if [[ "$(uname -s)" == "Darwin" ]]; then + SDKROOT="$(xcrun --show-sdk-path)" + EXTRA_ARGS+=(--extra-arg=-isysroot --extra-arg="${SDKROOT}") +fi + +for file in "${TRANSLATION_UNITS[@]}"; do + if ! "${CLANG_TIDY_BIN}" -quiet "${EXTRA_ARGS[@]}" -p "${BUILD_DIR}" "${file}" 2>&1 | + sed '/^[0-9][0-9]* warnings generated\.$/d'; then + status=1 + fi +done +exit "${status}" From 7663d09f1f6801fdae8cd02ecb1a5d4083f05bef Mon Sep 17 00:00:00 2001 From: Colin Roberts Date: Sun, 15 Feb 2026 08:38:34 -0700 Subject: [PATCH 4/9] chore(lint): add header include-cleaner pass and fix basis includes --- README.md | 2 +- include/igneous/ops/diffusion/basis.hpp | 6 +----- notes/structure_refactor/journal.md | 9 +++++++++ scripts/dev/lint.sh | 14 ++++++++++++++ 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 0e8e89e..b247b80 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ make format-check Notes: - `make lint` requires `build/compile_commands.json` and will run `cmake --preset default-local` via `make debug` first. -- `make lint` runs a fast/default lint pass on C++ translation units under `src/`. +- `make lint` runs a default lint pass on C++ translation units under `src/` and also checks headers under `include/igneous/` for unused/incorrect includes via `misc-include-cleaner`. - `make lint-all` extends lint coverage to `tests/` and `benches/` as well. - Tool binaries searched in `PATH`: `clang-tidy` and `clang-format` (version-suffixed variants are supported). diff --git a/include/igneous/ops/diffusion/basis.hpp b/include/igneous/ops/diffusion/basis.hpp index 652b341..3a29981 100644 --- a/include/igneous/ops/diffusion/basis.hpp +++ b/include/igneous/ops/diffusion/basis.hpp @@ -1,13 +1,9 @@ #pragma once #include -#include -#include -#include +#include #include #include -#include -#include #include namespace igneous::ops::diffusion { diff --git a/notes/structure_refactor/journal.md b/notes/structure_refactor/journal.md index a29ee71..e7bff9a 100644 --- a/notes/structure_refactor/journal.md +++ b/notes/structure_refactor/journal.md @@ -142,3 +142,12 @@ - `make lint-all` -> pass; reports additional warnings across tests/benches. - `make format-check` -> fail on existing formatting drift, as expected before first repo-wide `make format`. - `ctest --test-dir build --output-on-failure` -> `14/14` passed after tooling integration. + +## Entry 0009 +- Timestamp: 2026-02-15 +- Structural Difference Targeted: Ensure lint catches unused direct includes in headers. +- Lint Fixes: + - Updated `scripts/dev/lint.sh` to run a dedicated header pass with `misc-include-cleaner` over `include/igneous/**`. + - Added `std::cstddef` include and removed unused includes in `include/igneous/ops/diffusion/basis.hpp`. +- Verification: + - `make lint` now surfaces include-cleaner diagnostics from headers (including `basis.hpp`) and reports missing-direct-include issues. diff --git a/scripts/dev/lint.sh b/scripts/dev/lint.sh index 7500aac..9985904 100755 --- a/scripts/dev/lint.sh +++ b/scripts/dev/lint.sh @@ -51,6 +51,11 @@ if [[ ${#TRANSLATION_UNITS[@]} -eq 0 ]]; then exit 0 fi +mapfile -t HEADER_FILES < <( + cd "${ROOT_DIR}" && + rg --files include/igneous | rg '\.(hpp|h|hh)$' +) + cd "${ROOT_DIR}" status=0 EXTRA_ARGS=() @@ -65,4 +70,13 @@ for file in "${TRANSLATION_UNITS[@]}"; do status=1 fi done + +for header in "${HEADER_FILES[@]}"; do + if ! "${CLANG_TIDY_BIN}" -quiet "${EXTRA_ARGS[@]}" -checks='misc-include-cleaner' \ + -p "${BUILD_DIR}" "${header}" 2>&1 | + sed '/^[0-9][0-9]* warnings generated\.$/d'; then + status=1 + fi +done + exit "${status}" From 0277b961e0ba0ef6d020a08dacc6c734ed5baa6f Mon Sep 17 00:00:00 2001 From: Colin Roberts Date: Sun, 15 Feb 2026 08:39:12 -0700 Subject: [PATCH 5/9] style: apply repo-wide clang-format baseline --- benches/bench_dod.cpp | 38 ++-- benches/bench_geometry.cpp | 20 +- benches/bench_memory.cpp | 16 +- benches/bench_pipelines.cpp | 42 ++-- include/igneous/core/algebra.hpp | 80 ++++--- include/igneous/core/blades.hpp | 32 +-- include/igneous/core/gpu.hpp | 32 ++- include/igneous/core/memory.hpp | 27 +-- include/igneous/core/parallel.hpp | 31 ++- include/igneous/data/space.hpp | 38 +++- include/igneous/data/structure.hpp | 8 +- .../data/structures/diffusion_geometry.hpp | 86 ++++---- .../structures/discrete_exterior_calculus.hpp | 2 +- include/igneous/igneous.hpp | 2 +- include/igneous/io/exporter.hpp | 58 +++-- include/igneous/io/importer.hpp | 6 +- include/igneous/ops/dec/curvature.hpp | 13 +- include/igneous/ops/dec/flow.hpp | 15 +- include/igneous/ops/diffusion/basis.hpp | 19 +- include/igneous/ops/diffusion/forms.hpp | 179 +++++++--------- include/igneous/ops/diffusion/geometry.hpp | 116 +++++----- include/igneous/ops/diffusion/hodge.hpp | 67 +++--- include/igneous/ops/diffusion/products.hpp | 55 ++--- include/igneous/ops/diffusion/spectral.hpp | 198 +++++++++--------- include/igneous/ops/transform.hpp | 2 +- notes/structure_refactor/journal.md | 10 + src/core/gpu_stub.cpp | 22 +- src/core/metal_diffusion.mm | 190 +++++++---------- src/main_diffusion.cpp | 11 +- src/main_diffusion_geometry.cpp | 154 ++++++-------- src/main_hodge.cpp | 123 ++++------- src/main_mesh.cpp | 2 +- src/main_point.cpp | 2 +- src/main_spectral.cpp | 8 +- tests/test_io_meshes.cpp | 6 +- tests/test_ops_curvature_flow.cpp | 9 +- tests/test_ops_diffusion_basis.cpp | 2 +- tests/test_ops_diffusion_forms.cpp | 28 ++- tests/test_ops_diffusion_wedge.cpp | 21 +- tests/test_ops_hodge.cpp | 16 +- tests/test_ops_spectral_geometry.cpp | 3 +- tests/test_structure_dec.cpp | 3 +- tests/test_structure_diffusion_geometry.cpp | 23 +- 43 files changed, 815 insertions(+), 1000 deletions(-) diff --git a/benches/bench_dod.cpp b/benches/bench_dod.cpp index 41eafcc..18aceb1 100644 --- a/benches/bench_dod.cpp +++ b/benches/bench_dod.cpp @@ -20,7 +20,9 @@ using DiffusionMesh = igneous::data::Space; namespace { struct BenchEnvSetup { - BenchEnvSetup() { setenv("IGNEOUS_BENCH_MODE", "1", 1); } + BenchEnvSetup() { + setenv("IGNEOUS_BENCH_MODE", "1", 1); + } } kBenchEnvSetup; } // namespace @@ -82,12 +84,11 @@ static DiffusionMesh make_diffusion_cloud(size_t n_points) { mesh.push_point({x, y, z}); } - mesh.structure.build({mesh.x_span(), mesh.y_span(), - mesh.z_span(), 32}); + mesh.structure.build({mesh.x_span(), mesh.y_span(), mesh.z_span(), 32}); return mesh; } -static void bench_mesh_structure_build(benchmark::State &state) { +static void bench_mesh_structure_build(benchmark::State& state) { SurfaceMesh mesh = make_grid_mesh(static_cast(state.range(0))); for (auto _ : state) { mesh.structure.vertex_face_offsets.clear(); @@ -99,7 +100,7 @@ static void bench_mesh_structure_build(benchmark::State &state) { } } -static void bench_curvature_kernel(benchmark::State &state) { +static void bench_curvature_kernel(benchmark::State& state) { SurfaceMesh mesh = make_grid_mesh(static_cast(state.range(0))); std::vector H; std::vector K; @@ -112,7 +113,7 @@ static void bench_curvature_kernel(benchmark::State &state) { } } -static void bench_flow_kernel(benchmark::State &state) { +static void bench_flow_kernel(benchmark::State& state) { SurfaceMesh mesh = make_grid_mesh(static_cast(state.range(0))); igneous::ops::dec::FlowWorkspace ws; @@ -122,21 +123,19 @@ static void bench_flow_kernel(benchmark::State &state) { } } -static void bench_diffusion_build(benchmark::State &state) { +static void bench_diffusion_build(benchmark::State& state) { DiffusionMesh mesh = make_diffusion_cloud(static_cast(state.range(0))); for (auto _ : state) { - mesh.structure.build({mesh.x_span(), mesh.y_span(), - mesh.z_span(), 32}); + mesh.structure.build({mesh.x_span(), mesh.y_span(), mesh.z_span(), 32}); benchmark::DoNotOptimize(mesh.structure.markov_values.size()); } } -static void bench_markov_step(benchmark::State &state) { +static void bench_markov_step(benchmark::State& state) { DiffusionMesh mesh = make_diffusion_cloud(static_cast(state.range(0))); Eigen::VectorXf u = Eigen::VectorXf::Ones(static_cast(mesh.num_points())); - Eigen::VectorXf u_next = - Eigen::VectorXf::Zero(static_cast(mesh.num_points())); + Eigen::VectorXf u_next = Eigen::VectorXf::Zero(static_cast(mesh.num_points())); for (auto _ : state) { igneous::ops::diffusion::apply_markov_transition(mesh, u, u_next); @@ -145,13 +144,12 @@ static void bench_markov_step(benchmark::State &state) { } } -static void bench_markov_multi_step(benchmark::State &state) { +static void bench_markov_multi_step(benchmark::State& state) { const size_t n_points = static_cast(state.range(0)); const int steps = static_cast(state.range(1)); DiffusionMesh mesh = make_diffusion_cloud(n_points); Eigen::VectorXf u = Eigen::VectorXf::Ones(static_cast(mesh.num_points())); - Eigen::VectorXf u_next = - Eigen::VectorXf::Zero(static_cast(mesh.num_points())); + Eigen::VectorXf u_next = Eigen::VectorXf::Zero(static_cast(mesh.num_points())); igneous::ops::diffusion::DiffusionWorkspace ws; for (auto _ : state) { @@ -161,7 +159,7 @@ static void bench_markov_multi_step(benchmark::State &state) { } } -static void bench_eigenbasis(benchmark::State &state) { +static void bench_eigenbasis(benchmark::State& state) { DiffusionMesh mesh = make_diffusion_cloud(static_cast(state.range(0))); const int basis = static_cast(state.range(1)); @@ -171,7 +169,7 @@ static void bench_eigenbasis(benchmark::State &state) { } } -static void bench_1form_gram(benchmark::State &state) { +static void bench_1form_gram(benchmark::State& state) { DiffusionMesh mesh = make_diffusion_cloud(static_cast(state.range(0))); igneous::ops::diffusion::compute_eigenbasis(mesh, static_cast(state.range(1))); igneous::ops::diffusion::GeometryWorkspace ws; @@ -182,7 +180,7 @@ static void bench_1form_gram(benchmark::State &state) { } } -static void bench_weak_derivative(benchmark::State &state) { +static void bench_weak_derivative(benchmark::State& state) { DiffusionMesh mesh = make_diffusion_cloud(static_cast(state.range(0))); igneous::ops::diffusion::compute_eigenbasis(mesh, static_cast(state.range(1))); igneous::ops::diffusion::HodgeWorkspace ws; @@ -193,7 +191,7 @@ static void bench_weak_derivative(benchmark::State &state) { } } -static void bench_curl_energy(benchmark::State &state) { +static void bench_curl_energy(benchmark::State& state) { DiffusionMesh mesh = make_diffusion_cloud(static_cast(state.range(0))); igneous::ops::diffusion::compute_eigenbasis(mesh, static_cast(state.range(1))); igneous::ops::diffusion::HodgeWorkspace ws; @@ -204,7 +202,7 @@ static void bench_curl_energy(benchmark::State &state) { } } -static void bench_hodge_solve(benchmark::State &state) { +static void bench_hodge_solve(benchmark::State& state) { DiffusionMesh mesh = make_diffusion_cloud(static_cast(state.range(0))); igneous::ops::diffusion::compute_eigenbasis(mesh, static_cast(state.range(1))); diff --git a/benches/bench_geometry.cpp b/benches/bench_geometry.cpp index 757840f..7457b9e 100644 --- a/benches/bench_geometry.cpp +++ b/benches/bench_geometry.cpp @@ -101,10 +101,8 @@ static BenchResult run_workload(int grid_size) { const auto t5 = Clock::now(); const double ms_topo = std::chrono::duration(t1 - t0).count(); - const double ms_curv = - std::chrono::duration(t3 - t2).count() / iterations; - const double ms_flow = - std::chrono::duration(t5 - t4).count() / iterations; + const double ms_curv = std::chrono::duration(t3 - t2).count() / iterations; + const double ms_flow = std::chrono::duration(t5 - t4).count() / iterations; return { "Grid " + std::to_string(grid_size) + "x" + std::to_string(grid_size), @@ -123,18 +121,16 @@ int main() { std::cout << "\n==========================================================================\n"; std::cout << " IGNEOUS GEOMETRY ENGINE BENCHMARK\n"; std::cout << "==========================================================================\n"; - std::cout << std::left << std::setw(15) << "Mesh" << std::setw(12) << "Verts" - << std::setw(12) << "Faces" << std::setw(15) << "Struct (ms)" - << std::setw(15) << "Curv (ms)" << std::setw(15) << "Flow (ms)" - << std::setw(10) << "Sim FPS" << "\n"; + std::cout << std::left << std::setw(15) << "Mesh" << std::setw(12) << "Verts" << std::setw(12) + << "Faces" << std::setw(15) << "Struct (ms)" << std::setw(15) << "Curv (ms)" + << std::setw(15) << "Flow (ms)" << std::setw(10) << "Sim FPS" << "\n"; std::cout << "--------------------------------------------------------------------------\n"; for (int size : sizes) { const auto res = run_workload(size); - std::cout << std::left << std::setw(15) << res.name << std::setw(12) - << res.verts << std::setw(12) << res.faces << std::fixed - << std::setprecision(3) << std::setw(15) << res.t_structure_ms - << std::setw(15) << res.t_curvature_ms << std::setw(15) + std::cout << std::left << std::setw(15) << res.name << std::setw(12) << res.verts + << std::setw(12) << res.faces << std::fixed << std::setprecision(3) << std::setw(15) + << res.t_structure_ms << std::setw(15) << res.t_curvature_ms << std::setw(15) << res.t_flow_ms << std::setw(10) << res.fps << "\n"; } diff --git a/benches/bench_memory.cpp b/benches/bench_memory.cpp index d100e40..aa845ae 100644 --- a/benches/bench_memory.cpp +++ b/benches/bench_memory.cpp @@ -20,7 +20,7 @@ int main() { auto start = Clock::now(); // We use a vector of pointers to simulate creating distinct objects - std::vector heap_objects; + std::vector heap_objects; heap_objects.reserve(OBJECT_COUNT); for (int i = 0; i < OBJECT_COUNT; ++i) { @@ -29,13 +29,11 @@ int main() { } auto end = Clock::now(); - auto dur_heap = - std::chrono::duration_cast(end - start) - .count(); + auto dur_heap = std::chrono::duration_cast(end - start).count(); fmt::print("Time: {} ms\n", dur_heap); // Cleanup heap objects (slow!) - for (auto *ptr : heap_objects) + for (auto* ptr : heap_objects) delete ptr; // --- Benchmark 2: Arena Allocation --- @@ -46,22 +44,20 @@ int main() { MemoryArena arena(OBJECT_COUNT * sizeof(Algebra) * 2); // We use std::pmr::vector which uses our arena - std::pmr::vector arena_objects(&arena); + std::pmr::vector arena_objects(&arena); arena_objects.reserve(OBJECT_COUNT); start = Clock::now(); for (int i = 0; i < OBJECT_COUNT; ++i) { // This uses our do_allocate (pointer bump) - void *mem = arena.allocate(sizeof(Algebra)); + void* mem = arena.allocate(sizeof(Algebra)); // Placement new (construct object in that memory) arena_objects.push_back(new (mem) Algebra()); } end = Clock::now(); - auto dur_arena = - std::chrono::duration_cast(end - start) - .count(); + auto dur_arena = std::chrono::duration_cast(end - start).count(); fmt::print("Time: {} ms\n", dur_arena); fmt::print("Speedup: {:.2f}x\n", (double)dur_heap / (double)dur_arena); diff --git a/benches/bench_pipelines.cpp b/benches/bench_pipelines.cpp index ab58ff7..4ade70d 100644 --- a/benches/bench_pipelines.cpp +++ b/benches/bench_pipelines.cpp @@ -24,14 +24,16 @@ using DiffusionMesh = igneous::data::Space; namespace { struct BenchEnvSetup { - BenchEnvSetup() { setenv("IGNEOUS_BENCH_MODE", "1", 1); } + BenchEnvSetup() { + setenv("IGNEOUS_BENCH_MODE", "1", 1); + } } kBenchEnvSetup; std::filesystem::path resolve_bunny_path() { - static const std::array kCandidates = { - "assets/bunny.obj", "../assets/bunny.obj", "../../assets/bunny.obj", - "../../../assets/bunny.obj"}; - for (const char *candidate : kCandidates) { + static const std::array kCandidates = {"assets/bunny.obj", "../assets/bunny.obj", + "../../assets/bunny.obj", + "../../../assets/bunny.obj"}; + for (const char* candidate : kCandidates) { const std::filesystem::path path(candidate); if (std::filesystem::exists(path)) { return path; @@ -52,7 +54,7 @@ DiffusionMesh make_bunny_geometry() { return mesh; } -void generate_torus(DiffusionMesh &mesh, size_t n_points, float R, float r) { +void generate_torus(DiffusionMesh& mesh, size_t n_points, float R, float r) { mesh.clear(); mesh.reserve(n_points); @@ -70,7 +72,7 @@ void generate_torus(DiffusionMesh &mesh, size_t n_points, float R, float r) { } } -int compute_max_y_vertex(const DiffusionMesh &mesh) { +int compute_max_y_vertex(const DiffusionMesh& mesh) { float max_y = -std::numeric_limits::infinity(); int max_y_idx = 0; for (size_t i = 0; i < mesh.num_points(); ++i) { @@ -83,13 +85,11 @@ int compute_max_y_vertex(const DiffusionMesh &mesh) { return max_y_idx; } -void build_diffusion_geometry(DiffusionMesh &mesh, float /*bandwidth*/, - int k_neighbors) { - mesh.structure.build({mesh.x_span(), mesh.y_span(), - mesh.z_span(), k_neighbors}); +void build_diffusion_geometry(DiffusionMesh& mesh, float /*bandwidth*/, int k_neighbors) { + mesh.structure.build({mesh.x_span(), mesh.y_span(), mesh.z_span(), k_neighbors}); } -void bench_pipeline_diffusion_main(benchmark::State &state) { +void bench_pipeline_diffusion_main(benchmark::State& state) { static DiffusionMesh base_mesh = make_bunny_geometry(); if (!base_mesh.is_valid()) { state.SkipWithError("Failed to load assets/bunny.obj"); @@ -117,7 +117,7 @@ void bench_pipeline_diffusion_main(benchmark::State &state) { } } -void bench_pipeline_spectral_main(benchmark::State &state) { +void bench_pipeline_spectral_main(benchmark::State& state) { static DiffusionMesh base_mesh = make_bunny_geometry(); if (!base_mesh.is_valid()) { state.SkipWithError("Failed to load assets/bunny.obj"); @@ -141,7 +141,7 @@ void bench_pipeline_spectral_main(benchmark::State &state) { } } -void bench_pipeline_hodge_main(benchmark::State &state) { +void bench_pipeline_hodge_main(benchmark::State& state) { constexpr float kBandwidth = 0.05f; constexpr int kNeighbors = 32; constexpr int kBasis = 64; @@ -176,7 +176,7 @@ void bench_pipeline_hodge_main(benchmark::State &state) { } } -void bench_hodge_phase_structure_build(benchmark::State &state) { +void bench_hodge_phase_structure_build(benchmark::State& state) { constexpr float kBandwidth = 0.05f; constexpr int kNeighbors = 32; DiffusionMesh mesh; @@ -188,7 +188,7 @@ void bench_hodge_phase_structure_build(benchmark::State &state) { } } -void bench_hodge_phase_eigenbasis(benchmark::State &state) { +void bench_hodge_phase_eigenbasis(benchmark::State& state) { constexpr float kBandwidth = 0.05f; constexpr int kNeighbors = 32; constexpr int kBasis = 64; @@ -202,7 +202,7 @@ void bench_hodge_phase_eigenbasis(benchmark::State &state) { } } -void bench_hodge_phase_gram(benchmark::State &state) { +void bench_hodge_phase_gram(benchmark::State& state) { constexpr float kBandwidth = 0.05f; constexpr int kNeighbors = 32; constexpr int kBasis = 64; @@ -219,7 +219,7 @@ void bench_hodge_phase_gram(benchmark::State &state) { } } -void bench_hodge_phase_weak_derivative(benchmark::State &state) { +void bench_hodge_phase_weak_derivative(benchmark::State& state) { constexpr float kBandwidth = 0.05f; constexpr int kNeighbors = 32; constexpr int kBasis = 64; @@ -236,7 +236,7 @@ void bench_hodge_phase_weak_derivative(benchmark::State &state) { } } -void bench_hodge_phase_curl_energy(benchmark::State &state) { +void bench_hodge_phase_curl_energy(benchmark::State& state) { constexpr float kBandwidth = 0.05f; constexpr int kNeighbors = 32; constexpr int kBasis = 64; @@ -253,7 +253,7 @@ void bench_hodge_phase_curl_energy(benchmark::State &state) { } } -void bench_hodge_phase_solve(benchmark::State &state) { +void bench_hodge_phase_solve(benchmark::State& state) { constexpr float kBandwidth = 0.05f; constexpr int kNeighbors = 32; constexpr int kBasis = 64; @@ -279,7 +279,7 @@ void bench_hodge_phase_solve(benchmark::State &state) { } } -void bench_hodge_phase_circular(benchmark::State &state) { +void bench_hodge_phase_circular(benchmark::State& state) { constexpr float kBandwidth = 0.05f; constexpr int kNeighbors = 32; constexpr int kBasis = 64; diff --git a/include/igneous/core/algebra.hpp b/include/igneous/core/algebra.hpp index d09052c..3b592f7 100644 --- a/include/igneous/core/algebra.hpp +++ b/include/igneous/core/algebra.hpp @@ -41,8 +41,7 @@ template constexpr int get_basis_metric(int index) { * \param b Right blade bitmap. * \return Multiplicative sign/metric coefficient. */ -template -constexpr int geometric_product_sign(unsigned int a, unsigned int b) { +template constexpr int geometric_product_sign(unsigned int a, unsigned int b) { int sign = 1; unsigned int a_temp = a >> 1; int swaps = 0; @@ -80,7 +79,7 @@ template struct AlgebraKernels { // Helper: Single component accumulation template - static constexpr void accumulate(Field &acc, const MV &a, const MV &b) { + static constexpr void accumulate(Field& acc, const MV& a, const MV& b) { constexpr size_t J = I ^ TargetK; // Wedge Constraint: No shared factors allowed @@ -98,7 +97,7 @@ template struct AlgebraKernels { // Helper: Unroll sum for one target component template - static constexpr void compute_component(MV &result, const MV &a, const MV &b, + static constexpr void compute_component(MV& result, const MV& a, const MV& b, std::index_sequence) { Field sum = Field(0); (accumulate(sum, a, b), ...); @@ -107,12 +106,9 @@ template struct AlgebraKernels { // The Generic Product template - static constexpr MV product_generic(const MV &a, const MV &b, - std::index_sequence) { + static constexpr MV product_generic(const MV& a, const MV& b, std::index_sequence) { MV result; - (compute_component(result, a, b, - std::make_index_sequence{}), - ...); + (compute_component(result, a, b, std::make_index_sequence{}), ...); return result; } @@ -122,7 +118,7 @@ template struct AlgebraKernels { * \param b Right operand. * \return Product multivector. */ - static constexpr MV geometric_product(const MV &a, const MV &b) { + static constexpr MV geometric_product(const MV& a, const MV& b) { return product_generic(a, b, std::make_index_sequence{}); } @@ -132,7 +128,7 @@ template struct AlgebraKernels { * \param b Right operand. * \return Wedge-product multivector. */ - static constexpr MV wedge_product(const MV &a, const MV &b) { + static constexpr MV wedge_product(const MV& a, const MV& b) { return product_generic(a, b, std::make_index_sequence{}); } }; @@ -146,7 +142,7 @@ template struct AlgebraKernels { // 1: e1, 2: e2, 3: e12 // 4: e3, 5: e13, 6: e23, 7: e123 - static constexpr MV wedge_product(const MV &a, const MV &b) { + static constexpr MV wedge_product(const MV& a, const MV& b) { MV res; // 1. Scalar part (Grade 0 ^ Grade 0) // In wedge, scalar * anything is just scaling. @@ -167,55 +163,53 @@ template struct AlgebraKernels { // e2^e1 = -e12. // Scalar interactions included. res[3] = a[0] * b[3] + a[1] * b[2] - a[2] * b[1] + a[3] * b[0]; // e12 - res[5] = a[0] * b[5] + a[1] * b[4] - a[4] * b[1] + - a[5] * b[0]; // e13 (Wait. e1^e3 = e13) - res[6] = a[0] * b[6] + a[2] * b[4] - a[4] * b[2] + - a[6] * b[0]; // e23 (e2^e3 = e23) + res[5] = a[0] * b[5] + a[1] * b[4] - a[4] * b[1] + a[5] * b[0]; // e13 (Wait. e1^e3 = e13) + res[6] = a[0] * b[6] + a[2] * b[4] - a[4] * b[2] + a[6] * b[0]; // e23 (e2^e3 = e23) // Grade 3 (Trivector) - Index 7 (e123) // e1^e23 = e123 // e2^e13 = -e123 (swap 1,2) // e3^e12 = e123 (swap 1,3 then 2,3... wait. e3 e1 e2 -> -e1 e3 e2 -> e1 e2 // e3. Yes.) Plus scalar terms. - res[7] = a[0] * b[7] + a[1] * b[6] - a[2] * b[5] + a[3] * b[4] + - a[4] * b[3] - a[5] * b[2] + a[6] * b[1] + a[7] * b[0]; + res[7] = a[0] * b[7] + a[1] * b[6] - a[2] * b[5] + a[3] * b[4] + a[4] * b[3] - a[5] * b[2] + + a[6] * b[1] + a[7] * b[0]; return res; } - static constexpr MV geometric_product(const MV &a, const MV &b) { + static constexpr MV geometric_product(const MV& a, const MV& b) { MV res; // This is the full Cl(3,0) multiplication table unrolled. // Generated from standard cayley table logic. // Scalar (0) - res[0] = a[0] * b[0] + a[1] * b[1] + a[2] * b[2] - a[3] * b[3] + - a[4] * b[4] - a[5] * b[5] - a[6] * b[6] - a[7] * b[7]; + res[0] = a[0] * b[0] + a[1] * b[1] + a[2] * b[2] - a[3] * b[3] + a[4] * b[4] - a[5] * b[5] - + a[6] * b[6] - a[7] * b[7]; // Vector (e1, e2, e3) // e1: 1, 0, 3(e12*e2=-e1), 5(e13*e3=-e1) ... - res[1] = a[0] * b[1] + a[1] * b[0] - a[2] * b[3] + a[3] * b[2] - - a[4] * b[5] + a[5] * b[4] - a[6] * b[7] - a[7] * b[6]; + res[1] = a[0] * b[1] + a[1] * b[0] - a[2] * b[3] + a[3] * b[2] - a[4] * b[5] + a[5] * b[4] - + a[6] * b[7] - a[7] * b[6]; - res[2] = a[0] * b[2] + a[1] * b[3] + a[2] * b[0] - a[3] * b[1] - - a[4] * b[6] - a[5] * b[7] + a[6] * b[4] + a[7] * b[5]; + res[2] = a[0] * b[2] + a[1] * b[3] + a[2] * b[0] - a[3] * b[1] - a[4] * b[6] - a[5] * b[7] + + a[6] * b[4] + a[7] * b[5]; - res[4] = a[0] * b[4] + a[1] * b[5] + a[2] * b[6] + a[3] * b[7] + - a[4] * b[0] - a[5] * b[1] - a[6] * b[2] + a[7] * b[3]; + res[4] = a[0] * b[4] + a[1] * b[5] + a[2] * b[6] + a[3] * b[7] + a[4] * b[0] - a[5] * b[1] - + a[6] * b[2] + a[7] * b[3]; // Bivector (e12, e13, e23) - res[3] = a[0] * b[3] + a[1] * b[2] - a[2] * b[1] + a[3] * b[0] + - a[4] * b[7] + a[5] * b[6] - a[6] * b[5] + a[7] * b[4]; + res[3] = a[0] * b[3] + a[1] * b[2] - a[2] * b[1] + a[3] * b[0] + a[4] * b[7] + a[5] * b[6] - + a[6] * b[5] + a[7] * b[4]; - res[5] = a[0] * b[5] + a[1] * b[4] - a[2] * b[7] - a[3] * b[6] + - a[4] * b[1] + a[5] * b[0] + a[6] * b[3] + a[7] * b[2]; + res[5] = a[0] * b[5] + a[1] * b[4] - a[2] * b[7] - a[3] * b[6] + a[4] * b[1] + a[5] * b[0] + + a[6] * b[3] + a[7] * b[2]; - res[6] = a[0] * b[6] + a[1] * b[7] + a[2] * b[4] + a[3] * b[5] - - a[4] * b[2] - a[5] * b[3] + a[6] * b[0] + a[7] * b[1]; + res[6] = a[0] * b[6] + a[1] * b[7] + a[2] * b[4] + a[3] * b[5] - a[4] * b[2] - a[5] * b[3] + + a[6] * b[0] + a[7] * b[1]; // Trivector (e123) - res[7] = a[0] * b[7] + a[1] * b[6] - a[2] * b[5] + a[3] * b[4] + - a[4] * b[3] - a[5] * b[2] + a[6] * b[1] + a[7] * b[0]; + res[7] = a[0] * b[7] + a[1] * b[6] - a[2] * b[5] + a[3] * b[4] + a[4] * b[3] - a[5] * b[2] + + a[6] * b[1] + a[7] * b[0]; return res; } @@ -247,20 +241,24 @@ template struct Multivector { * \param i Component index. * \return Component value. */ - constexpr Field operator[](size_t i) const { return data[i]; } + constexpr Field operator[](size_t i) const { + return data[i]; + } /** * \brief Mutable component access. * \param i Component index. * \return Mutable reference to component. */ - constexpr Field &operator[](size_t i) { return data[i]; } + constexpr Field& operator[](size_t i) { + return data[i]; + } /** * \brief Geometric product. * \param other Right-hand operand. * \return Product multivector. */ - constexpr Multivector operator*(const Multivector &other) const { + constexpr Multivector operator*(const Multivector& other) const { return AlgebraKernels::geometric_product(*this, other); } @@ -269,7 +267,7 @@ template struct Multivector { * \param other Right-hand operand. * \return Wedge-product multivector. */ - constexpr Multivector operator^(const Multivector &other) const { + constexpr Multivector operator^(const Multivector& other) const { return AlgebraKernels::wedge_product(*this, other); } @@ -278,7 +276,7 @@ template struct Multivector { * \param other Right-hand operand. * \return Sum multivector. */ - constexpr Multivector operator+(const Multivector &other) const { + constexpr Multivector operator+(const Multivector& other) const { Multivector res; for (size_t i = 0; i < Size; ++i) res[i] = data[i] + other[i]; @@ -290,7 +288,7 @@ template struct Multivector { * \param other Right-hand operand. * \return Difference multivector. */ - constexpr Multivector operator-(const Multivector &other) const { + constexpr Multivector operator-(const Multivector& other) const { Multivector res; for (size_t i = 0; i < Size; ++i) res[i] = data[i] - other[i]; diff --git a/include/igneous/core/blades.hpp b/include/igneous/core/blades.hpp index aca5b0c..0caf6e8 100644 --- a/include/igneous/core/blades.hpp +++ b/include/igneous/core/blades.hpp @@ -16,13 +16,17 @@ struct Bivec3 { * \brief Squared Euclidean magnitude. * \return Squared norm. */ - float norm_sq() const { return xy * xy + yz * yz + zx * zx; } + float norm_sq() const { + return xy * xy + yz * yz + zx * zx; + } /** * \brief Euclidean magnitude. * \return Norm. */ - float norm() const { return std::sqrt(norm_sq()); } + float norm() const { + return std::sqrt(norm_sq()); + } }; /// \brief Lightweight 3D vector utility for geometry kernels. @@ -39,39 +43,43 @@ struct Vec3 { * \param o Right-hand operand. * \return Sum vector. */ - Vec3 operator+(const Vec3 &o) const { return {x + o.x, y + o.y, z + o.z}; } + Vec3 operator+(const Vec3& o) const { + return {x + o.x, y + o.y, z + o.z}; + } /** * \brief Vector subtraction. * \param o Right-hand operand. * \return Difference vector. */ - Vec3 operator-(const Vec3 &o) const { return {x - o.x, y - o.y, z - o.z}; } + Vec3 operator-(const Vec3& o) const { + return {x - o.x, y - o.y, z - o.z}; + } /** * \brief Scalar multiplication. * \param s Scalar factor. * \return Scaled vector. */ - Vec3 operator*(float s) const { return {x * s, y * s, z * s}; } + Vec3 operator*(float s) const { + return {x * s, y * s, z * s}; + } /** * \brief Dot product. * \param o Right-hand operand. * \return Dot product scalar. */ - float dot(const Vec3 &o) const { return x * o.x + y * o.y + z * o.z; } + float dot(const Vec3& o) const { + return x * o.x + y * o.y + z * o.z; + } /** * \brief Exterior product producing a bivector. * \param o Right-hand operand. * \return Bivector wedge product. */ - Bivec3 operator^(const Vec3 &o) const { - return { - x * o.y - y * o.x, - y * o.z - z * o.y, - z * o.x - x * o.z - }; + Bivec3 operator^(const Vec3& o) const { + return {x * o.y - y * o.x, y * o.z - z * o.y, z * o.x - x * o.z}; } }; diff --git a/include/igneous/core/gpu.hpp b/include/igneous/core/gpu.hpp index 99b7391..421dea3 100644 --- a/include/igneous/core/gpu.hpp +++ b/include/igneous/core/gpu.hpp @@ -13,7 +13,7 @@ namespace igneous::core::gpu { * \return `true` when force-offload is enabled. */ inline bool gpu_force_enabled() { - const char *raw = std::getenv("IGNEOUS_GPU_FORCE"); + const char* raw = std::getenv("IGNEOUS_GPU_FORCE"); if (raw == nullptr) { return false; } @@ -29,7 +29,7 @@ inline bool gpu_force_enabled() { * \return Row threshold. */ inline int gpu_min_rows() { - const char *raw = std::getenv("IGNEOUS_GPU_MIN_ROWS"); + const char* raw = std::getenv("IGNEOUS_GPU_MIN_ROWS"); if (raw != nullptr) { const int parsed = std::atoi(raw); if (parsed > 0) { @@ -44,7 +44,7 @@ inline int gpu_min_rows() { * \return Work threshold. */ inline long long gpu_min_row_steps() { - const char *raw = std::getenv("IGNEOUS_GPU_MIN_ROW_STEPS"); + const char* raw = std::getenv("IGNEOUS_GPU_MIN_ROW_STEPS"); if (raw != nullptr) { const long long parsed = std::atoll(raw); if (parsed > 0) { @@ -61,7 +61,7 @@ inline long long gpu_min_row_steps() { * \brief Invalidate cached GPU resources associated with `cache_key`. * \param cache_key Cache identifier (typically structure address). */ -void invalidate_markov_cache(const void *cache_key); +void invalidate_markov_cache(const void* cache_key); /** * \brief GPU Markov single-step transition. @@ -73,12 +73,10 @@ void invalidate_markov_cache(const void *cache_key); * \param output Output scalar field. * \return `true` when GPU path executed successfully. */ -[[nodiscard]] bool apply_markov_transition(const void *cache_key, - std::span row_offsets, +[[nodiscard]] bool apply_markov_transition(const void* cache_key, std::span row_offsets, std::span col_indices, std::span weights, - std::span input, - std::span output); + std::span input, std::span output); /** * \brief GPU Markov multi-step transition. @@ -91,10 +89,10 @@ void invalidate_markov_cache(const void *cache_key); * \param output Output scalar field. * \return `true` when GPU path executed successfully. */ -[[nodiscard]] bool apply_markov_transition_steps( - const void *cache_key, std::span row_offsets, - std::span col_indices, std::span weights, - std::span input, int steps, std::span output); +[[nodiscard]] bool +apply_markov_transition_steps(const void* cache_key, std::span row_offsets, + std::span col_indices, std::span weights, + std::span input, int steps, std::span output); /** * \brief GPU carré du champ evaluation over CSR diffusion graph. @@ -108,13 +106,9 @@ void invalidate_markov_cache(const void *cache_key); * \param output Output gamma field. * \return `true` when GPU path executed successfully. */ -[[nodiscard]] bool carre_du_champ(const void *cache_key, - std::span row_offsets, - std::span col_indices, - std::span weights, - std::span f, - std::span h, - float inv_2t, +[[nodiscard]] bool carre_du_champ(const void* cache_key, std::span row_offsets, + std::span col_indices, std::span weights, + std::span f, std::span h, float inv_2t, std::span output); } // namespace igneous::core::gpu diff --git a/include/igneous/core/memory.hpp b/include/igneous/core/memory.hpp index 4cd83fd..5df145d 100644 --- a/include/igneous/core/memory.hpp +++ b/include/igneous/core/memory.hpp @@ -17,7 +17,7 @@ class MemoryArena : public std::pmr::memory_resource { /// \brief Owned backing storage. std::vector buffer; /// \brief Base pointer into `buffer`. - std::byte *ptr = nullptr; + std::byte* ptr = nullptr; /// \brief Current linear allocation offset in bytes. std::size_t offset = 0; @@ -32,18 +32,24 @@ class MemoryArena : public std::pmr::memory_resource { } /// \brief Reset all allocations in constant time. - void reset() { offset = 0; } + void reset() { + offset = 0; + } /** * \brief Number of bytes currently allocated from this arena. * \return Used bytes. */ - std::size_t used_bytes() const { return offset; } + std::size_t used_bytes() const { + return offset; + } /** * \brief Total arena capacity in bytes. * \return Total bytes. */ - std::size_t total_bytes() const { return buffer.size(); } + std::size_t total_bytes() const { + return buffer.size(); + } protected: /** @@ -52,11 +58,10 @@ class MemoryArena : public std::pmr::memory_resource { * \param alignment Requested alignment. * \return Pointer to allocated storage. */ - void *do_allocate(std::size_t bytes, std::size_t alignment) override { + void* do_allocate(std::size_t bytes, std::size_t alignment) override { // Calculate padding needed for alignment std::size_t padding = 0; - std::uintptr_t current_addr = - reinterpret_cast(ptr + offset); + std::uintptr_t current_addr = reinterpret_cast(ptr + offset); if (alignment > 0) { std::size_t mask = alignment - 1; @@ -69,7 +74,7 @@ class MemoryArena : public std::pmr::memory_resource { throw std::bad_alloc(); } - void *result = ptr + offset + padding; + void* result = ptr + offset + padding; offset += padding + bytes; return result; } @@ -80,8 +85,7 @@ class MemoryArena : public std::pmr::memory_resource { * \param bytes Ignored. * \param alignment Ignored. */ - void do_deallocate(void *p, std::size_t bytes, - std::size_t alignment) override { + void do_deallocate(void* p, std::size_t bytes, std::size_t alignment) override { (void)p; (void)bytes; (void)alignment; @@ -92,8 +96,7 @@ class MemoryArena : public std::pmr::memory_resource { * \param other Resource to compare against. * \return `true` if both references point to the same object. */ - bool - do_is_equal(const std::pmr::memory_resource &other) const noexcept override { + bool do_is_equal(const std::pmr::memory_resource& other) const noexcept override { return this == &other; } }; diff --git a/include/igneous/core/parallel.hpp b/include/igneous/core/parallel.hpp index 22cf778..1531e10 100644 --- a/include/igneous/core/parallel.hpp +++ b/include/igneous/core/parallel.hpp @@ -14,24 +14,20 @@ namespace igneous::core { /// \brief Runtime compute backend choice. -enum class ComputeBackend { - Cpu, - CpuParallel, - Gpu -}; +enum class ComputeBackend { Cpu, CpuParallel, Gpu }; /** * \brief Parse compute backend from `IGNEOUS_BACKEND`. * \return Selected backend enum value. */ inline ComputeBackend compute_backend_from_env() { - const char *raw = std::getenv("IGNEOUS_BACKEND"); + const char* raw = std::getenv("IGNEOUS_BACKEND"); if (raw == nullptr) { return ComputeBackend::CpuParallel; } std::string value(raw); - for (char &c : value) { + for (char& c : value) { c = static_cast(std::tolower(static_cast(c))); } @@ -49,7 +45,7 @@ inline ComputeBackend compute_backend_from_env() { * \return Worker count used by parallel loops. */ inline int compute_thread_count() { - const char *raw = std::getenv("IGNEOUS_NUM_THREADS"); + const char* raw = std::getenv("IGNEOUS_NUM_THREADS"); if (raw != nullptr) { const int requested = std::atoi(raw); if (requested > 0) { @@ -88,8 +84,7 @@ class ParallelWorkerPool { * \brief Construct pool with up to `max_workers - 1` background threads. * \param max_workers Total participants including caller thread. */ - explicit ParallelWorkerPool(int max_workers) - : max_workers_(std::max(0, max_workers - 1)) { + explicit ParallelWorkerPool(int max_workers) : max_workers_(std::max(0, max_workers - 1)) { workers_.reserve(static_cast(max_workers_)); for (int worker_idx = 0; worker_idx < max_workers_; ++worker_idx) { workers_.emplace_back([this, worker_idx]() { worker_loop(worker_idx); }); @@ -104,13 +99,13 @@ class ParallelWorkerPool { ++generation_; } cv_work_.notify_all(); - for (auto &worker : workers_) { + for (auto& worker : workers_) { worker.join(); } } - ParallelWorkerPool(const ParallelWorkerPool &) = delete; - ParallelWorkerPool &operator=(const ParallelWorkerPool &) = delete; + ParallelWorkerPool(const ParallelWorkerPool&) = delete; + ParallelWorkerPool& operator=(const ParallelWorkerPool&) = delete; /** * \brief Execute `[begin, end)` with up to `requested_workers` participants. @@ -119,15 +114,13 @@ class ParallelWorkerPool { * \param requested_workers Requested total participants. * \param fn Loop body receiving index `i`. */ - template - void run(int begin, int end, int requested_workers, Fn &&fn) { + template void run(int begin, int end, int requested_workers, Fn&& fn) { if (end <= begin) { return; } const int total = end - begin; - const int participants = - std::max(1, std::min({requested_workers, total, max_workers_ + 1})); + const int participants = std::max(1, std::min({requested_workers, total, max_workers_ + 1})); if (participants <= 1) { for (int i = begin; i < end; ++i) { fn(i); @@ -236,7 +229,7 @@ class ParallelWorkerPool { * \brief Singleton worker pool used by `parallel_for_index`. * \return Process-wide worker pool instance. */ -inline ParallelWorkerPool ¶llel_worker_pool() { +inline ParallelWorkerPool& parallel_worker_pool() { static ParallelWorkerPool pool(hardware_thread_count()); return pool; } @@ -251,7 +244,7 @@ inline ParallelWorkerPool ¶llel_worker_pool() { * \param min_parallel_range Minimum range length to enable parallel execution. */ template -void parallel_for_index(int begin, int end, Fn &&fn, int min_parallel_range = 32) { +void parallel_for_index(int begin, int end, Fn&& fn, int min_parallel_range = 32) { if (end <= begin) { return; } diff --git a/include/igneous/data/space.hpp b/include/igneous/data/space.hpp index 708351f..dc98927 100644 --- a/include/igneous/data/space.hpp +++ b/include/igneous/data/space.hpp @@ -7,8 +7,8 @@ #include #include -#include #include +#include namespace igneous::data { @@ -42,7 +42,9 @@ template struct Space { * \brief Number of stored points. * \return Number of points in the geometry arrays. */ - [[nodiscard]] size_t num_points() const { return x.size(); } + [[nodiscard]] size_t num_points() const { + return x.size(); + } /** * \brief Reserve geometry capacity for `vertices` points. @@ -69,14 +71,16 @@ template struct Space { * \param i Point index. * \return Point coordinates at index `i`. */ - [[nodiscard]] core::Vec3 get_vec3(size_t i) const { return {x[i], y[i], z[i]}; } + [[nodiscard]] core::Vec3 get_vec3(size_t i) const { + return {x[i], y[i], z[i]}; + } /** * \brief Overwrite a 3D point from `core::Vec3`. * \param i Point index. * \param v Replacement coordinate value. */ - void set_vec3(size_t i, const core::Vec3 &v) { + void set_vec3(size_t i, const core::Vec3& v) { x[i] = v.x; y[i] = v.y; z[i] = v.z; @@ -86,7 +90,7 @@ template struct Space { * \brief Append a new point to all coordinate channels. * \param v Point to append. */ - void push_point(const core::Vec3 &v) { + void push_point(const core::Vec3& v) { x.push_back(v.x); y.push_back(v.y); z.push_back(v.z); @@ -96,33 +100,45 @@ template struct Space { * \brief Immutable view of X coordinates. * \return Span over `x`. */ - [[nodiscard]] std::span x_span() const { return x; } + [[nodiscard]] std::span x_span() const { + return x; + } /** * \brief Immutable view of Y coordinates. * \return Span over `y`. */ - [[nodiscard]] std::span y_span() const { return y; } + [[nodiscard]] std::span y_span() const { + return y; + } /** * \brief Immutable view of Z coordinates. * \return Span over `z`. */ - [[nodiscard]] std::span z_span() const { return z; } + [[nodiscard]] std::span z_span() const { + return z; + } /** * \brief Mutable view of X coordinates. * \return Mutable span over `x`. */ - [[nodiscard]] std::span x_span() { return x; } + [[nodiscard]] std::span x_span() { + return x; + } /** * \brief Mutable view of Y coordinates. * \return Mutable span over `y`. */ - [[nodiscard]] std::span y_span() { return y; } + [[nodiscard]] std::span y_span() { + return y; + } /** * \brief Mutable view of Z coordinates. * \return Mutable span over `z`. */ - [[nodiscard]] std::span z_span() { return z; } + [[nodiscard]] std::span z_span() { + return z; + } /** * \brief Immutable grouped axis spans in X/Y/Z order. diff --git a/include/igneous/data/structure.hpp b/include/igneous/data/structure.hpp index 16f96dd..cb357f2 100644 --- a/include/igneous/data/structure.hpp +++ b/include/igneous/data/structure.hpp @@ -18,7 +18,7 @@ namespace igneous::data { * - a neighborhood query (`get_neighborhood`) */ template -concept Structure = requires(T &t, const T &ct, uint32_t idx) { +concept Structure = requires(T& t, const T& ct, uint32_t idx) { typename T::Input; { T::DIMENSION } -> std::convertible_to; { t.build(std::declval()) } -> std::same_as; @@ -33,13 +33,13 @@ concept Structure = requires(T &t, const T &ct, uint32_t idx) { */ template concept SurfaceStructure = - Structure && requires(T &t, const T &ct, size_t f_idx, size_t v_idx, int corner) { + Structure && requires(T& t, const T& ct, size_t f_idx, size_t v_idx, int corner) { { ct.num_faces() } -> std::convertible_to; { ct.get_vertex_for_face(f_idx, corner) } -> std::convertible_to; { ct.get_faces_for_vertex(v_idx) } -> std::convertible_to>; { ct.get_vertex_neighbors(v_idx) } -> std::convertible_to>; - { t.faces_to_vertices } -> std::same_as &>; - { ct.faces_to_vertices } -> std::same_as &>; + { t.faces_to_vertices } -> std::same_as&>; + { ct.faces_to_vertices } -> std::same_as&>; }; } // namespace igneous::data diff --git a/include/igneous/data/structures/diffusion_geometry.hpp b/include/igneous/data/structures/diffusion_geometry.hpp index b567f58..65eaeb4 100644 --- a/include/igneous/data/structures/diffusion_geometry.hpp +++ b/include/igneous/data/structures/diffusion_geometry.hpp @@ -139,7 +139,9 @@ struct DiffusionGeometry { std::span z; /// \brief Number of points available to the k-d tree. - [[nodiscard]] size_t kdtree_get_point_count() const { return x.size(); } + [[nodiscard]] size_t kdtree_get_point_count() const { + return x.size(); + } /// \brief Coordinate accessor required by nanoflann. [[nodiscard]] float kdtree_get_pt(size_t idx, size_t dim) const { @@ -153,13 +155,15 @@ struct DiffusionGeometry { } /// \brief Bounding box callback (unused for this adaptor). - template bool kdtree_get_bbox(BBOX &) const { return false; } + template bool kdtree_get_bbox(BBOX&) const { + return false; + } }; /// \brief nanoflann 3D L2 tree type. - using KDTree = nanoflann::KDTreeSingleIndexAdaptor< - nanoflann::L2_Simple_Adaptor, PointCloudAdaptor, - 3>; + using KDTree = + nanoflann::KDTreeSingleIndexAdaptor, + PointCloudAdaptor, 3>; /** * \brief Candidate epsilon sweep used during kernel tuning. @@ -182,9 +186,8 @@ struct DiffusionGeometry { * \param epsilons Candidate kernel scales. * \return Pair `(epsilon, dim_surrogate)`. */ - static std::pair tune_kernel(const std::vector &entries, - int n, int k, - const std::vector &epsilons) { + static std::pair tune_kernel(const std::vector& entries, int n, int k, + const std::vector& epsilons) { const int e_count = static_cast(epsilons.size()); std::vector averages(static_cast(e_count), 0.0f); const float inv_nk = 1.0f / static_cast(std::max(1, n * k)); @@ -216,8 +219,7 @@ struct DiffusionGeometry { } } - const float epsilon = - std::max(epsilons[static_cast(best_idx)], 1e-12f); + const float epsilon = std::max(epsilons[static_cast(best_idx)], 1e-12f); const float dim = 2.0f * best_criterion; return {epsilon, dim}; } @@ -230,9 +232,8 @@ struct DiffusionGeometry { * \param knn_bandwidth Number of neighbors used for RMS estimate. * \param bandwidths_out Output vector of local bandwidths. */ - static void compute_local_bandwidths(const std::vector &nbr_distances, - int n, int k, int knn_bandwidth, - Eigen::VectorXf &bandwidths_out) { + static void compute_local_bandwidths(const std::vector& nbr_distances, int n, int k, + int knn_bandwidth, Eigen::VectorXf& bandwidths_out) { bandwidths_out.resize(n); bandwidths_out.setOnes(); if (k <= 1) { @@ -263,8 +264,7 @@ struct DiffusionGeometry { return 1.0f; } const size_t mid = values.size() / 2; - std::nth_element(values.begin(), values.begin() + static_cast(mid), - values.end()); + std::nth_element(values.begin(), values.begin() + static_cast(mid), values.end()); return values[mid]; } @@ -281,8 +281,7 @@ struct DiffusionGeometry { } const int n = static_cast(n_verts); - const int k_neighbors = - std::max(1, std::min(input.k_neighbors, static_cast(n_verts))); + const int k_neighbors = std::max(1, std::min(input.k_neighbors, static_cast(n_verts))); const int knn_bandwidth = std::max(2, std::min(input.knn_bandwidth, k_neighbors)); use_mean_centres = input.use_mean_centres; knn_k = k_neighbors; @@ -310,8 +309,7 @@ struct DiffusionGeometry { const float query_pt[3] = {input.x[i], input.y[i], input.z[i]}; const size_t found = tree.knnSearch(query_pt, static_cast(k_neighbors), - ret_index.data(), - out_dist_sqr.data()); + ret_index.data(), out_dist_sqr.data()); const size_t usable = std::min(found, static_cast(k_neighbors)); for (size_t k = 0; k < usable; ++k) { knn_indices[base + k] = static_cast(ret_index[k]); @@ -325,8 +323,7 @@ struct DiffusionGeometry { 64); Eigen::VectorXf bandwidths_A; - compute_local_bandwidths(knn_distances, n, k_neighbors, knn_bandwidth, - bandwidths_A); + compute_local_bandwidths(knn_distances, n, k_neighbors, knn_bandwidth, bandwidths_A); std::vector kernel_entries_A(row_capacity, 0.0f); for (int i = 0; i < n; ++i) { @@ -336,18 +333,15 @@ struct DiffusionGeometry { const int j = knn_indices[base + static_cast(k)]; const float bw_j = std::max(bandwidths_A[j], 1e-8f); const float dist = knn_distances[base + static_cast(k)]; - kernel_entries_A[base + static_cast(k)] = - (dist * dist) / (bw_j * bw_i); + kernel_entries_A[base + static_cast(k)] = (dist * dist) / (bw_j * bw_i); } } const std::vector epsilons = build_epsilons(); - const auto [epsilon_A, dim_A] = - tune_kernel(kernel_entries_A, n, k_neighbors, epsilons); + const auto [epsilon_A, dim_A] = tune_kernel(kernel_entries_A, n, k_neighbors, epsilons); constexpr float kPi = 3.14159265358979323846f; - const float kernel_A_norm = - std::pow(kPi * std::max(epsilon_A, 1e-12f), dim_A * 0.5f); + const float kernel_A_norm = std::pow(kPi * std::max(epsilon_A, 1e-12f), dim_A * 0.5f); Eigen::VectorXf density_estimate_A(n); density_estimate_A.setZero(); for (int i = 0; i < n; ++i) { @@ -358,16 +352,14 @@ struct DiffusionGeometry { std::max(epsilon_A, 1e-12f)); } const float bw = std::max(bandwidths_A[i], 1e-8f); - density_estimate_A[i] = - (row_sum / std::max(kernel_A_norm, 1e-12f)) / - (static_cast(n) * std::pow(bw, dim_A)); + density_estimate_A[i] = (row_sum / std::max(kernel_A_norm, 1e-12f)) / + (static_cast(n) * std::pow(bw, dim_A)); density_estimate_A[i] = std::max(density_estimate_A[i], 1e-12f); } Eigen::VectorXf bandwidths_B(n); for (int i = 0; i < n; ++i) { - bandwidths_B[i] = - std::pow(density_estimate_A[i], input.bandwidth_variability); + bandwidths_B[i] = std::pow(density_estimate_A[i], input.bandwidth_variability); } std::vector bw_values(static_cast(n)); for (int i = 0; i < n; ++i) { @@ -384,13 +376,11 @@ struct DiffusionGeometry { const int j = knn_indices[base + static_cast(k)]; const float bw_j = std::max(bandwidths_B[j], 1e-8f); const float dist = knn_distances[base + static_cast(k)]; - kernel_entries_B[base + static_cast(k)] = - (dist * dist) / (bw_j * bw_i); + kernel_entries_B[base + static_cast(k)] = (dist * dist) / (bw_j * bw_i); } } - const auto [epsilon_B, dim_B] = - tune_kernel(kernel_entries_B, n, k_neighbors, epsilons); + const auto [epsilon_B, dim_B] = tune_kernel(kernel_entries_B, n, k_neighbors, epsilons); Eigen::VectorXf density_estimate_B(n); density_estimate_B.setZero(); @@ -406,8 +396,8 @@ struct DiffusionGeometry { density_estimate_B[i] = std::max(density_estimate_B[i], 1e-12f); } - const float alpha = 1.0f - (input.c * 0.5f) + - input.bandwidth_variability * ((dim_B * 0.5f) + 1.0f); + const float alpha = + 1.0f - (input.c * 0.5f) + input.bandwidth_variability * ((dim_B * 0.5f) + 1.0f); Eigen::VectorXf density_alpha(n); for (int i = 0; i < n; ++i) { density_alpha[i] = std::pow(density_estimate_B[i], -alpha); @@ -425,9 +415,8 @@ struct DiffusionGeometry { float row_sum = 0.0f; for (int k = 0; k < k_neighbors; ++k) { const int j = knn_indices[base + static_cast(k)]; - const float kernel_B = - std::exp(-kernel_entries_B[base + static_cast(k)] / - std::max(epsilon_B, 1e-12f)); + const float kernel_B = std::exp(-kernel_entries_B[base + static_cast(k)] / + std::max(epsilon_B, 1e-12f)); const float val = kernel_B * density_alpha[i] * density_alpha[j]; markov_values[base + static_cast(k)] = val; row_sum += val; @@ -445,8 +434,7 @@ struct DiffusionGeometry { self_idx = 0; } for (int k = 0; k < k_neighbors; ++k) { - markov_values[base + static_cast(k)] = - (k == self_idx) ? 1.0f : 0.0f; + markov_values[base + static_cast(k)] = (k == self_idx) ? 1.0f : 0.0f; } knn_kernel[base + static_cast(self_idx)] = 1.0f; for (int k = 0; k < k_neighbors; ++k) { @@ -489,8 +477,7 @@ struct DiffusionGeometry { const Eigen::VectorXf ones = Eigen::VectorXf::Ones(n); symmetric_row_sums = P_row * ones; - symmetric_row_sums = - symmetric_row_sums.array().max(1e-12f).matrix(); + symmetric_row_sums = symmetric_row_sums.array().max(1e-12f).matrix(); const float mu_sum = symmetric_row_sums.sum(); if (mu_sum > 1e-12f) { @@ -499,9 +486,9 @@ struct DiffusionGeometry { mu = Eigen::VectorXf::Constant(n, 1.0f / static_cast(n)); } - const int *outer = P_row.outerIndexPtr(); - const int *inner = P_row.innerIndexPtr(); - const float *values = P_row.valuePtr(); + const int* outer = P_row.outerIndexPtr(); + const int* inner = P_row.innerIndexPtr(); + const float* values = P_row.valuePtr(); const int nnz_sym = P_row.nonZeros(); symmetric_row_offsets.assign(outer, outer + (n + 1)); symmetric_col_indices.assign(inner, inner + nnz_sym); @@ -534,8 +521,7 @@ struct DiffusionGeometry { if (std::getenv("IGNEOUS_BENCH_MODE") == nullptr) { std::cout << "[Diffusion] Built reference-style kernel for " << n_verts - << " points (k=" << k_neighbors << ", knn_bw=" << knn_bandwidth - << ").\n"; + << " points (k=" << k_neighbors << ", knn_bw=" << knn_bandwidth << ").\n"; } } }; diff --git a/include/igneous/data/structures/discrete_exterior_calculus.hpp b/include/igneous/data/structures/discrete_exterior_calculus.hpp index 3bb6054..b10b8d3 100644 --- a/include/igneous/data/structures/discrete_exterior_calculus.hpp +++ b/include/igneous/data/structures/discrete_exterior_calculus.hpp @@ -246,7 +246,7 @@ struct DiscreteExteriorCalculus { return; } - const auto gather_unique_neighbors = [&](uint32_t v, std::vector &neighbors) { + const auto gather_unique_neighbors = [&](uint32_t v, std::vector& neighbors) { neighbors.clear(); const uint32_t face_begin = vertex_face_offsets[v]; const uint32_t face_end = vertex_face_offsets[v + 1]; diff --git a/include/igneous/igneous.hpp b/include/igneous/igneous.hpp index 3335397..90f075b 100644 --- a/include/igneous/igneous.hpp +++ b/include/igneous/igneous.hpp @@ -9,8 +9,8 @@ #include #include -#include #include +#include #include #include diff --git a/include/igneous/io/exporter.hpp b/include/igneous/io/exporter.hpp index 56efa2a..3f02b8b 100644 --- a/include/igneous/io/exporter.hpp +++ b/include/igneous/io/exporter.hpp @@ -56,7 +56,8 @@ inline std::tuple get_heatmap_color_bytes(double t) { * \return Pair `(min_value, max_value)` used for color normalization. */ template -inline std::pair compute_field_bounds(std::span field, double sigma_clip) { +inline std::pair compute_field_bounds(std::span field, + double sigma_clip) { double sum = 0.0; double sum_sq = 0.0; int count = 0; @@ -86,8 +87,8 @@ inline std::pair compute_field_bounds(std::span fie * \param sigma_clip Sigma clipping for color normalization. */ template -void export_ply(const Space &mesh, std::span field, - const std::string &filename, double sigma_clip = 2.0) { +void export_ply(const Space& mesh, std::span field, + const std::string& filename, double sigma_clip = 2.0) { const auto [min_v, max_v] = compute_field_bounds(field, sigma_clip); std::ofstream file(filename); @@ -115,8 +116,8 @@ void export_ply(const Space &mesh, std::span field, const double t = (val - min_v) / denom; const auto [r, g, b] = get_heatmap_color_bytes(t); - file << p.x << " " << p.y << " " << p.z << " " << static_cast(r) << " " << static_cast(g) - << " " << static_cast(b) << "\n"; + file << p.x << " " << p.y << " " << p.z << " " << static_cast(r) << " " + << static_cast(g) << " " << static_cast(b) << "\n"; } std::cout << "[IO] Exported PLY " << filename << "\n"; @@ -130,10 +131,9 @@ void export_ply(const Space &mesh, std::span field, * \param sigma_clip Sigma clipping for color normalization. */ template -void export_ply(const Space &mesh, const std::vector &field, - const std::string &filename, double sigma_clip = 2.0) { - export_ply(mesh, std::span(field.data(), field.size()), filename, - sigma_clip); +void export_ply(const Space& mesh, const std::vector& field, + const std::string& filename, double sigma_clip = 2.0) { + export_ply(mesh, std::span(field.data(), field.size()), filename, sigma_clip); } /** @@ -148,8 +148,8 @@ void export_ply(const Space &mesh, const std::vector &field, * \param sigma_clip Sigma clipping for color normalization. */ template -void export_heatmap(const Space &mesh, std::span field, - const std::string &filename, double sigma_clip = 2.0) { +void export_heatmap(const Space& mesh, std::span field, + const std::string& filename, double sigma_clip = 2.0) { const auto [min_v, max_v] = compute_field_bounds(field, sigma_clip); std::ofstream file(filename); @@ -166,8 +166,8 @@ void export_heatmap(const Space &mesh, std::span field, const double t = (val - min_v) / denom; const auto [r, g, b] = get_heatmap_color_bytes(t); - file << "v " << p.x << " " << p.y << " " << p.z << " " << (r / 255.0f) << " " << (g / 255.0f) << " " - << (b / 255.0f) << "\n"; + file << "v " << p.x << " " << p.y << " " << p.z << " " << (r / 255.0f) << " " << (g / 255.0f) + << " " << (b / 255.0f) << "\n"; } if constexpr (igneous::data::SurfaceStructure) { @@ -199,11 +199,9 @@ void export_heatmap(const Space &mesh, std::span field, * \param sigma_clip Sigma clipping for color normalization. */ template -void export_heatmap(const Space &mesh, - const std::vector &field, - const std::string &filename, double sigma_clip = 2.0) { - export_heatmap(mesh, std::span(field.data(), field.size()), - filename, sigma_clip); +void export_heatmap(const Space& mesh, const std::vector& field, + const std::string& filename, double sigma_clip = 2.0) { + export_heatmap(mesh, std::span(field.data(), field.size()), filename, sigma_clip); } /** @@ -219,9 +217,8 @@ void export_heatmap(const Space &mesh, * \param sigma_clip Sigma clipping for color normalization. */ template -void export_ply_solid(const Space &mesh, std::span field, - const std::string &filename, double radius = 0.01, - double sigma_clip = 2.0) { +void export_ply_solid(const Space& mesh, std::span field, + const std::string& filename, double radius = 0.01, double sigma_clip = 2.0) { const auto [min_v, max_v] = compute_field_bounds(field, sigma_clip); std::ofstream file(filename); @@ -261,9 +258,12 @@ void export_ply_solid(const Space &mesh, std::span fiel const int ib = b; file << p.x << " " << (p.y + h) << " " << p.z << " " << ir << " " << ig << " " << ib << "\n"; - file << (p.x - s) << " " << (p.y - s) << " " << (p.z + s) << " " << ir << " " << ig << " " << ib << "\n"; - file << (p.x + s) << " " << (p.y - s) << " " << (p.z + s) << " " << ir << " " << ig << " " << ib << "\n"; - file << p.x << " " << (p.y - s) << " " << (p.z - s) << " " << ir << " " << ig << " " << ib << "\n"; + file << (p.x - s) << " " << (p.y - s) << " " << (p.z + s) << " " << ir << " " << ig << " " << ib + << "\n"; + file << (p.x + s) << " " << (p.y - s) << " " << (p.z + s) << " " << ir << " " << ig << " " << ib + << "\n"; + file << p.x << " " << (p.y - s) << " " << (p.z - s) << " " << ir << " " << ig << " " << ib + << "\n"; } for (size_t i = 0; i < n_points; ++i) { @@ -286,12 +286,10 @@ void export_ply_solid(const Space &mesh, std::span fiel * \param sigma_clip Sigma clipping for color normalization. */ template -void export_ply_solid(const Space &mesh, - const std::vector &field, - const std::string &filename, double radius = 0.01, - double sigma_clip = 2.0) { - export_ply_solid(mesh, std::span(field.data(), field.size()), - filename, radius, sigma_clip); +void export_ply_solid(const Space& mesh, const std::vector& field, + const std::string& filename, double radius = 0.01, double sigma_clip = 2.0) { + export_ply_solid(mesh, std::span(field.data(), field.size()), filename, radius, + sigma_clip); } } // namespace igneous::io diff --git a/include/igneous/io/importer.hpp b/include/igneous/io/importer.hpp index 090e4a9..e782143 100644 --- a/include/igneous/io/importer.hpp +++ b/include/igneous/io/importer.hpp @@ -23,8 +23,7 @@ using igneous::data::Space; * \param mesh Destination space to overwrite. * \param filename OBJ file path. */ -template -void load_obj(Space &mesh, const std::string &filename) { +template void load_obj(Space& mesh, const std::string& filename) { std::ifstream file(filename); if (!file.is_open()) { std::cerr << "Failed to open " << filename << "\n"; @@ -63,8 +62,7 @@ void load_obj(Space &mesh, const std::string &filename) { } } } - std::cout << "[IO] Loaded " << filename << " (" << mesh.num_points() - << " verts)\n"; + std::cout << "[IO] Loaded " << filename << " (" << mesh.num_points() << " verts)\n"; } } // namespace igneous::io diff --git a/include/igneous/ops/dec/curvature.hpp b/include/igneous/ops/dec/curvature.hpp index 17a0584..e69d40e 100644 --- a/include/igneous/ops/dec/curvature.hpp +++ b/include/igneous/ops/dec/curvature.hpp @@ -29,11 +29,9 @@ template struct CurvatureWorkspace { * \param workspace Reused temporary buffers. */ template -void compute_curvature_measures(const data::Space &space, - std::vector &H, - std::vector &K, - CurvatureWorkspace &workspace) { - const auto &structure = space.structure; +void compute_curvature_measures(const data::Space& space, std::vector& H, + std::vector& K, CurvatureWorkspace& workspace) { + const auto& structure = space.structure; const size_t num_verts = space.num_points(); const size_t num_faces = structure.num_faces(); @@ -129,9 +127,8 @@ void compute_curvature_measures(const data::Space &space, } if (area_sum > 1e-12f) { - K[i] = static_cast( - (2.0 * std::numbers::pi_v - angle_sum) / - (static_cast(area_sum) / 3.0)); + K[i] = static_cast((2.0 * std::numbers::pi_v - angle_sum) / + (static_cast(area_sum) / 3.0)); } const float n_mag_sq = n_xy * n_xy + n_yz * n_yz + n_zx * n_zx; diff --git a/include/igneous/ops/dec/flow.hpp b/include/igneous/ops/dec/flow.hpp index c1112c0..0f3b222 100644 --- a/include/igneous/ops/dec/flow.hpp +++ b/include/igneous/ops/dec/flow.hpp @@ -25,9 +25,9 @@ template struct FlowWorkspace { * \param workspace Reused temporary buffers. */ template -void integrate_mean_curvature_flow(data::Space &space, float dt, - FlowWorkspace &workspace) { - const auto &structure = space.structure; +void integrate_mean_curvature_flow(data::Space& space, float dt, + FlowWorkspace& workspace) { + const auto& structure = space.structure; const size_t num_verts = space.num_points(); @@ -36,8 +36,8 @@ void integrate_mean_curvature_flow(data::Space &space, float dt, } if constexpr (std::is_same_v) { - const auto &neighbor_offsets = structure.vertex_neighbor_offsets; - const auto &neighbor_data = structure.vertex_neighbor_data; + const auto& neighbor_offsets = structure.vertex_neighbor_offsets; + const auto& neighbor_data = structure.vertex_neighbor_data; core::parallel_for_index( 0, static_cast(num_verts), @@ -61,8 +61,7 @@ void integrate_mean_curvature_flow(data::Space &space, float dt, } const float inv_count = 1.0f / static_cast(end - begin); - workspace.displacements[i] = {sx * inv_count - space.x[i], - sy * inv_count - space.y[i], + workspace.displacements[i] = {sx * inv_count - space.x[i], sy * inv_count - space.y[i], sz * inv_count - space.z[i]}; }, 131072); @@ -71,7 +70,7 @@ void integrate_mean_curvature_flow(data::Space &space, float dt, 0, static_cast(num_verts), [&](int vertex_idx) { const size_t i = static_cast(vertex_idx); - const core::Vec3 &d = workspace.displacements[i]; + const core::Vec3& d = workspace.displacements[i]; space.x[i] += d.x * dt; space.y[i] += d.y * dt; space.z[i] += d.z * dt; diff --git a/include/igneous/ops/diffusion/basis.hpp b/include/igneous/ops/diffusion/basis.hpp index 3a29981..b224a74 100644 --- a/include/igneous/ops/diffusion/basis.hpp +++ b/include/igneous/ops/diffusion/basis.hpp @@ -65,9 +65,8 @@ inline int binomial_coeff(int n, int k) { * \param current Current partial combination. * \param out Destination list of combinations. */ -inline void combinations_recursive(int n, int k, int start, - std::vector ¤t, - std::vector> &out) { +inline void combinations_recursive(int n, int k, int start, std::vector& current, + std::vector>& out) { if (static_cast(current.size()) == k) { out.push_back(current); return; @@ -110,7 +109,7 @@ inline std::vector> get_wedge_basis_indices(int d, int k) { * \param d Ambient dimension. * \return Zero-based rank among all `k`-combinations. */ -inline int lex_rank_combination(const std::vector &comb, int d) { +inline int lex_rank_combination(const std::vector& comb, int d) { const int k = static_cast(comb.size()); if (k == 0) { return 0; @@ -149,7 +148,7 @@ inline Kp1ChildrenAndSignsData kp1_children_and_signs(int d, int k) { out.children.resize(out.idx_kp1.size(), std::vector(static_cast(k + 1), 0)); for (size_t row = 0; row < out.idx_kp1.size(); ++row) { - const auto &parent = out.idx_kp1[row]; + const auto& parent = out.idx_kp1[row]; for (int r = 0; r < k + 1; ++r) { std::vector child; child.reserve(static_cast(k)); @@ -172,7 +171,7 @@ inline Kp1ChildrenAndSignsData kp1_children_and_signs(int d, int k) { * \param subset Sorted subset indices. * \return Sorted complement indices. */ -inline std::vector complement_indices(int total, const std::vector &subset) { +inline std::vector complement_indices(int total, const std::vector& subset) { std::vector out; out.reserve(static_cast(total - static_cast(subset.size()))); int cursor = 0; @@ -215,9 +214,9 @@ inline WedgeProductIndexData get_wedge_product_indices(int d, int k1, int k2) { out.signs.reserve(n_terms); for (int target_idx = 0; target_idx < out.n_targets; ++target_idx) { - const auto &target = targets[static_cast(target_idx)]; + const auto& target = targets[static_cast(target_idx)]; for (int split = 0; split < out.n_splits; ++split) { - const auto &local_left = local_I[static_cast(split)]; + const auto& local_left = local_I[static_cast(split)]; const auto local_right = complement_indices(k_total, local_left); std::vector left; @@ -233,9 +232,7 @@ inline WedgeProductIndexData get_wedge_product_indices(int d, int k1, int k2) { } const int parity = - (std::accumulate(local_left.begin(), local_left.end(), 0) - - (k1 * (k1 - 1) / 2)) & - 1; + (std::accumulate(local_left.begin(), local_left.end(), 0) - (k1 * (k1 - 1) / 2)) & 1; const int sign = (parity == 0) ? 1 : -1; out.target_indices.push_back(target_idx); diff --git a/include/igneous/ops/diffusion/forms.hpp b/include/igneous/ops/diffusion/forms.hpp index d6d6a85..7125d1d 100644 --- a/include/igneous/ops/diffusion/forms.hpp +++ b/include/igneous/ops/diffusion/forms.hpp @@ -49,7 +49,9 @@ struct CompoundDeterminantData { * \brief Ambient coordinate dimension used by current diffusion form operators. * \return Ambient dimension (`3`). */ -inline int ambient_dim_3d() { return 3; } +inline int ambient_dim_3d() { + return 3; +} /** * \brief Determinant of a tiny dense matrix packed row-major in `data`. @@ -57,7 +59,7 @@ inline int ambient_dim_3d() { return 3; } * \param k Matrix dimension. * \return Determinant value. */ -inline float determinant_small(const float *data, int k) { +inline float determinant_small(const float* data, int k) { if (k == 0) { return 1.0f; } @@ -88,9 +90,9 @@ inline float determinant_small(const float *data, int k) { * \param cols Column-index subset. * \return Vector of determinant values per point. */ -inline Eigen::VectorXf build_minor_det_vector( - const std::array, 3> &gamma_coords, - const std::vector &rows, const std::vector &cols) { +inline Eigen::VectorXf +build_minor_det_vector(const std::array, 3>& gamma_coords, + const std::vector& rows, const std::vector& cols) { const int n = gamma_coords[0][0].size(); const int k = static_cast(rows.size()); Eigen::VectorXf out(n); @@ -114,7 +116,7 @@ inline Eigen::VectorXf build_minor_det_vector( * \param workspace Scratch workspace to populate. */ template -void ensure_gamma_coords(const MeshT &mesh, DiffusionFormWorkspace &workspace) { +void ensure_gamma_coords(const MeshT& mesh, DiffusionFormWorkspace& workspace) { if (workspace.gamma_coords_ready) { return; } @@ -141,9 +143,9 @@ void ensure_gamma_coords(const MeshT &mesh, DiffusionFormWorkspace &works * \param workspace Scratch workspace to populate. */ template -void ensure_gamma_mixed(const MeshT &mesh, int n_coefficients, - DiffusionFormWorkspace &workspace) { - const auto &U = mesh.structure.eigen_basis; +void ensure_gamma_mixed(const MeshT& mesh, int n_coefficients, + DiffusionFormWorkspace& workspace) { + const auto& U = mesh.structure.eigen_basis; const int n = static_cast(mesh.num_points()); const int n1 = std::max(1, std::min(n_coefficients, static_cast(U.cols()))); @@ -173,8 +175,8 @@ void ensure_gamma_mixed(const MeshT &mesh, int n_coefficients, * \return Precomputed compound determinant data. */ template -CompoundDeterminantData compute_compound_determinants( - const MeshT &mesh, int k, DiffusionFormWorkspace &workspace) { +CompoundDeterminantData compute_compound_determinants(const MeshT& mesh, int k, + DiffusionFormWorkspace& workspace) { ensure_gamma_coords(mesh, workspace); CompoundDeterminantData out; @@ -190,8 +192,7 @@ CompoundDeterminantData compute_compound_determinants( for (int a = 0; a < out.n_components; ++a) { for (int b = 0; b < out.n_components; ++b) { out.values[static_cast(a * out.n_components + b)] = - build_minor_det_vector(workspace.gamma_coords, - out.indices[static_cast(a)], + build_minor_det_vector(workspace.gamma_coords, out.indices[static_cast(a)], out.indices[static_cast(b)]); } } @@ -208,11 +209,10 @@ CompoundDeterminantData compute_compound_determinants( * \return Symmetric Gram matrix for k-forms. */ template -Eigen::MatrixXf compute_kform_gram_matrix(const MeshT &mesh, int k, - int n_coefficients, - DiffusionFormWorkspace &workspace) { - const auto &U = mesh.structure.eigen_basis; - const auto &mu = mesh.structure.mu; +Eigen::MatrixXf compute_kform_gram_matrix(const MeshT& mesh, int k, int n_coefficients, + DiffusionFormWorkspace& workspace) { + const auto& U = mesh.structure.eigen_basis; + const auto& mu = mesh.structure.mu; const int n1 = std::max(1, std::min(n_coefficients, static_cast(U.cols()))); const auto compounds = compute_compound_determinants(mesh, k, workspace); const int Ck = std::max(1, compounds.n_components); @@ -224,8 +224,7 @@ Eigen::MatrixXf compute_kform_gram_matrix(const MeshT &mesh, int k, if (k == 0) { for (int i = 0; i < n1; ++i) { for (int l = i; l < n1; ++l) { - const float val = - (U.col(i).array() * U.col(l).array() * mu.array()).sum(); + const float val = (U.col(i).array() * U.col(l).array() * mu.array()).sum(); G(i, l) = val; if (i != l) { G(l, i) = val; @@ -240,8 +239,7 @@ Eigen::MatrixXf compute_kform_gram_matrix(const MeshT &mesh, int k, weights = U.col(i).array() * U.col(l).array() * mu.array(); for (int a = 0; a < Ck; ++a) { for (int b = 0; b < Ck; ++b) { - const auto &compound = - compounds.values[static_cast(a * Ck + b)]; + const auto& compound = compounds.values[static_cast(a * Ck + b)]; const float val = (weights * compound.array()).sum(); const int row = i * Ck + a; const int col = l * Ck + b; @@ -265,8 +263,7 @@ Eigen::MatrixXf compute_kform_gram_matrix(const MeshT &mesh, int k, * \return Symmetric Gram matrix for k-forms. */ template -Eigen::MatrixXf compute_kform_gram_matrix(const MeshT &mesh, int k, - int n_coefficients) { +Eigen::MatrixXf compute_kform_gram_matrix(const MeshT& mesh, int k, int n_coefficients) { DiffusionFormWorkspace workspace; return compute_kform_gram_matrix(mesh, k, n_coefficients, workspace); } @@ -280,15 +277,14 @@ Eigen::MatrixXf compute_kform_gram_matrix(const MeshT &mesh, int k, * \return Weak derivative matrix. */ template -Eigen::MatrixXf compute_weak_exterior_derivative( - const MeshT &mesh, int k, int n_coefficients, - DiffusionFormWorkspace &workspace) { +Eigen::MatrixXf compute_weak_exterior_derivative(const MeshT& mesh, int k, int n_coefficients, + DiffusionFormWorkspace& workspace) { if (k < 0 || k > 2) { return Eigen::MatrixXf(); } - const auto &U = mesh.structure.eigen_basis; - const auto &mu = mesh.structure.mu; + const auto& U = mesh.structure.eigen_basis; + const auto& mu = mesh.structure.mu; const int n1 = std::max(1, std::min(n_coefficients, static_cast(U.cols()))); ensure_gamma_mixed(mesh, n1, workspace); @@ -297,8 +293,7 @@ Eigen::MatrixXf compute_weak_exterior_derivative( Eigen::MatrixXf D = Eigen::MatrixXf::Zero(n1 * d, n1); Eigen::MatrixXf weighted_u = U.leftCols(n1).array().colwise() * mu.array(); for (int a = 0; a < d; ++a) { - const Eigen::MatrixXf coupling = - weighted_u.transpose() * workspace.gamma_mixed[a]; + const Eigen::MatrixXf coupling = weighted_u.transpose() * workspace.gamma_mixed[a]; for (int i = 0; i < n1; ++i) { D.row(i * d + a) = coupling.row(i); } @@ -322,15 +317,11 @@ Eigen::MatrixXf compute_weak_exterior_derivative( for (int p = 0; p < n; ++p) { float laplace_sum = 0.0f; for (int r = 0; r < k + 1; ++r) { - const int coord_dim = - kp1.idx_kp1[static_cast(Jp)][static_cast(r)]; - const int child_idx = - kp1.children[static_cast(Jp)][static_cast(r)]; - const auto &minor_det = - compounds.values[static_cast(child_idx * Ck + J)]; - laplace_sum += - static_cast(kp1.signs[static_cast(r)]) * - workspace.gamma_mixed[coord_dim](p, i) * minor_det[p]; + const int coord_dim = kp1.idx_kp1[static_cast(Jp)][static_cast(r)]; + const int child_idx = kp1.children[static_cast(Jp)][static_cast(r)]; + const auto& minor_det = compounds.values[static_cast(child_idx * Ck + J)]; + laplace_sum += static_cast(kp1.signs[static_cast(r)]) * + workspace.gamma_mixed[coord_dim](p, i) * minor_det[p]; } accum += U(p, I) * laplace_sum * mu[p]; } @@ -351,8 +342,7 @@ Eigen::MatrixXf compute_weak_exterior_derivative( * \return Weak derivative matrix. */ template -Eigen::MatrixXf compute_weak_exterior_derivative(const MeshT &mesh, int k, - int n_coefficients) { +Eigen::MatrixXf compute_weak_exterior_derivative(const MeshT& mesh, int k, int n_coefficients) { DiffusionFormWorkspace workspace; return compute_weak_exterior_derivative(mesh, k, n_coefficients, workspace); } @@ -363,8 +353,7 @@ Eigen::MatrixXf compute_weak_exterior_derivative(const MeshT &mesh, int k, * \param rcond Relative conditioning threshold. * \return Pseudo-inverse matrix. */ -inline Eigen::MatrixXf pseudo_inverse_symmetric(const Eigen::MatrixXf &matrix, - float rcond); +inline Eigen::MatrixXf pseudo_inverse_symmetric(const Eigen::MatrixXf& matrix, float rcond); /** * \brief Assemble codifferential matrix `delta_k` from `d_{k-1}` and mass. @@ -375,18 +364,15 @@ inline Eigen::MatrixXf pseudo_inverse_symmetric(const Eigen::MatrixXf &matrix, * \return Codifferential matrix. */ template -Eigen::MatrixXf compute_codifferential_matrix( - const MeshT &mesh, int k, int n_coefficients, - DiffusionFormWorkspace &workspace) { +Eigen::MatrixXf compute_codifferential_matrix(const MeshT& mesh, int k, int n_coefficients, + DiffusionFormWorkspace& workspace) { if (k <= 0) { return Eigen::MatrixXf(); } const Eigen::MatrixXf D_prev = compute_weak_exterior_derivative(mesh, k - 1, n_coefficients, workspace); - const Eigen::MatrixXf G_prev = - compute_kform_gram_matrix(mesh, k - 1, n_coefficients, workspace); - const Eigen::MatrixXf G_prev_inv = - pseudo_inverse_symmetric(G_prev, 1e-5f); + const Eigen::MatrixXf G_prev = compute_kform_gram_matrix(mesh, k - 1, n_coefficients, workspace); + const Eigen::MatrixXf G_prev_inv = pseudo_inverse_symmetric(G_prev, 1e-5f); return G_prev_inv * D_prev.transpose(); } @@ -398,8 +384,7 @@ Eigen::MatrixXf compute_codifferential_matrix( * \return Codifferential matrix. */ template -Eigen::MatrixXf compute_codifferential_matrix(const MeshT &mesh, int k, - int n_coefficients) { +Eigen::MatrixXf compute_codifferential_matrix(const MeshT& mesh, int k, int n_coefficients) { DiffusionFormWorkspace workspace; return compute_codifferential_matrix(mesh, k, n_coefficients, workspace); } @@ -410,8 +395,7 @@ Eigen::MatrixXf compute_codifferential_matrix(const MeshT &mesh, int k, * \param b Right-hand side vector. * \return Solution vector. */ -inline Eigen::Vector2f solve_stable_2x2(const Eigen::Matrix2f &A, - const Eigen::Vector2f &b) { +inline Eigen::Vector2f solve_stable_2x2(const Eigen::Matrix2f& A, const Eigen::Vector2f& b) { const float det = A(0, 0) * A(1, 1) - A(0, 1) * A(1, 0); if (std::abs(det) > 1e-8f) { const float inv_det = 1.0f / det; @@ -438,7 +422,7 @@ inline Eigen::Vector2f solve_stable_2x2(const Eigen::Matrix2f &A, * \param rcond Relative threshold for inverting eigenvalues. * \return Pseudo-inverse matrix. */ -inline Eigen::MatrixXf pseudo_inverse_symmetric(const Eigen::MatrixXf &matrix, +inline Eigen::MatrixXf pseudo_inverse_symmetric(const Eigen::MatrixXf& matrix, float rcond = 1e-5f) { if (matrix.rows() == 0 || matrix.cols() == 0) { return Eigen::MatrixXf(matrix.cols(), matrix.rows()); @@ -449,8 +433,8 @@ inline Eigen::MatrixXf pseudo_inverse_symmetric(const Eigen::MatrixXf &matrix, return Eigen::MatrixXf::Zero(matrix.cols(), matrix.rows()); } - const auto &evals = solver.eigenvalues(); - const auto &evecs = solver.eigenvectors(); + const auto& evals = solver.eigenvalues(); + const auto& evecs = solver.eigenvectors(); Eigen::VectorXf inv = Eigen::VectorXf::Zero(evals.size()); const float max_eval = evals.size() > 0 ? evals.cwiseAbs().maxCoeff() : 0.0f; @@ -473,15 +457,14 @@ inline Eigen::MatrixXf pseudo_inverse_symmetric(const Eigen::MatrixXf &matrix, * \return Up-Laplacian matrix. */ template -Eigen::MatrixXf compute_up_laplacian_matrix(const MeshT &mesh, int k, - int n_coefficients, - DiffusionFormWorkspace &workspace) { +Eigen::MatrixXf compute_up_laplacian_matrix(const MeshT& mesh, int k, int n_coefficients, + DiffusionFormWorkspace& workspace) { if (k < 0 || k > 2) { return Eigen::MatrixXf(); } - const auto &U = mesh.structure.eigen_basis; - const auto &mu = mesh.structure.mu; + const auto& U = mesh.structure.eigen_basis; + const auto& mu = mesh.structure.mu; const int n1 = std::max(1, std::min(n_coefficients, static_cast(U.cols()))); const int n = static_cast(mesh.num_points()); ensure_gamma_coords(mesh, workspace); @@ -516,15 +499,14 @@ Eigen::MatrixXf compute_up_laplacian_matrix(const MeshT &mesh, int k, for (int K = 0; K < Ck; ++K) { float val = 0.0f; if (k == 1) { - const auto &gJK = workspace.gamma_coords[J][K]; + const auto& gJK = workspace.gamma_coords[J][K]; const Eigen::ArrayXf term = (workspace.gamma_tmp.array() * gJK.array()) - - (workspace.gamma_mixed[K].col(i).array() * - workspace.gamma_mixed[J].col(l).array()); + (workspace.gamma_mixed[K].col(i).array() * workspace.gamma_mixed[J].col(l).array()); val = (term * mu.array()).sum(); } else { - const auto &row_combo = compounds.indices[static_cast(J)]; - const auto &col_combo = compounds.indices[static_cast(K)]; + const auto& row_combo = compounds.indices[static_cast(J)]; + const auto& col_combo = compounds.indices[static_cast(K)]; for (int p = 0; p < n; ++p) { Eigen::Matrix2f Dm; Dm(0, 0) = workspace.gamma_coords[row_combo[0]][col_combo[0]][p]; @@ -570,8 +552,7 @@ Eigen::MatrixXf compute_up_laplacian_matrix(const MeshT &mesh, int k, * \return Up-Laplacian matrix. */ template -Eigen::MatrixXf compute_up_laplacian_matrix(const MeshT &mesh, int k, - int n_coefficients) { +Eigen::MatrixXf compute_up_laplacian_matrix(const MeshT& mesh, int k, int n_coefficients) { DiffusionFormWorkspace workspace; return compute_up_laplacian_matrix(mesh, k, n_coefficients, workspace); } @@ -585,21 +566,19 @@ Eigen::MatrixXf compute_up_laplacian_matrix(const MeshT &mesh, int k, * \return Down-Laplacian matrix. */ template -Eigen::MatrixXf compute_down_laplacian_matrix( - const MeshT &mesh, int k, int n_coefficients, - DiffusionFormWorkspace &workspace) { +Eigen::MatrixXf compute_down_laplacian_matrix(const MeshT& mesh, int k, int n_coefficients, + DiffusionFormWorkspace& workspace) { if (k <= 0) { const int Ck = std::max(1, binomial_coeff(ambient_dim_3d(), std::max(0, k))); - const int n1 = std::max(1, std::min(n_coefficients, static_cast(mesh.structure.eigen_basis.cols()))); + const int n1 = + std::max(1, std::min(n_coefficients, static_cast(mesh.structure.eigen_basis.cols()))); return Eigen::MatrixXf::Zero(n1 * Ck, n1 * Ck); } const Eigen::MatrixXf D_prev = compute_weak_exterior_derivative(mesh, k - 1, n_coefficients, workspace); - const Eigen::MatrixXf G_prev = - compute_kform_gram_matrix(mesh, k - 1, n_coefficients, workspace); - const Eigen::MatrixXf G_prev_inv = - pseudo_inverse_symmetric(G_prev, 1e-5f); + const Eigen::MatrixXf G_prev = compute_kform_gram_matrix(mesh, k - 1, n_coefficients, workspace); + const Eigen::MatrixXf G_prev_inv = pseudo_inverse_symmetric(G_prev, 1e-5f); return D_prev * G_prev_inv * D_prev.transpose(); } @@ -611,8 +590,7 @@ Eigen::MatrixXf compute_down_laplacian_matrix( * \return Down-Laplacian matrix. */ template -Eigen::MatrixXf compute_down_laplacian_matrix(const MeshT &mesh, int k, - int n_coefficients) { +Eigen::MatrixXf compute_down_laplacian_matrix(const MeshT& mesh, int k, int n_coefficients) { DiffusionFormWorkspace workspace; return compute_down_laplacian_matrix(mesh, k, n_coefficients, workspace); } @@ -623,9 +601,8 @@ Eigen::MatrixXf compute_down_laplacian_matrix(const MeshT &mesh, int k, * \param down Down-Laplacian contribution. * \return Combined Hodge Laplacian. */ -inline Eigen::MatrixXf -assemble_hodge_laplacian_matrix(const Eigen::MatrixXf &up, - const Eigen::MatrixXf &down) { +inline Eigen::MatrixXf assemble_hodge_laplacian_matrix(const Eigen::MatrixXf& up, + const Eigen::MatrixXf& down) { return up + down; } @@ -637,16 +614,15 @@ assemble_hodge_laplacian_matrix(const Eigen::MatrixXf &up, * \return Pair `(eigenvalues, eigenvectors)`. */ inline std::pair -compute_form_spectrum(const Eigen::MatrixXf &laplacian, - const Eigen::MatrixXf &mass_matrix, +compute_form_spectrum(const Eigen::MatrixXf& laplacian, const Eigen::MatrixXf& mass_matrix, float rcond = 1e-5f) { Eigen::SelfAdjointEigenSolver mass_solver(mass_matrix); if (mass_solver.info() != Eigen::Success) { return std::make_pair(Eigen::VectorXf(), Eigen::MatrixXf()); } - const auto &mass_evals = mass_solver.eigenvalues(); - const auto &mass_evecs = mass_solver.eigenvectors(); + const auto& mass_evals = mass_solver.eigenvalues(); + const auto& mass_evecs = mass_solver.eigenvectors(); std::vector keep_indices; keep_indices.reserve(static_cast(mass_evals.size())); for (int i = 0; i < mass_evals.size(); ++i) { @@ -683,9 +659,8 @@ compute_form_spectrum(const Eigen::MatrixXf &laplacian, * \param max_modes Maximum number of modes to return. * \return Harmonic mode indices. */ -inline std::vector extract_harmonic_mode_indices(const Eigen::VectorXf &evals, - float tolerance = 1e-3f, - int max_modes = 3) { +inline std::vector extract_harmonic_mode_indices(const Eigen::VectorXf& evals, + float tolerance = 1e-3f, int max_modes = 3) { std::vector out; for (int i = 0; i < evals.size(); ++i) { if (std::abs(evals[i]) <= tolerance) { @@ -713,10 +688,9 @@ inline std::vector extract_harmonic_mode_indices(const Eigen::VectorXf &eva * \return Pointwise matrix (`n_points x C(k)`). */ template -Eigen::MatrixXf coefficients_to_pointwise(const MeshT &mesh, - const Eigen::VectorXf &coeffs, int k, +Eigen::MatrixXf coefficients_to_pointwise(const MeshT& mesh, const Eigen::VectorXf& coeffs, int k, int n_coefficients) { - const auto &U = mesh.structure.eigen_basis; + const auto& U = mesh.structure.eigen_basis; const int n1 = std::max(1, std::min(n_coefficients, static_cast(U.cols()))); const int Ck = std::max(1, binomial_coeff(ambient_dim_3d(), k)); if (coeffs.size() != n1 * Ck) { @@ -736,11 +710,11 @@ Eigen::MatrixXf coefficients_to_pointwise(const MeshT &mesh, * \return Flattened coefficient vector. */ template -Eigen::VectorXf project_pointwise_to_coefficients(const MeshT &mesh, - const Eigen::MatrixXf &pointwise, +Eigen::VectorXf project_pointwise_to_coefficients(const MeshT& mesh, + const Eigen::MatrixXf& pointwise, int n_coefficients) { - const auto &U = mesh.structure.eigen_basis; - const auto &mu = mesh.structure.mu; + const auto& U = mesh.structure.eigen_basis; + const auto& mu = mesh.structure.mu; const int n1 = std::max(1, std::min(n_coefficients, static_cast(U.cols()))); if (pointwise.rows() != U.rows()) { @@ -748,14 +722,12 @@ Eigen::VectorXf project_pointwise_to_coefficients(const MeshT &mesh, } const Eigen::MatrixXf U1 = U.leftCols(n1); - const Eigen::MatrixXf gram = - U1.transpose() * (U1.array().colwise() * mu.array()).matrix(); + const Eigen::MatrixXf gram = U1.transpose() * (U1.array().colwise() * mu.array()).matrix(); Eigen::LDLT solver(gram); Eigen::MatrixXf coeffs(pointwise.cols(), n1); for (int c = 0; c < pointwise.cols(); ++c) { - const Eigen::VectorXf rhs = - U1.transpose() * (pointwise.col(c).array() * mu.array()).matrix(); + const Eigen::VectorXf rhs = U1.transpose() * (pointwise.col(c).array() * mu.array()).matrix(); coeffs.row(c) = solver.solve(rhs).transpose(); } @@ -771,9 +743,8 @@ Eigen::VectorXf project_pointwise_to_coefficients(const MeshT &mesh, * \return Per-point dual vector field. */ inline std::vector -pointwise_2form_to_dual_vectors(const Eigen::MatrixXf &pointwise_2form) { - std::vector out(static_cast(pointwise_2form.rows()), - {0.0f, 0.0f, 0.0f}); +pointwise_2form_to_dual_vectors(const Eigen::MatrixXf& pointwise_2form) { + std::vector out(static_cast(pointwise_2form.rows()), {0.0f, 0.0f, 0.0f}); if (pointwise_2form.cols() < 3) { return out; } diff --git a/include/igneous/ops/diffusion/geometry.hpp b/include/igneous/ops/diffusion/geometry.hpp index 69d4de6..2b904e7 100644 --- a/include/igneous/ops/diffusion/geometry.hpp +++ b/include/igneous/ops/diffusion/geometry.hpp @@ -40,15 +40,14 @@ template struct DiffusionWorkspace { * \param coords Output coordinate vectors (`x,y,z`). */ template -void fill_coordinate_vectors(const MeshT &mesh, - std::array &coords) { +void fill_coordinate_vectors(const MeshT& mesh, std::array& coords) { const size_t n_verts = mesh.num_points(); for (int d = 0; d < 3; ++d) { coords[d].resize(static_cast(n_verts)); } if constexpr (requires { mesh.structure.immersion_coords; }) { - const auto &immersion = mesh.structure.immersion_coords; + const auto& immersion = mesh.structure.immersion_coords; if (immersion.rows() == static_cast(n_verts) && immersion.cols() >= 3) { for (size_t i = 0; i < n_verts; ++i) { const int idx = static_cast(i); @@ -75,8 +74,7 @@ void fill_coordinate_vectors(const MeshT &mesh, * \param coords Output coordinate vectors (`x,y,z`). */ template -void fill_data_coordinate_vectors(const MeshT &mesh, - std::array &coords) { +void fill_data_coordinate_vectors(const MeshT& mesh, std::array& coords) { const size_t n_verts = mesh.num_points(); for (int d = 0; d < 3; ++d) { coords[d].resize(static_cast(n_verts)); @@ -102,29 +100,28 @@ void fill_data_coordinate_vectors(const MeshT &mesh, * \param gamma_out Output vector of pointwise gamma values. */ template -void carre_du_champ(const MeshT &mesh, Eigen::Ref f, +void carre_du_champ(const MeshT& mesh, Eigen::Ref f, Eigen::Ref h, [[maybe_unused]] float bandwidth, Eigen::Ref gamma_out) { - [[maybe_unused]] const int expected_size = - static_cast(mesh.num_points()); + [[maybe_unused]] const int expected_size = static_cast(mesh.num_points()); assert(gamma_out.size() == expected_size); gamma_out.setZero(); - static_assert(requires { - mesh.structure.markov_row_offsets; - mesh.structure.markov_col_indices; - mesh.structure.markov_values; - }, - "carre_du_champ requires diffusion CSR storage."); + static_assert( + requires { + mesh.structure.markov_row_offsets; + mesh.structure.markov_col_indices; + mesh.structure.markov_values; + }, "carre_du_champ requires diffusion CSR storage."); - const auto &row_offsets = mesh.structure.markov_row_offsets; - const auto &col_indices = mesh.structure.markov_col_indices; - const auto &weights = mesh.structure.markov_values; + const auto& row_offsets = mesh.structure.markov_row_offsets; + const auto& col_indices = mesh.structure.markov_col_indices; + const auto& weights = mesh.structure.markov_values; assert(row_offsets.size() == static_cast(expected_size) + 1); - const float *f_data = f.data(); - const float *h_data = h.data(); + const float* f_data = f.data(); + const float* h_data = h.data(); Eigen::VectorXf means_f = Eigen::VectorXf::Zero(expected_size); Eigen::VectorXf means_h = Eigen::VectorXf::Zero(expected_size); const bool use_mean_centres = [&]() { @@ -138,8 +135,8 @@ void carre_du_champ(const MeshT &mesh, Eigen::Ref f, for (int i = 0; i < expected_size; ++i) { const int begin = row_offsets[static_cast(i)]; const int end = row_offsets[static_cast(i) + 1]; - const int *cols = col_indices.data() + begin; - const float *w = weights.data() + begin; + const int* cols = col_indices.data() + begin; + const float* w = weights.data() + begin; float mean_f = 0.0f; float mean_h = 0.0f; for (int idx = 0; idx < (end - begin); ++idx) { @@ -155,8 +152,8 @@ void carre_du_champ(const MeshT &mesh, Eigen::Ref f, const int begin = row_offsets[static_cast(i)]; const int end = row_offsets[static_cast(i) + 1]; const int count = end - begin; - const int *cols = col_indices.data() + begin; - const float *w = weights.data() + begin; + const int* cols = col_indices.data() + begin; + const float* w = weights.data() + begin; const float center_f = use_mean_centres ? means_f[i] : f_data[i]; const float center_h = use_mean_centres ? means_h[i] : h_data[i]; @@ -194,23 +191,22 @@ void carre_du_champ(const MeshT &mesh, Eigen::Ref f, * \param output Output scalar vector. */ template -void apply_markov_transition(const MeshT &mesh, - Eigen::Ref input, +void apply_markov_transition(const MeshT& mesh, Eigen::Ref input, Eigen::Ref output) { const int expected_size = static_cast(mesh.num_points()); assert(input.size() == expected_size); assert(output.size() == expected_size); - static_assert(requires { - mesh.structure.markov_row_offsets; - mesh.structure.markov_col_indices; - mesh.structure.markov_values; - }, - "apply_markov_transition requires diffusion CSR storage."); + static_assert( + requires { + mesh.structure.markov_row_offsets; + mesh.structure.markov_col_indices; + mesh.structure.markov_values; + }, "apply_markov_transition requires diffusion CSR storage."); - const auto &row_offsets = mesh.structure.markov_row_offsets; - const auto &col_indices = mesh.structure.markov_col_indices; - const auto &weights = mesh.structure.markov_values; + const auto& row_offsets = mesh.structure.markov_row_offsets; + const auto& col_indices = mesh.structure.markov_col_indices; + const auto& weights = mesh.structure.markov_values; assert(row_offsets.size() == static_cast(expected_size) + 1); assert(input.data() != output.data()); @@ -220,7 +216,7 @@ void apply_markov_transition(const MeshT &mesh, (core::gpu::gpu_force_enabled() || expected_size >= core::gpu::gpu_min_rows()); if (use_gpu) { if (core::gpu::apply_markov_transition( - static_cast(&mesh.structure), + static_cast(&mesh.structure), std::span(row_offsets.data(), row_offsets.size()), std::span(col_indices.data(), col_indices.size()), std::span(weights.data(), weights.size()), @@ -230,15 +226,15 @@ void apply_markov_transition(const MeshT &mesh, } } - const float *input_data = input.data(); - float *output_data = output.data(); + const float* input_data = input.data(); + float* output_data = output.data(); for (int i = 0; i < expected_size; ++i) { const int begin = row_offsets[static_cast(i)]; const int end = row_offsets[static_cast(i) + 1]; const int count = end - begin; - const int *cols = col_indices.data() + begin; - const float *w = weights.data() + begin; + const int* cols = col_indices.data() + begin; + const float* w = weights.data() + begin; float acc = 0.0f; int idx = 0; @@ -266,11 +262,9 @@ void apply_markov_transition(const MeshT &mesh, * \param workspace Scratch buffers reused across calls. */ template -void apply_markov_transition_steps(const MeshT &mesh, - Eigen::Ref input, - int steps, - Eigen::Ref output, - DiffusionWorkspace &workspace) { +void apply_markov_transition_steps(const MeshT& mesh, Eigen::Ref input, + int steps, Eigen::Ref output, + DiffusionWorkspace& workspace) { const int expected_size = static_cast(mesh.num_points()); assert(input.size() == expected_size); assert(output.size() == expected_size); @@ -285,26 +279,24 @@ void apply_markov_transition_steps(const MeshT &mesh, mesh.structure.markov_col_indices; mesh.structure.markov_values; }) { - const auto &row_offsets = mesh.structure.markov_row_offsets; - const auto &col_indices = mesh.structure.markov_col_indices; - const auto &weights = mesh.structure.markov_values; + const auto& row_offsets = mesh.structure.markov_row_offsets; + const auto& col_indices = mesh.structure.markov_col_indices; + const auto& weights = mesh.structure.markov_values; assert(row_offsets.size() == static_cast(expected_size) + 1); const long long row_step_work = static_cast(expected_size) * static_cast(steps); const bool use_gpu = core::compute_backend_from_env() == core::ComputeBackend::Gpu && - (core::gpu::gpu_force_enabled() || - expected_size >= core::gpu::gpu_min_rows() || + (core::gpu::gpu_force_enabled() || expected_size >= core::gpu::gpu_min_rows() || row_step_work >= core::gpu::gpu_min_row_steps()); if (use_gpu) { if (core::gpu::apply_markov_transition_steps( - static_cast(&mesh.structure), + static_cast(&mesh.structure), std::span(row_offsets.data(), row_offsets.size()), std::span(col_indices.data(), col_indices.size()), std::span(weights.data(), weights.size()), - std::span(input.data(), static_cast(expected_size)), - steps, + std::span(input.data(), static_cast(expected_size)), steps, std::span(output.data(), static_cast(expected_size)))) { return; } @@ -319,8 +311,8 @@ void apply_markov_transition_steps(const MeshT &mesh, workspace.ping = input; workspace.pong.resize(expected_size); - Eigen::VectorXf *src = &workspace.ping; - Eigen::VectorXf *dst = &workspace.pong; + Eigen::VectorXf* src = &workspace.ping; + Eigen::VectorXf* dst = &workspace.pong; for (int step = 0; step < steps; ++step) { apply_markov_transition(mesh, *src, *dst); std::swap(src, dst); @@ -337,10 +329,8 @@ void apply_markov_transition_steps(const MeshT &mesh, * \param output Output scalar vector. */ template -void apply_markov_transition_steps(const MeshT &mesh, - Eigen::Ref input, - int steps, - Eigen::Ref output) { +void apply_markov_transition_steps(const MeshT& mesh, Eigen::Ref input, + int steps, Eigen::Ref output) { DiffusionWorkspace workspace; apply_markov_transition_steps(mesh, input, steps, output, workspace); } @@ -353,8 +343,8 @@ void apply_markov_transition_steps(const MeshT &mesh, * \return Symmetric 1-form Gram matrix. */ template -Eigen::MatrixXf compute_1form_gram_matrix(const MeshT &mesh, float bandwidth, - GeometryWorkspace &workspace) { +Eigen::MatrixXf compute_1form_gram_matrix(const MeshT& mesh, float bandwidth, + GeometryWorkspace& workspace) { const int n_basis = mesh.structure.eigen_basis.cols(); const int n_total = n_basis * 3; @@ -374,8 +364,8 @@ Eigen::MatrixXf compute_1form_gram_matrix(const MeshT &mesh, float bandwidth, } } - const auto &U = mesh.structure.eigen_basis; - const auto &mu = mesh.structure.mu; + const auto& U = mesh.structure.eigen_basis; + const auto& mu = mesh.structure.mu; core::parallel_for_index( 0, n_basis, @@ -412,7 +402,7 @@ Eigen::MatrixXf compute_1form_gram_matrix(const MeshT &mesh, float bandwidth, * \return Symmetric 1-form Gram matrix. */ template -Eigen::MatrixXf compute_1form_gram_matrix(const MeshT &mesh, float bandwidth) { +Eigen::MatrixXf compute_1form_gram_matrix(const MeshT& mesh, float bandwidth) { GeometryWorkspace workspace; return compute_1form_gram_matrix(mesh, bandwidth, workspace); } diff --git a/include/igneous/ops/diffusion/hodge.hpp b/include/igneous/ops/diffusion/hodge.hpp index 5329eb9..bec28d4 100644 --- a/include/igneous/ops/diffusion/hodge.hpp +++ b/include/igneous/ops/diffusion/hodge.hpp @@ -46,11 +46,10 @@ template struct HodgeWorkspace { * \return Weak derivative matrix. */ template -Eigen::MatrixXf compute_weak_exterior_derivative(const MeshT &mesh, - float bandwidth, - HodgeWorkspace &workspace) { - const auto &U = mesh.structure.eigen_basis; - const auto &mu = mesh.structure.mu; +Eigen::MatrixXf compute_weak_exterior_derivative(const MeshT& mesh, float bandwidth, + HodgeWorkspace& workspace) { + const auto& U = mesh.structure.eigen_basis; + const auto& mu = mesh.structure.mu; const int n0 = U.cols(); const int n_verts = static_cast(mesh.num_points()); @@ -72,8 +71,7 @@ Eigen::MatrixXf compute_weak_exterior_derivative(const MeshT &mesh, 8); } - if (workspace.weighted_u.rows() != n_verts || - workspace.weighted_u.cols() != n0) { + if (workspace.weighted_u.rows() != n_verts || workspace.weighted_u.cols() != n0) { workspace.weighted_u.resize(n_verts, n0); } workspace.weighted_u = U.array().colwise() * mu.array(); @@ -96,8 +94,7 @@ Eigen::MatrixXf compute_weak_exterior_derivative(const MeshT &mesh, * \return Weak derivative matrix. */ template -Eigen::MatrixXf compute_weak_exterior_derivative(const MeshT &mesh, - float bandwidth) { +Eigen::MatrixXf compute_weak_exterior_derivative(const MeshT& mesh, float bandwidth) { HodgeWorkspace workspace; return compute_weak_exterior_derivative(mesh, bandwidth, workspace); } @@ -110,10 +107,10 @@ Eigen::MatrixXf compute_weak_exterior_derivative(const MeshT &mesh, * \return Curl-energy matrix. */ template -Eigen::MatrixXf compute_curl_energy_matrix(const MeshT &mesh, float bandwidth, - HodgeWorkspace &workspace) { - const auto &U = mesh.structure.eigen_basis; - const auto &mu = mesh.structure.mu; +Eigen::MatrixXf compute_curl_energy_matrix(const MeshT& mesh, float bandwidth, + HodgeWorkspace& workspace) { + const auto& U = mesh.structure.eigen_basis; + const auto& mu = mesh.structure.mu; const auto mu_arr = mu.array(); const int n0 = U.cols(); const int n_basis = 3 * n0; @@ -149,16 +146,13 @@ Eigen::MatrixXf compute_curl_energy_matrix(const MeshT &mesh, float bandwidth, Eigen::VectorXf gamma_phi_phi_local(n_verts); for (int l = k; l < n0; ++l) { - carre_du_champ(mesh, U.col(k), U.col(l), bandwidth, - gamma_phi_phi_local); + carre_du_champ(mesh, U.col(k), U.col(l), bandwidth, gamma_phi_phi_local); for (int a = 0; a < 3; ++a) { for (int b = 0; b < 3; ++b) { const float val = - (((gamma_phi_phi_local.array() * - workspace.gamma_xx[a][b].array()) - - (workspace.gamma_phi_x[k][b].array() * - workspace.gamma_phi_x[l][a].array())) * + (((gamma_phi_phi_local.array() * workspace.gamma_xx[a][b].array()) - + (workspace.gamma_phi_x[k][b].array() * workspace.gamma_phi_x[l][a].array())) * mu_arr) .sum(); @@ -185,7 +179,7 @@ Eigen::MatrixXf compute_curl_energy_matrix(const MeshT &mesh, float bandwidth, * \return Curl-energy matrix. */ template -Eigen::MatrixXf compute_curl_energy_matrix(const MeshT &mesh, float bandwidth) { +Eigen::MatrixXf compute_curl_energy_matrix(const MeshT& mesh, float bandwidth) { HodgeWorkspace workspace; return compute_curl_energy_matrix(mesh, bandwidth, workspace); } @@ -196,8 +190,8 @@ Eigen::MatrixXf compute_curl_energy_matrix(const MeshT &mesh, float bandwidth) { * \param E_up Curl-energy matrix. * \return Hodge Laplacian matrix. */ -inline Eigen::MatrixXf compute_hodge_laplacian_matrix( - const Eigen::MatrixXf &D_weak, const Eigen::MatrixXf &E_up) { +inline Eigen::MatrixXf compute_hodge_laplacian_matrix(const Eigen::MatrixXf& D_weak, + const Eigen::MatrixXf& E_up) { const Eigen::MatrixXf L_down = D_weak * D_weak.transpose(); return L_down + E_up; } @@ -210,16 +204,15 @@ inline Eigen::MatrixXf compute_hodge_laplacian_matrix( * \return Pair `(eigenvalues, eigenvectors)`. */ inline std::pair -compute_hodge_spectrum(const Eigen::MatrixXf &laplacian, - const Eigen::MatrixXf &mass_matrix, +compute_hodge_spectrum(const Eigen::MatrixXf& laplacian, const Eigen::MatrixXf& mass_matrix, float rcond = 1e-5f) { Eigen::SelfAdjointEigenSolver mass_solver(mass_matrix); if (mass_solver.info() != Eigen::Success) { return std::make_pair(Eigen::VectorXf(), Eigen::MatrixXf()); } - const auto &mass_evals = mass_solver.eigenvalues(); - const auto &mass_evecs = mass_solver.eigenvectors(); + const auto& mass_evals = mass_solver.eigenvalues(); + const auto& mass_evecs = mass_solver.eigenvectors(); std::vector keep_indices; keep_indices.reserve(static_cast(mass_evals.size())); for (int i = 0; i < mass_evals.size(); ++i) { @@ -265,14 +258,12 @@ compute_hodge_spectrum(const Eigen::MatrixXf &laplacian, * \return Angle field in `[0, 2*pi)`. */ template -Eigen::VectorXf compute_circular_coordinates(const MeshT &mesh, - const Eigen::VectorXf &alpha_coeffs, - float bandwidth, - float lambda = 1.0f, +Eigen::VectorXf compute_circular_coordinates(const MeshT& mesh, const Eigen::VectorXf& alpha_coeffs, + float bandwidth, float lambda = 1.0f, int positive_imag_mode = 0, - std::complex *selected_eval = nullptr) { - const auto &U = mesh.structure.eigen_basis; - const auto &mu = mesh.structure.mu; + std::complex* selected_eval = nullptr) { + const auto& U = mesh.structure.eigen_basis; + const auto& mu = mesh.structure.mu; const int n0 = U.cols(); const size_t n_verts = mesh.num_points(); @@ -308,10 +299,9 @@ Eigen::VectorXf compute_circular_coordinates(const MeshT &mesh, [&](int t) { Eigen::VectorXf weight_local(n_verts_i); weight_local = - mu.array() * - ((workspace.gamma_x_phi_mat[0].col(t).array() * q.col(0).array()) + - (workspace.gamma_x_phi_mat[1].col(t).array() * q.col(1).array()) + - (workspace.gamma_x_phi_mat[2].col(t).array() * q.col(2).array())); + mu.array() * ((workspace.gamma_x_phi_mat[0].col(t).array() * q.col(0).array()) + + (workspace.gamma_x_phi_mat[1].col(t).array() * q.col(1).array()) + + (workspace.gamma_x_phi_mat[2].col(t).array() * q.col(2).array())); X_op.col(t).noalias() = U_t * weight_local; }, 8); @@ -374,8 +364,7 @@ Eigen::VectorXf compute_circular_coordinates(const MeshT &mesh, } const int mode = - std::clamp(positive_imag_mode, 0, - static_cast(positive_imag_indices.size()) - 1); + std::clamp(positive_imag_mode, 0, static_cast(positive_imag_indices.size()) - 1); const int best_idx = positive_imag_indices[static_cast(mode)]; if (selected_eval != nullptr) { *selected_eval = sorted_evals[best_idx]; diff --git a/include/igneous/ops/diffusion/products.hpp b/include/igneous/ops/diffusion/products.hpp index 2212627..c3ff389 100644 --- a/include/igneous/ops/diffusion/products.hpp +++ b/include/igneous/ops/diffusion/products.hpp @@ -21,16 +21,15 @@ namespace igneous::ops::diffusion { * \return Flattened coefficient vector of the wedge product. */ template -Eigen::VectorXf compute_wedge_product_coeffs( - const MeshT &mesh, const Eigen::VectorXf &alpha_coeffs, int k1, - const Eigen::VectorXf &beta_coeffs, int k2, int n_coefficients, - DiffusionFormWorkspace &workspace) { +Eigen::VectorXf compute_wedge_product_coeffs(const MeshT& mesh, const Eigen::VectorXf& alpha_coeffs, + int k1, const Eigen::VectorXf& beta_coeffs, int k2, + int n_coefficients, + DiffusionFormWorkspace& workspace) { (void)workspace; const int d = ambient_dim_3d(); const int k_total = k1 + k2; const int n1 = - std::max(1, std::min(n_coefficients, - static_cast(mesh.structure.eigen_basis.cols()))); + std::max(1, std::min(n_coefficients, static_cast(mesh.structure.eigen_basis.cols()))); if (k_total > d) { return Eigen::VectorXf::Zero(n1); @@ -44,10 +43,8 @@ Eigen::VectorXf compute_wedge_product_coeffs( return Eigen::VectorXf(); } - const Eigen::MatrixXf alpha_pw = - coefficients_to_pointwise(mesh, alpha_coeffs, k1, n1); - const Eigen::MatrixXf beta_pw = - coefficients_to_pointwise(mesh, beta_coeffs, k2, n1); + const Eigen::MatrixXf alpha_pw = coefficients_to_pointwise(mesh, alpha_coeffs, k1, n1); + const Eigen::MatrixXf beta_pw = coefficients_to_pointwise(mesh, beta_coeffs, k2, n1); if (alpha_pw.rows() == 0 || beta_pw.rows() == 0) { return Eigen::VectorXf(); } @@ -60,8 +57,7 @@ Eigen::VectorXf compute_wedge_product_coeffs( const int left = wedge_idx.left_indices[t]; const int right = wedge_idx.right_indices[t]; const float sign = static_cast(wedge_idx.signs[t]); - wedge_pw.col(target).array() += - sign * alpha_pw.col(left).array() * beta_pw.col(right).array(); + wedge_pw.col(target).array() += sign * alpha_pw.col(left).array() * beta_pw.col(right).array(); } return project_pointwise_to_coefficients(mesh, wedge_pw, n1); @@ -78,15 +74,12 @@ Eigen::VectorXf compute_wedge_product_coeffs( * \return Flattened coefficient vector of the wedge product. */ template -Eigen::VectorXf compute_wedge_product_coeffs(const MeshT &mesh, - const Eigen::VectorXf &alpha_coeffs, - int k1, - const Eigen::VectorXf &beta_coeffs, - int k2, +Eigen::VectorXf compute_wedge_product_coeffs(const MeshT& mesh, const Eigen::VectorXf& alpha_coeffs, + int k1, const Eigen::VectorXf& beta_coeffs, int k2, int n_coefficients) { DiffusionFormWorkspace workspace; - return compute_wedge_product_coeffs(mesh, alpha_coeffs, k1, beta_coeffs, k2, - n_coefficients, workspace); + return compute_wedge_product_coeffs(mesh, alpha_coeffs, k1, beta_coeffs, k2, n_coefficients, + workspace); } /** @@ -102,13 +95,13 @@ Eigen::VectorXf compute_wedge_product_coeffs(const MeshT &mesh, * \return Dense linear operator matrix. */ template -Eigen::MatrixXf compute_wedge_operator_matrix( - const MeshT &mesh, const Eigen::VectorXf &alpha_coeffs, int k_left, - int k_right, int n_coefficients, DiffusionFormWorkspace &workspace) { +Eigen::MatrixXf compute_wedge_operator_matrix(const MeshT& mesh, + const Eigen::VectorXf& alpha_coeffs, int k_left, + int k_right, int n_coefficients, + DiffusionFormWorkspace& workspace) { const int d = ambient_dim_3d(); const int n1 = - std::max(1, std::min(n_coefficients, - static_cast(mesh.structure.eigen_basis.cols()))); + std::max(1, std::min(n_coefficients, static_cast(mesh.structure.eigen_basis.cols()))); const int in_dim = n1 * std::max(1, binomial_coeff(d, k_right)); const int out_dim = n1 * std::max(1, binomial_coeff(d, k_left + k_right)); @@ -117,8 +110,7 @@ Eigen::MatrixXf compute_wedge_operator_matrix( Eigen::VectorXf beta = Eigen::VectorXf::Zero(in_dim); beta[col] = 1.0f; const Eigen::VectorXf wedge = - compute_wedge_product_coeffs(mesh, alpha_coeffs, k_left, beta, k_right, - n1, workspace); + compute_wedge_product_coeffs(mesh, alpha_coeffs, k_left, beta, k_right, n1, workspace); if (wedge.size() == out_dim) { op.col(col) = wedge; } @@ -136,13 +128,12 @@ Eigen::MatrixXf compute_wedge_operator_matrix( * \return Dense linear operator matrix. */ template -Eigen::MatrixXf compute_wedge_operator_matrix(const MeshT &mesh, - const Eigen::VectorXf &alpha_coeffs, - int k_left, int k_right, - int n_coefficients) { +Eigen::MatrixXf compute_wedge_operator_matrix(const MeshT& mesh, + const Eigen::VectorXf& alpha_coeffs, int k_left, + int k_right, int n_coefficients) { DiffusionFormWorkspace workspace; - return compute_wedge_operator_matrix(mesh, alpha_coeffs, k_left, k_right, - n_coefficients, workspace); + return compute_wedge_operator_matrix(mesh, alpha_coeffs, k_left, k_right, n_coefficients, + workspace); } } // namespace igneous::ops::diffusion diff --git a/include/igneous/ops/diffusion/spectral.hpp b/include/igneous/ops/diffusion/spectral.hpp index 641445e..150fb45 100644 --- a/include/igneous/ops/diffusion/spectral.hpp +++ b/include/igneous/ops/diffusion/spectral.hpp @@ -22,31 +22,33 @@ class MarkovCsrMatProd { using Scalar = float; /// \brief Construct operator view over CSR arrays. - MarkovCsrMatProd(const std::vector &row_offsets, - const std::vector &col_indices, - const std::vector &values, int n) - : row_offsets_(row_offsets), col_indices_(col_indices), values_(values), - n_(n) {} + MarkovCsrMatProd(const std::vector& row_offsets, const std::vector& col_indices, + const std::vector& values, int n) + : row_offsets_(row_offsets), col_indices_(col_indices), values_(values), n_(n) {} /// \return Matrix row count. - [[nodiscard]] int rows() const { return n_; } + [[nodiscard]] int rows() const { + return n_; + } /// \return Matrix column count. - [[nodiscard]] int cols() const { return n_; } + [[nodiscard]] int cols() const { + return n_; + } /** * \brief Apply matrix-vector product. * \param x_in Input vector. * \param y_out Output vector. */ - void perform_op(const Scalar *x_in, Scalar *y_out) const { + void perform_op(const Scalar* x_in, Scalar* y_out) const { core::parallel_for_index( 0, n_, [&](int i) { const int begin = row_offsets_[static_cast(i)]; const int end = row_offsets_[static_cast(i) + 1]; const int count = end - begin; - const int *cols = col_indices_.data() + begin; - const float *vals = values_.data() + begin; + const int* cols = col_indices_.data() + begin; + const float* vals = values_.data() + begin; float acc = 0.0f; int k = 0; @@ -66,11 +68,11 @@ class MarkovCsrMatProd { private: /// \brief CSR row offsets. - const std::vector &row_offsets_; + const std::vector& row_offsets_; /// \brief CSR column indices. - const std::vector &col_indices_; + const std::vector& col_indices_; /// \brief CSR values. - const std::vector &values_; + const std::vector& values_; /// \brief Matrix dimension. int n_ = 0; }; @@ -81,27 +83,30 @@ class MarkovSymmetricCsrMatProd { using Scalar = float; /// \brief Construct normalized symmetric Markov operator. - MarkovSymmetricCsrMatProd(const std::vector &row_offsets, - const std::vector &col_indices, - const std::vector &values, - const Eigen::VectorXf &sqrt_mu, - const Eigen::VectorXf &inv_sqrt_mu, int n) - : row_offsets_(row_offsets), col_indices_(col_indices), values_(values), - sqrt_mu_(sqrt_mu), inv_sqrt_mu_(inv_sqrt_mu), n_(n) {} + MarkovSymmetricCsrMatProd(const std::vector& row_offsets, + const std::vector& col_indices, const std::vector& values, + const Eigen::VectorXf& sqrt_mu, const Eigen::VectorXf& inv_sqrt_mu, + int n) + : row_offsets_(row_offsets), col_indices_(col_indices), values_(values), sqrt_mu_(sqrt_mu), + inv_sqrt_mu_(inv_sqrt_mu), n_(n) {} /// \return Matrix row count. - [[nodiscard]] int rows() const { return n_; } + [[nodiscard]] int rows() const { + return n_; + } /// \return Matrix column count. - [[nodiscard]] int cols() const { return n_; } + [[nodiscard]] int cols() const { + return n_; + } /** * \brief Apply matrix-vector product. * \param x_in Input vector. * \param y_out Output vector. */ - void perform_op(const Scalar *x_in, Scalar *y_out) const { - const float *sqrt_mu_data = sqrt_mu_.data(); - const float *inv_sqrt_mu_data = inv_sqrt_mu_.data(); + void perform_op(const Scalar* x_in, Scalar* y_out) const { + const float* sqrt_mu_data = sqrt_mu_.data(); + const float* inv_sqrt_mu_data = inv_sqrt_mu_.data(); core::parallel_for_index( 0, n_, @@ -109,8 +114,8 @@ class MarkovSymmetricCsrMatProd { const int begin = row_offsets_[static_cast(i)]; const int end = row_offsets_[static_cast(i) + 1]; const int count = end - begin; - const int *cols = col_indices_.data() + begin; - const float *vals = values_.data() + begin; + const int* cols = col_indices_.data() + begin; + const float* vals = values_.data() + begin; float acc = 0.0f; int k = 0; @@ -135,15 +140,15 @@ class MarkovSymmetricCsrMatProd { private: /// \brief CSR row offsets. - const std::vector &row_offsets_; + const std::vector& row_offsets_; /// \brief CSR column indices. - const std::vector &col_indices_; + const std::vector& col_indices_; /// \brief CSR values. - const std::vector &values_; + const std::vector& values_; /// \brief `sqrt(mu)` scaling. - const Eigen::VectorXf &sqrt_mu_; + const Eigen::VectorXf& sqrt_mu_; /// \brief `1/sqrt(mu)` scaling. - const Eigen::VectorXf &inv_sqrt_mu_; + const Eigen::VectorXf& inv_sqrt_mu_; /// \brief Matrix dimension. int n_ = 0; }; @@ -154,31 +159,34 @@ class SymmetricKernelCsrMatProd { using Scalar = float; /// \brief Construct operator view over symmetric-kernel CSR arrays. - SymmetricKernelCsrMatProd(const std::vector &row_offsets, - const std::vector &col_indices, - const std::vector &values, int n) - : row_offsets_(row_offsets), col_indices_(col_indices), values_(values), - n_(n) {} + SymmetricKernelCsrMatProd(const std::vector& row_offsets, + const std::vector& col_indices, const std::vector& values, + int n) + : row_offsets_(row_offsets), col_indices_(col_indices), values_(values), n_(n) {} /// \return Matrix row count. - [[nodiscard]] int rows() const { return n_; } + [[nodiscard]] int rows() const { + return n_; + } /// \return Matrix column count. - [[nodiscard]] int cols() const { return n_; } + [[nodiscard]] int cols() const { + return n_; + } /** * \brief Apply matrix-vector product. * \param x_in Input vector. * \param y_out Output vector. */ - void perform_op(const Scalar *x_in, Scalar *y_out) const { + void perform_op(const Scalar* x_in, Scalar* y_out) const { core::parallel_for_index( 0, n_, [&](int i) { const int begin = row_offsets_[static_cast(i)]; const int end = row_offsets_[static_cast(i) + 1]; const int count = end - begin; - const int *cols = col_indices_.data() + begin; - const float *vals = values_.data() + begin; + const int* cols = col_indices_.data() + begin; + const float* vals = values_.data() + begin; float acc = 0.0f; int k = 0; @@ -198,11 +206,11 @@ class SymmetricKernelCsrMatProd { private: /// \brief CSR row offsets. - const std::vector &row_offsets_; + const std::vector& row_offsets_; /// \brief CSR column indices. - const std::vector &col_indices_; + const std::vector& col_indices_; /// \brief CSR values. - const std::vector &values_; + const std::vector& values_; /// \brief Matrix dimension. int n_ = 0; }; @@ -213,34 +221,37 @@ class NormalizedSymmetricKernelCsrMatProd { using Scalar = float; /// \brief Construct normalized symmetric-kernel operator. - NormalizedSymmetricKernelCsrMatProd(const std::vector &row_offsets, - const std::vector &col_indices, - const std::vector &values, - const Eigen::VectorXf &inv_sqrt_rows, - int n) + NormalizedSymmetricKernelCsrMatProd(const std::vector& row_offsets, + const std::vector& col_indices, + const std::vector& values, + const Eigen::VectorXf& inv_sqrt_rows, int n) : row_offsets_(row_offsets), col_indices_(col_indices), values_(values), inv_sqrt_rows_(inv_sqrt_rows), n_(n) {} /// \return Matrix row count. - [[nodiscard]] int rows() const { return n_; } + [[nodiscard]] int rows() const { + return n_; + } /// \return Matrix column count. - [[nodiscard]] int cols() const { return n_; } + [[nodiscard]] int cols() const { + return n_; + } /** * \brief Apply matrix-vector product. * \param x_in Input vector. * \param y_out Output vector. */ - void perform_op(const Scalar *x_in, Scalar *y_out) const { - const float *inv_sqrt_data = inv_sqrt_rows_.data(); + void perform_op(const Scalar* x_in, Scalar* y_out) const { + const float* inv_sqrt_data = inv_sqrt_rows_.data(); core::parallel_for_index( 0, n_, [&](int i) { const int begin = row_offsets_[static_cast(i)]; const int end = row_offsets_[static_cast(i) + 1]; const int count = end - begin; - const int *cols = col_indices_.data() + begin; - const float *vals = values_.data() + begin; + const int* cols = col_indices_.data() + begin; + const float* vals = values_.data() + begin; float acc = 0.0f; int k = 0; @@ -265,13 +276,13 @@ class NormalizedSymmetricKernelCsrMatProd { private: /// \brief CSR row offsets. - const std::vector &row_offsets_; + const std::vector& row_offsets_; /// \brief CSR column indices. - const std::vector &col_indices_; + const std::vector& col_indices_; /// \brief CSR values. - const std::vector &values_; + const std::vector& values_; /// \brief Inverse square-root row scaling. - const Eigen::VectorXf &inv_sqrt_rows_; + const Eigen::VectorXf& inv_sqrt_rows_; /// \brief Matrix dimension. int n_ = 0; }; @@ -281,40 +292,35 @@ class NormalizedSymmetricKernelCsrMatProd { * \param mesh Input diffusion space. `mesh.structure.eigen_basis` is overwritten. * \param n_eigenvectors Number of eigenvectors requested. */ -template -void compute_eigenbasis(MeshT &mesh, int n_eigenvectors) { +template void compute_eigenbasis(MeshT& mesh, int n_eigenvectors) { const bool verbose = std::getenv("IGNEOUS_BENCH_MODE") == nullptr; if (verbose) { - std::cout << "[Spectral] Computing top " << n_eigenvectors - << " eigenfunctions...\n"; + std::cout << "[Spectral] Computing top " << n_eigenvectors << " eigenfunctions...\n"; } - static_assert(requires { - mesh.structure.markov_row_offsets; - mesh.structure.markov_col_indices; - mesh.structure.markov_values; - }, - "compute_eigenbasis requires markov CSR arrays."); + static_assert( + requires { + mesh.structure.markov_row_offsets; + mesh.structure.markov_col_indices; + mesh.structure.markov_values; + }, "compute_eigenbasis requires markov CSR arrays."); const int n = mesh.structure.markov_row_offsets.empty() ? 0 : static_cast(mesh.structure.markov_row_offsets.size() - 1); - const auto solve_with_op = [&](auto &op) { + const auto solve_with_op = [&](auto& op) { using OpType = std::decay_t; const int full_ncv = std::min(n, std::max(2 * n_eigenvectors + 1, 20)); const bool try_compact = n_eigenvectors >= 32; - const int compact_ncv = - try_compact ? std::min(n, std::max(n_eigenvectors + 16, 20)) : full_ncv; + const int compact_ncv = try_compact ? std::min(n, std::max(n_eigenvectors + 16, 20)) : full_ncv; Spectra::GenEigsSolver eigs(op, n_eigenvectors, compact_ncv); eigs.init(); int nconv = eigs.compute(Spectra::SortRule::LargestReal); - if (try_compact && - (eigs.info() != Spectra::CompInfo::Successful || - nconv < n_eigenvectors) && + if (try_compact && (eigs.info() != Spectra::CompInfo::Successful || nconv < n_eigenvectors) && full_ncv > compact_ncv) { Spectra::GenEigsSolver fallback(op, n_eigenvectors, full_ncv); fallback.init(); @@ -324,8 +330,7 @@ void compute_eigenbasis(MeshT &mesh, int n_eigenvectors) { mesh.structure.eigen_basis = fallback.eigenvectors(nconv).real(); if (verbose) { std::cout << "[Spectral] Converged! Found " << nconv - << " eigenvectors. Basis shape: " - << mesh.structure.eigen_basis.rows() << "x" + << " eigenvectors. Basis shape: " << mesh.structure.eigen_basis.rows() << "x" << mesh.structure.eigen_basis.cols() << "\n"; } return; @@ -337,8 +342,7 @@ void compute_eigenbasis(MeshT &mesh, int n_eigenvectors) { if (verbose) { std::cout << "[Spectral] Converged! Found " << nconv - << " eigenvectors. Basis shape: " - << mesh.structure.eigen_basis.rows() << "x" + << " eigenvectors. Basis shape: " << mesh.structure.eigen_basis.rows() << "x" << mesh.structure.eigen_basis.cols() << "\n"; } return; @@ -350,8 +354,7 @@ void compute_eigenbasis(MeshT &mesh, int n_eigenvectors) { }; if (n <= 1) { - mesh.structure.eigen_basis = - Eigen::MatrixXf::Ones(std::max(1, n), std::max(1, n)); + mesh.structure.eigen_basis = Eigen::MatrixXf::Ones(std::max(1, n), std::max(1, n)); return; } @@ -361,30 +364,26 @@ void compute_eigenbasis(MeshT &mesh, int n_eigenvectors) { mesh.structure.symmetric_values; mesh.structure.symmetric_row_sums; }) { - const auto &sym_row_offsets = mesh.structure.symmetric_row_offsets; - const auto &sym_col_indices = mesh.structure.symmetric_col_indices; - const auto &sym_values = mesh.structure.symmetric_values; - const auto &row_sums = mesh.structure.symmetric_row_sums; - if (!sym_row_offsets.empty() && - static_cast(sym_row_offsets.size()) == n + 1 && + const auto& sym_row_offsets = mesh.structure.symmetric_row_offsets; + const auto& sym_col_indices = mesh.structure.symmetric_col_indices; + const auto& sym_values = mesh.structure.symmetric_values; + const auto& row_sums = mesh.structure.symmetric_row_sums; + if (!sym_row_offsets.empty() && static_cast(sym_row_offsets.size()) == n + 1 && row_sums.size() == n) { const int k_eval = std::max(1, std::min(n_eigenvectors, std::max(1, n - 1))); const int full_ncv = std::min(n, std::max(2 * k_eval + 1, 20)); const bool try_compact = k_eval >= 32; - const int compact_ncv = - try_compact ? std::min(n, std::max(k_eval + 16, 20)) : full_ncv; + const int compact_ncv = try_compact ? std::min(n, std::max(k_eval + 16, 20)) : full_ncv; Eigen::VectorXf inv_sqrt_rows(n); for (int i = 0; i < n; ++i) { inv_sqrt_rows[i] = 1.0f / std::sqrt(std::max(row_sums[i], 1e-12f)); } - NormalizedSymmetricKernelCsrMatProd op(sym_row_offsets, sym_col_indices, - sym_values, inv_sqrt_rows, n); + NormalizedSymmetricKernelCsrMatProd op(sym_row_offsets, sym_col_indices, sym_values, + inv_sqrt_rows, n); const auto solve_symmetric = [&](int ncv) -> bool { - Spectra::SymEigsSolver eigs(op, - k_eval, - ncv); + Spectra::SymEigsSolver eigs(op, k_eval, ncv); eigs.init(); const int nconv = eigs.compute(Spectra::SortRule::LargestMagn); if (eigs.info() != Spectra::CompInfo::Successful || nconv <= 0) { @@ -400,15 +399,13 @@ void compute_eigenbasis(MeshT &mesh, int n_eigenvectors) { if (verbose) { std::cout << "[Spectral] Converged! Found " << nconv - << " eigenvectors. Basis shape: " - << mesh.structure.eigen_basis.rows() << "x" + << " eigenvectors. Basis shape: " << mesh.structure.eigen_basis.rows() << "x" << mesh.structure.eigen_basis.cols() << "\n"; } return true; }; - if ((try_compact && solve_symmetric(compact_ncv)) || - solve_symmetric(full_ncv)) { + if ((try_compact && solve_symmetric(compact_ncv)) || solve_symmetric(full_ncv)) { return; } @@ -419,8 +416,7 @@ void compute_eigenbasis(MeshT &mesh, int n_eigenvectors) { } } - MarkovCsrMatProd fallback_op(mesh.structure.markov_row_offsets, - mesh.structure.markov_col_indices, + MarkovCsrMatProd fallback_op(mesh.structure.markov_row_offsets, mesh.structure.markov_col_indices, mesh.structure.markov_values, n); solve_with_op(fallback_op); } diff --git a/include/igneous/ops/transform.hpp b/include/igneous/ops/transform.hpp index 4e5a53f..a283bd3 100644 --- a/include/igneous/ops/transform.hpp +++ b/include/igneous/ops/transform.hpp @@ -13,7 +13,7 @@ namespace igneous::ops { * The largest axis extent is mapped to length `2.0`. * \param mesh Input/output space geometry. */ -template void normalize(data::Space &mesh) { +template void normalize(data::Space& mesh) { const size_t n_verts = mesh.num_points(); if (n_verts == 0) { return; diff --git a/notes/structure_refactor/journal.md b/notes/structure_refactor/journal.md index e7bff9a..3317bcd 100644 --- a/notes/structure_refactor/journal.md +++ b/notes/structure_refactor/journal.md @@ -151,3 +151,13 @@ - Added `std::cstddef` include and removed unused includes in `include/igneous/ops/diffusion/basis.hpp`. - Verification: - `make lint` now surfaces include-cleaner diagnostics from headers (including `basis.hpp`) and reports missing-direct-include issues. + +## Entry 0010 +- Timestamp: 2026-02-15 +- Structural Difference Targeted: Establish a repo-wide canonical C++ formatting baseline. +- Formatting Changes: + - Ran `make format` across `include/`, `src/`, `tests/`, and `benches/` using `.clang-format`. + - Applied style-only edits (spacing/wrapping/alignment) with no intended behavioral changes. +- Verification: + - `make format-check` -> pass. + - `ctest --test-dir build --output-on-failure` -> `14/14` passed after formatting sweep. diff --git a/src/core/gpu_stub.cpp b/src/core/gpu_stub.cpp index bca9900..f6e46e1 100644 --- a/src/core/gpu_stub.cpp +++ b/src/core/gpu_stub.cpp @@ -2,27 +2,25 @@ namespace igneous::core::gpu { -bool available() { return false; } +bool available() { + return false; +} -void invalidate_markov_cache(const void *) {} +void invalidate_markov_cache(const void*) {} -bool apply_markov_transition(const void *, std::span, - std::span, std::span, - std::span, std::span) { +bool apply_markov_transition(const void*, std::span, std::span, + std::span, std::span, std::span) { return false; } -bool apply_markov_transition_steps(const void *, std::span, - std::span, - std::span, - std::span, int, +bool apply_markov_transition_steps(const void*, std::span, std::span, + std::span, std::span, int, std::span) { return false; } -bool carre_du_champ(const void *, std::span, std::span, - std::span, std::span, - std::span, float, std::span) { +bool carre_du_champ(const void*, std::span, std::span, std::span, + std::span, std::span, float, std::span) { return false; } diff --git a/src/core/metal_diffusion.mm b/src/core/metal_diffusion.mm index 64e262c..aee0964 100644 --- a/src/core/metal_diffusion.mm +++ b/src/core/metal_diffusion.mm @@ -12,7 +12,7 @@ namespace igneous::core::gpu { namespace { -constexpr const char *kMetalDiffusionSource = R"METAL( +constexpr const char* kMetalDiffusionSource = R"METAL( #include using namespace metal; @@ -74,24 +74,24 @@ kernel void csr_carre_du_champ(device const int *row_offsets [[buffer(0)]], class MetalDiffusionRuntime { public: - static MetalDiffusionRuntime &instance() { + static MetalDiffusionRuntime& instance() { static MetalDiffusionRuntime runtime; return runtime; } - [[nodiscard]] bool is_available() const { return ready_; } + [[nodiscard]] bool is_available() const { + return ready_; + } - void invalidate_markov_cache(const void *cache_key) { + void invalidate_markov_cache(const void* cache_key) { std::lock_guard lock(cache_mutex_); csr_cache_.erase(cache_key); } - [[nodiscard]] bool apply_markov_transition(const void *cache_key, - std::span row_offsets, - std::span col_indices, - std::span weights, - std::span input, - std::span output) { + [[nodiscard]] bool + apply_markov_transition(const void* cache_key, std::span row_offsets, + std::span col_indices, std::span weights, + std::span input, std::span output) { if (!ready_ || row_offsets.size() < 2) { return false; } @@ -105,18 +105,16 @@ void invalidate_markov_cache(const void *cache_key) { id rows_buffer = nil; id cols_buffer = nil; id weights_buffer = nil; - if (!prepare_csr_buffers(cache_key, row_offsets, col_indices, weights, - rows_buffer, cols_buffer, weights_buffer)) { + if (!prepare_csr_buffers(cache_key, row_offsets, col_indices, weights, rows_buffer, cols_buffer, + weights_buffer)) { return false; } const size_t vector_bytes = row_count * sizeof(float); - id input_buffer = - [device_ newBufferWithLength:vector_bytes - options:MTLResourceStorageModeShared]; - id output_buffer = - [device_ newBufferWithLength:vector_bytes - options:MTLResourceStorageModeShared]; + id input_buffer = [device_ newBufferWithLength:vector_bytes + options:MTLResourceStorageModeShared]; + id output_buffer = [device_ newBufferWithLength:vector_bytes + options:MTLResourceStorageModeShared]; if (input_buffer == nil || output_buffer == nil) { return false; } @@ -154,10 +152,10 @@ void invalidate_markov_cache(const void *cache_key) { return true; } - [[nodiscard]] bool apply_markov_transition_steps( - const void *cache_key, std::span row_offsets, - std::span col_indices, std::span weights, - std::span input, int steps, std::span output) { + [[nodiscard]] bool + apply_markov_transition_steps(const void* cache_key, std::span row_offsets, + std::span col_indices, std::span weights, + std::span input, int steps, std::span output) { if (!ready_ || row_offsets.size() < 2 || steps <= 0) { return false; } @@ -169,25 +167,22 @@ void invalidate_markov_cache(const void *cache_key) { } if (steps == 1) { - return apply_markov_transition(cache_key, row_offsets, col_indices, weights, - input, output); + return apply_markov_transition(cache_key, row_offsets, col_indices, weights, input, output); } id rows_buffer = nil; id cols_buffer = nil; id weights_buffer = nil; - if (!prepare_csr_buffers(cache_key, row_offsets, col_indices, weights, - rows_buffer, cols_buffer, weights_buffer)) { + if (!prepare_csr_buffers(cache_key, row_offsets, col_indices, weights, rows_buffer, cols_buffer, + weights_buffer)) { return false; } const size_t vector_bytes = row_count * sizeof(float); - id buffer_a = - [device_ newBufferWithLength:vector_bytes - options:MTLResourceStorageModeShared]; - id buffer_b = - [device_ newBufferWithLength:vector_bytes - options:MTLResourceStorageModeShared]; + id buffer_a = [device_ newBufferWithLength:vector_bytes + options:MTLResourceStorageModeShared]; + id buffer_b = [device_ newBufferWithLength:vector_bytes + options:MTLResourceStorageModeShared]; if (buffer_a == nil || buffer_b == nil) { return false; } @@ -232,11 +227,9 @@ void invalidate_markov_cache(const void *cache_key) { return true; } - [[nodiscard]] bool carre_du_champ(const void *cache_key, - std::span row_offsets, + [[nodiscard]] bool carre_du_champ(const void* cache_key, std::span row_offsets, std::span col_indices, - std::span weights, - std::span f, + std::span weights, std::span f, std::span h, float inv_2t, std::span output) { if (!ready_ || row_offsets.size() < 2) { @@ -244,29 +237,26 @@ void invalidate_markov_cache(const void *cache_key) { } const size_t row_count = row_offsets.size() - 1; - if (f.size() != row_count || h.size() != row_count || - output.size() != row_count || col_indices.size() != weights.size()) { + if (f.size() != row_count || h.size() != row_count || output.size() != row_count || + col_indices.size() != weights.size()) { return false; } id rows_buffer = nil; id cols_buffer = nil; id weights_buffer = nil; - if (!prepare_csr_buffers(cache_key, row_offsets, col_indices, weights, - rows_buffer, cols_buffer, weights_buffer)) { + if (!prepare_csr_buffers(cache_key, row_offsets, col_indices, weights, rows_buffer, cols_buffer, + weights_buffer)) { return false; } const size_t vector_bytes = row_count * sizeof(float); - id f_buffer = - [device_ newBufferWithLength:vector_bytes - options:MTLResourceStorageModeShared]; - id h_buffer = - [device_ newBufferWithLength:vector_bytes - options:MTLResourceStorageModeShared]; - id output_buffer = - [device_ newBufferWithLength:vector_bytes - options:MTLResourceStorageModeShared]; + id f_buffer = [device_ newBufferWithLength:vector_bytes + options:MTLResourceStorageModeShared]; + id h_buffer = [device_ newBufferWithLength:vector_bytes + options:MTLResourceStorageModeShared]; + id output_buffer = [device_ newBufferWithLength:vector_bytes + options:MTLResourceStorageModeShared]; if (f_buffer == nil || h_buffer == nil || output_buffer == nil) { return false; } @@ -319,10 +309,9 @@ void invalidate_markov_cache(const void *cache_key) { return; } - NSError *error = nil; - NSString *source = [NSString stringWithUTF8String:kMetalDiffusionSource]; - id library = - [device_ newLibraryWithSource:source options:nil error:&error]; + NSError* error = nil; + NSString* source = [NSString stringWithUTF8String:kMetalDiffusionSource]; + id library = [device_ newLibraryWithSource:source options:nil error:&error]; if (library == nil) { return; } @@ -332,20 +321,17 @@ void invalidate_markov_cache(const void *cache_key) { return; } - matvec_pipeline_ = - [device_ newComputePipelineStateWithFunction:matvec_fn error:&error]; + matvec_pipeline_ = [device_ newComputePipelineStateWithFunction:matvec_fn error:&error]; if (matvec_pipeline_ == nil) { return; } - id carre_fn = - [library newFunctionWithName:@"csr_carre_du_champ"]; + id carre_fn = [library newFunctionWithName:@"csr_carre_du_champ"]; if (carre_fn == nil) { return; } - carre_pipeline_ = - [device_ newComputePipelineStateWithFunction:carre_fn error:&error]; + carre_pipeline_ = [device_ newComputePipelineStateWithFunction:carre_fn error:&error]; if (carre_pipeline_ == nil) { return; } @@ -353,40 +339,32 @@ void invalidate_markov_cache(const void *cache_key) { ready_ = true; } - [[nodiscard]] bool prepare_csr_buffers(const void *cache_key, - std::span row_offsets, + [[nodiscard]] bool prepare_csr_buffers(const void* cache_key, std::span row_offsets, std::span col_indices, - std::span weights, - id &rows_buffer, - id &cols_buffer, - id &weights_buffer) { - const void *key = - (cache_key != nullptr) ? cache_key - : static_cast(row_offsets.data()); + std::span weights, id& rows_buffer, + id& cols_buffer, + id& weights_buffer) { + const void* key = + (cache_key != nullptr) ? cache_key : static_cast(row_offsets.data()); const size_t row_count = row_offsets.size() - 1; const size_t nnz = weights.size(); std::lock_guard lock(cache_mutex_); - CsrBuffers &entry = csr_cache_[key]; + CsrBuffers& entry = csr_cache_[key]; - if (entry.row_count != row_count || entry.nnz != nnz || - entry.row_offsets == nil || entry.col_indices == nil || - entry.weights == nil) { + if (entry.row_count != row_count || entry.nnz != nnz || entry.row_offsets == nil || + entry.col_indices == nil || entry.weights == nil) { const size_t row_bytes = row_offsets.size() * sizeof(int); const size_t col_bytes = col_indices.size() * sizeof(int); const size_t weight_bytes = weights.size() * sizeof(float); - entry.row_offsets = - [device_ newBufferWithLength:row_bytes - options:MTLResourceStorageModeShared]; - entry.col_indices = - [device_ newBufferWithLength:col_bytes - options:MTLResourceStorageModeShared]; - entry.weights = - [device_ newBufferWithLength:weight_bytes - options:MTLResourceStorageModeShared]; - if (entry.row_offsets == nil || entry.col_indices == nil || - entry.weights == nil) { + entry.row_offsets = [device_ newBufferWithLength:row_bytes + options:MTLResourceStorageModeShared]; + entry.col_indices = [device_ newBufferWithLength:col_bytes + options:MTLResourceStorageModeShared]; + entry.weights = [device_ newBufferWithLength:weight_bytes + options:MTLResourceStorageModeShared]; + if (entry.row_offsets == nil || entry.col_indices == nil || entry.weights == nil) { csr_cache_.erase(key); return false; } @@ -405,16 +383,12 @@ void invalidate_markov_cache(const void *cache_key) { } static void dispatch_rows(id encoder, - id pipeline, - size_t row_count) { + id pipeline, size_t row_count) { const NSUInteger width = - std::max(1, std::min( - pipeline.maxTotalThreadsPerThreadgroup, 256)); + std::max(1, std::min(pipeline.maxTotalThreadsPerThreadgroup, 256)); const MTLSize threads_per_group = MTLSizeMake(width, 1, 1); - const MTLSize threads_per_grid = - MTLSizeMake(static_cast(row_count), 1, 1); - [encoder dispatchThreads:threads_per_grid - threadsPerThreadgroup:threads_per_group]; + const MTLSize threads_per_grid = MTLSizeMake(static_cast(row_count), 1, 1); + [encoder dispatchThreads:threads_per_grid threadsPerThreadgroup:threads_per_group]; } bool ready_ = false; @@ -424,47 +398,43 @@ static void dispatch_rows(id encoder, id carre_pipeline_ = nil; std::mutex cache_mutex_; - std::unordered_map csr_cache_; + std::unordered_map csr_cache_; }; } // namespace -bool available() { return MetalDiffusionRuntime::instance().is_available(); } +bool available() { + return MetalDiffusionRuntime::instance().is_available(); +} -void invalidate_markov_cache(const void *cache_key) { +void invalidate_markov_cache(const void* cache_key) { if (cache_key == nullptr) { return; } MetalDiffusionRuntime::instance().invalidate_markov_cache(cache_key); } -bool apply_markov_transition(const void *cache_key, - std::span row_offsets, - std::span col_indices, - std::span weights, - std::span input, - std::span output) { +bool apply_markov_transition(const void* cache_key, std::span row_offsets, + std::span col_indices, std::span weights, + std::span input, std::span output) { return MetalDiffusionRuntime::instance().apply_markov_transition( cache_key, row_offsets, col_indices, weights, input, output); } -bool apply_markov_transition_steps(const void *cache_key, - std::span row_offsets, - std::span col_indices, - std::span weights, +bool apply_markov_transition_steps(const void* cache_key, std::span row_offsets, + std::span col_indices, std::span weights, std::span input, int steps, std::span output) { return MetalDiffusionRuntime::instance().apply_markov_transition_steps( cache_key, row_offsets, col_indices, weights, input, steps, output); } -bool carre_du_champ(const void *cache_key, std::span row_offsets, - std::span col_indices, - std::span weights, std::span f, - std::span h, float inv_2t, +bool carre_du_champ(const void* cache_key, std::span row_offsets, + std::span col_indices, std::span weights, + std::span f, std::span h, float inv_2t, std::span output) { - return MetalDiffusionRuntime::instance().carre_du_champ( - cache_key, row_offsets, col_indices, weights, f, h, inv_2t, output); + return MetalDiffusionRuntime::instance().carre_du_champ(cache_key, row_offsets, col_indices, + weights, f, h, inv_2t, output); } } // namespace igneous::core::gpu diff --git a/src/main_diffusion.cpp b/src/main_diffusion.cpp index 46f57ea..1c2eb4c 100644 --- a/src/main_diffusion.cpp +++ b/src/main_diffusion.cpp @@ -15,7 +15,7 @@ using namespace igneous; using DiffusionMesh = data::Space; -static std::vector to_std_vector(const Eigen::VectorXf &v) { +static std::vector to_std_vector(const Eigen::VectorXf& v) { std::vector out(static_cast(v.size())); for (int i = 0; i < v.size(); ++i) { out[static_cast(i)] = v[i]; @@ -23,7 +23,7 @@ static std::vector to_std_vector(const Eigen::VectorXf &v) { return out; } -int main(int argc, char **argv) { +int main(int argc, char** argv) { if (argc < 2) { std::cout << "Usage: ./igneous-diffusion \n"; return 1; @@ -41,12 +41,11 @@ int main(int argc, char **argv) { io::load_obj(mesh, input_path); ops::normalize(mesh); - mesh.structure.build({mesh.x_span(), mesh.y_span(), - mesh.z_span(), 32}); + mesh.structure.build({mesh.x_span(), mesh.y_span(), mesh.z_span(), 32}); const int n = static_cast(mesh.num_points()); - std::cout << "Markov Chain P: " << n << "x" << n << " (" - << mesh.structure.markov_values.size() << " nnz)\n"; + std::cout << "Markov Chain P: " << n << "x" << n << " (" << mesh.structure.markov_values.size() + << " nnz)\n"; auto density_field = to_std_vector(mesh.structure.mu); if (!bench_mode) { diff --git a/src/main_diffusion_geometry.cpp b/src/main_diffusion_geometry.cpp index a015c23..9c683db 100644 --- a/src/main_diffusion_geometry.cpp +++ b/src/main_diffusion_geometry.cpp @@ -46,32 +46,31 @@ struct Config { }; static void print_usage() { - std::cout - << "Usage: ./build/igneous-diffusion-geometry [options]\n" - << " --input-csv \n" - << " --output-dir \n" - << " --n-points \n" - << " --major-radius \n" - << " --minor-radius \n" - << " --sphere-radius \n" - << " --generate-sphere\n" - << " --seed \n" - << " --n-basis \n" - << " --n-coefficients \n" - << " --k-neighbors \n" - << " --knn-bandwidth \n" - << " --bandwidth-variability \n" - << " --c \n" - << " --harmonic-tolerance \n" - << " --circular-lambda \n" - << " --circular-mode-0 \n" - << " --circular-mode-1 \n"; + std::cout << "Usage: ./build/igneous-diffusion-geometry [options]\n" + << " --input-csv \n" + << " --output-dir \n" + << " --n-points \n" + << " --major-radius \n" + << " --minor-radius \n" + << " --sphere-radius \n" + << " --generate-sphere\n" + << " --seed \n" + << " --n-basis \n" + << " --n-coefficients \n" + << " --k-neighbors \n" + << " --knn-bandwidth \n" + << " --bandwidth-variability \n" + << " --c \n" + << " --harmonic-tolerance \n" + << " --circular-lambda \n" + << " --circular-mode-0 \n" + << " --circular-mode-1 \n"; } -static bool parse_args(int argc, char **argv, Config &cfg) { +static bool parse_args(int argc, char** argv, Config& cfg) { for (int i = 1; i < argc; ++i) { const std::string arg = argv[i]; - auto require_value = [&](const char *name) -> const char * { + auto require_value = [&](const char* name) -> const char* { if (i + 1 >= argc) { std::cerr << "Missing value for " << name << "\n"; std::exit(1); @@ -164,7 +163,7 @@ static bool parse_args(int argc, char **argv, Config &cfg) { return true; } -static void generate_torus(DiffusionMesh &mesh, size_t n_points, float major_radius, +static void generate_torus(DiffusionMesh& mesh, size_t n_points, float major_radius, float minor_radius, uint32_t seed) { mesh.clear(); mesh.reserve(n_points); @@ -182,8 +181,7 @@ static void generate_torus(DiffusionMesh &mesh, size_t n_points, float major_rad } } -static void generate_sphere(DiffusionMesh &mesh, size_t n_points, float radius, - uint32_t seed) { +static void generate_sphere(DiffusionMesh& mesh, size_t n_points, float radius, uint32_t seed) { mesh.clear(); mesh.reserve(n_points); @@ -196,13 +194,12 @@ static void generate_sphere(DiffusionMesh &mesh, size_t n_points, float radius, const float theta = 2.0f * 3.14159265358979323846f * u; const float phi = std::acos(2.0f * v - 1.0f); const float sin_phi = std::sin(phi); - mesh.push_point({radius * sin_phi * std::cos(theta), - radius * sin_phi * std::sin(theta), - radius * std::cos(phi)}); + mesh.push_point({radius * sin_phi * std::cos(theta), radius * sin_phi * std::sin(theta), + radius * std::cos(phi)}); } } -static bool load_point_cloud_csv(const std::string &filename, DiffusionMesh &mesh) { +static bool load_point_cloud_csv(const std::string& filename, DiffusionMesh& mesh) { std::ifstream file(filename); if (!file.is_open()) { std::cerr << "Failed to open input CSV: " << filename << "\n"; @@ -216,7 +213,7 @@ static bool load_point_cloud_csv(const std::string &filename, DiffusionMesh &mes continue; } - for (char &c : line) { + for (char& c : line) { if (c == ',') { c = ' '; } @@ -235,7 +232,7 @@ static bool load_point_cloud_csv(const std::string &filename, DiffusionMesh &mes return mesh.num_points() > 0; } -static std::vector to_scalar_field(const Eigen::VectorXf &values) { +static std::vector to_scalar_field(const Eigen::VectorXf& values) { std::vector out(static_cast(values.size())); for (int i = 0; i < values.size(); ++i) { out[static_cast(i)] = values[i]; @@ -243,9 +240,8 @@ static std::vector to_scalar_field(const Eigen::VectorXf &values) { return out; } -static void export_vector_field(const std::string &filename, - const DiffusionMesh &mesh, - const std::vector &vectors) { +static void export_vector_field(const std::string& filename, const DiffusionMesh& mesh, + const std::vector& vectors) { std::ofstream file(filename); if (!file.is_open()) { return; @@ -266,13 +262,12 @@ static void export_vector_field(const std::string &filename, for (size_t i = 0; i < n; ++i) { const auto p = mesh.get_vec3(i); const auto v = vectors[i]; - file << p.x << " " << p.y << " " << p.z << " " << v.x << " " << v.y - << " " << v.z << "\n"; + file << p.x << " " << p.y << " " << p.z << " " << v.x << " " << v.y << " " << v.z << "\n"; } } static std::array, 3> -build_gamma_data_immersion(const DiffusionMesh &mesh) { +build_gamma_data_immersion(const DiffusionMesh& mesh) { std::array data_coords; std::array immersion_coords; ops::diffusion::fill_data_coordinate_vectors(mesh, data_coords); @@ -282,16 +277,15 @@ build_gamma_data_immersion(const DiffusionMesh &mesh) { for (int a = 0; a < 3; ++a) { for (int b = 0; b < 3; ++b) { gamma[a][b].resize(static_cast(mesh.num_points())); - ops::diffusion::carre_du_champ(mesh, data_coords[a], immersion_coords[b], 0.0f, - gamma[a][b]); + ops::diffusion::carre_du_champ(mesh, data_coords[a], immersion_coords[b], 0.0f, gamma[a][b]); } } return gamma; } static std::vector reconstruct_1form_ambient( - const DiffusionMesh &mesh, const Eigen::VectorXf &coeffs, int n_coefficients, - const std::array, 3> &gamma_data_immersion) { + const DiffusionMesh& mesh, const Eigen::VectorXf& coeffs, int n_coefficients, + const std::array, 3>& gamma_data_immersion) { const Eigen::MatrixXf pointwise = ops::diffusion::coefficients_to_pointwise(mesh, coeffs, 1, n_coefficients); std::vector field(mesh.num_points(), {0.0f, 0.0f, 0.0f}); @@ -316,8 +310,8 @@ static std::vector reconstruct_1form_ambient( } static std::vector reconstruct_2form_dual_ambient( - const DiffusionMesh &mesh, const Eigen::VectorXf &coeffs, int n_coefficients, - const std::array, 3> &gamma_data_immersion) { + const DiffusionMesh& mesh, const Eigen::VectorXf& coeffs, int n_coefficients, + const std::array, 3>& gamma_data_immersion) { const Eigen::MatrixXf pointwise = ops::diffusion::coefficients_to_pointwise(mesh, coeffs, 2, n_coefficients); std::vector field(mesh.num_points(), {0.0f, 0.0f, 0.0f}); @@ -352,7 +346,7 @@ static std::vector reconstruct_2form_dual_ambient( return field; } -static Eigen::VectorXf pad_1form_coeffs(const Eigen::VectorXf &coeffs, int n_basis, +static Eigen::VectorXf pad_1form_coeffs(const Eigen::VectorXf& coeffs, int n_basis, int n_coefficients) { Eigen::VectorXf padded = Eigen::VectorXf::Zero(n_basis * 3); const int copy_n = std::min(n_basis, n_coefficients); @@ -364,7 +358,7 @@ static Eigen::VectorXf pad_1form_coeffs(const Eigen::VectorXf &coeffs, int n_bas return padded; } -int main(int argc, char **argv) { +int main(int argc, char** argv) { Config cfg; if (!parse_args(argc, argv, cfg)) { return 0; @@ -383,19 +377,12 @@ int main(int argc, char **argv) { generate_torus(mesh, cfg.n_points, cfg.major_radius, cfg.minor_radius, cfg.seed); } - mesh.structure.build({mesh.x_span(), - mesh.y_span(), - mesh.z_span(), - cfg.k_neighbors, - cfg.knn_bandwidth, - cfg.bandwidth_variability, - cfg.c, - true}); + mesh.structure.build({mesh.x_span(), mesh.y_span(), mesh.z_span(), cfg.k_neighbors, + cfg.knn_bandwidth, cfg.bandwidth_variability, cfg.c, true}); ops::diffusion::compute_eigenbasis(mesh, cfg.n_basis); - const int n_coeff = - std::max(1, std::min(cfg.n_coefficients, - static_cast(mesh.structure.eigen_basis.cols()))); + const int n_coeff = std::max( + 1, std::min(cfg.n_coefficients, static_cast(mesh.structure.eigen_basis.cols()))); ops::diffusion::DiffusionFormWorkspace forms_ws; @@ -403,7 +390,8 @@ int main(int argc, char **argv) { const Eigen::MatrixXf G1 = ops::diffusion::compute_kform_gram_matrix(mesh, 1, n_coeff, forms_ws); const Eigen::MatrixXf down1 = ops::diffusion::compute_down_laplacian_matrix(mesh, 1, n_coeff, forms_ws); - const Eigen::MatrixXf up1 = ops::diffusion::compute_up_laplacian_matrix(mesh, 1, n_coeff, forms_ws); + const Eigen::MatrixXf up1 = + ops::diffusion::compute_up_laplacian_matrix(mesh, 1, n_coeff, forms_ws); const Eigen::MatrixXf L1 = ops::diffusion::assemble_hodge_laplacian_matrix(up1, down1); auto [evals1, evecs1] = ops::diffusion::compute_form_spectrum(L1, G1); @@ -414,8 +402,7 @@ int main(int argc, char **argv) { const auto harmonic1_idx = ops::diffusion::extract_harmonic_mode_indices(evals1, cfg.harmonic_tolerance, 3); - Eigen::MatrixXf harmonic1_coeffs(evecs1.rows(), - static_cast(harmonic1_idx.size())); + Eigen::MatrixXf harmonic1_coeffs(evecs1.rows(), static_cast(harmonic1_idx.size())); for (int c = 0; c < static_cast(harmonic1_idx.size()); ++c) { harmonic1_coeffs.col(c) = evecs1.col(harmonic1_idx[static_cast(c)]); } @@ -425,24 +412,24 @@ int main(int argc, char **argv) { std::vector> harmonic1_fields; for (int c = 0; c < harmonic1_coeffs.cols(); ++c) { harmonic1_fields.push_back( - reconstruct_1form_ambient(mesh, harmonic1_coeffs.col(c), n_coeff, - gamma_data_immersion)); + reconstruct_1form_ambient(mesh, harmonic1_coeffs.col(c), n_coeff, gamma_data_immersion)); } const int idx0 = harmonic1_idx.empty() ? 0 : harmonic1_idx[0]; const int idx1 = harmonic1_idx.size() > 1 ? harmonic1_idx[1] : idx0; const Eigen::VectorXf theta_0 = ops::diffusion::compute_circular_coordinates( - mesh, pad_1form_coeffs(evecs1.col(idx0), mesh.structure.eigen_basis.cols(), n_coeff), - 0.0f, cfg.circular_lambda, cfg.circular_mode_0, nullptr); + mesh, pad_1form_coeffs(evecs1.col(idx0), mesh.structure.eigen_basis.cols(), n_coeff), 0.0f, + cfg.circular_lambda, cfg.circular_mode_0, nullptr); const Eigen::VectorXf theta_1 = ops::diffusion::compute_circular_coordinates( - mesh, pad_1form_coeffs(evecs1.col(idx1), mesh.structure.eigen_basis.cols(), n_coeff), - 0.0f, cfg.circular_lambda, cfg.circular_mode_1, nullptr); + mesh, pad_1form_coeffs(evecs1.col(idx1), mesh.structure.eigen_basis.cols(), n_coeff), 0.0f, + cfg.circular_lambda, cfg.circular_mode_1, nullptr); // 2-forms const Eigen::MatrixXf G2 = ops::diffusion::compute_kform_gram_matrix(mesh, 2, n_coeff, forms_ws); const Eigen::MatrixXf down2 = ops::diffusion::compute_down_laplacian_matrix(mesh, 2, n_coeff, forms_ws); - const Eigen::MatrixXf up2 = ops::diffusion::compute_up_laplacian_matrix(mesh, 2, n_coeff, forms_ws); + const Eigen::MatrixXf up2 = + ops::diffusion::compute_up_laplacian_matrix(mesh, 2, n_coeff, forms_ws); const Eigen::MatrixXf L2 = ops::diffusion::assemble_hodge_laplacian_matrix(up2, down2); auto [evals2, evecs2] = ops::diffusion::compute_form_spectrum(L2, G2); @@ -453,16 +440,15 @@ int main(int argc, char **argv) { const auto harmonic2_idx = ops::diffusion::extract_harmonic_mode_indices(evals2, cfg.harmonic_tolerance, 3); - Eigen::MatrixXf harmonic2_coeffs(evecs2.rows(), - static_cast(harmonic2_idx.size())); + Eigen::MatrixXf harmonic2_coeffs(evecs2.rows(), static_cast(harmonic2_idx.size())); for (int c = 0; c < static_cast(harmonic2_idx.size()); ++c) { harmonic2_coeffs.col(c) = evecs2.col(harmonic2_idx[static_cast(c)]); } std::vector> harmonic2_fields; for (int c = 0; c < harmonic2_coeffs.cols(); ++c) { - harmonic2_fields.push_back(reconstruct_2form_dual_ambient( - mesh, harmonic2_coeffs.col(c), n_coeff, gamma_data_immersion)); + harmonic2_fields.push_back(reconstruct_2form_dual_ambient(mesh, harmonic2_coeffs.col(c), + n_coeff, gamma_data_immersion)); } // wedge(h1_0, h1_1) @@ -470,35 +456,31 @@ int main(int argc, char **argv) { if (harmonic1_coeffs.cols() >= 1) { const int rhs_col = harmonic1_coeffs.cols() >= 2 ? 1 : 0; wedge_coeffs = ops::diffusion::compute_wedge_product_coeffs( - mesh, harmonic1_coeffs.col(0), 1, harmonic1_coeffs.col(rhs_col), 1, n_coeff, - forms_ws); + mesh, harmonic1_coeffs.col(0), 1, harmonic1_coeffs.col(rhs_col), 1, n_coeff, forms_ws); } else { wedge_coeffs = Eigen::VectorXf::Zero(n_coeff * 3); } - const auto wedge_field = reconstruct_2form_dual_ambient( - mesh, wedge_coeffs, n_coeff, gamma_data_immersion); + const auto wedge_field = + reconstruct_2form_dual_ambient(mesh, wedge_coeffs, n_coeff, gamma_data_immersion); - io::export_ply_solid(mesh, to_scalar_field(theta_0), - cfg.output_dir + "/circular_theta_0.ply", 0.01); - io::export_ply_solid(mesh, to_scalar_field(theta_1), - cfg.output_dir + "/circular_theta_1.ply", 0.01); + io::export_ply_solid(mesh, to_scalar_field(theta_0), cfg.output_dir + "/circular_theta_0.ply", + 0.01); + io::export_ply_solid(mesh, to_scalar_field(theta_1), cfg.output_dir + "/circular_theta_1.ply", + 0.01); for (size_t i = 0; i < harmonic1_fields.size(); ++i) { - export_vector_field(cfg.output_dir + "/harmonic1_form_" + std::to_string(i) + - ".ply", - mesh, harmonic1_fields[i]); + export_vector_field(cfg.output_dir + "/harmonic1_form_" + std::to_string(i) + ".ply", mesh, + harmonic1_fields[i]); } for (size_t i = 0; i < harmonic2_fields.size(); ++i) { - export_vector_field(cfg.output_dir + "/harmonic2_form_" + std::to_string(i) + - ".ply", - mesh, harmonic2_fields[i]); + export_vector_field(cfg.output_dir + "/harmonic2_form_" + std::to_string(i) + ".ply", mesh, + harmonic2_fields[i]); } export_vector_field(cfg.output_dir + "/wedge_h1h1_dual.ply", mesh, wedge_field); - std::cout << "Computed form spectra and wedge outputs in: " << cfg.output_dir - << "\n"; + std::cout << "Computed form spectra and wedge outputs in: " << cfg.output_dir << "\n"; return 0; } diff --git a/src/main_hodge.cpp b/src/main_hodge.cpp index b27c6a3..c2483e5 100644 --- a/src/main_hodge.cpp +++ b/src/main_hodge.cpp @@ -60,10 +60,10 @@ static void print_usage() { << " --no-ply\n"; } -static bool parse_args(int argc, char **argv, Config &cfg) { +static bool parse_args(int argc, char** argv, Config& cfg) { for (int i = 1; i < argc; ++i) { const std::string arg = argv[i]; - auto require_value = [&](const char *name) -> const char * { + auto require_value = [&](const char* name) -> const char* { if (i + 1 >= argc) { std::cerr << "Missing value for " << name << "\n"; std::exit(1); @@ -145,7 +145,7 @@ static bool parse_args(int argc, char **argv, Config &cfg) { return true; } -static void generate_torus(DiffusionMesh &mesh, size_t n_points, float major_radius, +static void generate_torus(DiffusionMesh& mesh, size_t n_points, float major_radius, float minor_radius, uint32_t seed) { mesh.clear(); mesh.reserve(n_points); @@ -163,7 +163,7 @@ static void generate_torus(DiffusionMesh &mesh, size_t n_points, float major_rad } } -static bool load_point_cloud_csv(const std::string &filename, DiffusionMesh &mesh) { +static bool load_point_cloud_csv(const std::string& filename, DiffusionMesh& mesh) { std::ifstream file(filename); if (!file.is_open()) { std::cerr << "Failed to open input CSV: " << filename << "\n"; @@ -177,7 +177,7 @@ static bool load_point_cloud_csv(const std::string &filename, DiffusionMesh &mes continue; } - for (char &c : line) { + for (char& c : line) { if (c == ',') { c = ' '; } @@ -196,7 +196,7 @@ static bool load_point_cloud_csv(const std::string &filename, DiffusionMesh &mes return mesh.num_points() > 0; } -static void write_points_csv(const std::string &filename, const DiffusionMesh &mesh) { +static void write_points_csv(const std::string& filename, const DiffusionMesh& mesh) { std::ofstream file(filename); file << "x,y,z\n"; const size_t n = mesh.num_points(); @@ -206,8 +206,7 @@ static void write_points_csv(const std::string &filename, const DiffusionMesh &m } } -static void write_spectrum_csv(const std::string &filename, - const Eigen::VectorXf &evals) { +static void write_spectrum_csv(const std::string& filename, const Eigen::VectorXf& evals) { std::ofstream file(filename); file << "mode,lambda\n"; for (int i = 0; i < evals.size(); ++i) { @@ -215,8 +214,7 @@ static void write_spectrum_csv(const std::string &filename, } } -static void write_harmonic_coeffs_csv(const std::string &filename, - const Eigen::MatrixXf &evecs, +static void write_harmonic_coeffs_csv(const std::string& filename, const Eigen::MatrixXf& evecs, int n_forms) { std::ofstream file(filename); file << "coeff_index"; @@ -234,12 +232,11 @@ static void write_harmonic_coeffs_csv(const std::string &filename, } } -static std::vector -reconstruct_harmonic_ambient(const DiffusionMesh &mesh, - const Eigen::VectorXf &coeffs) { +static std::vector reconstruct_harmonic_ambient(const DiffusionMesh& mesh, + const Eigen::VectorXf& coeffs) { const size_t n_verts = mesh.num_points(); const int n_basis = mesh.structure.eigen_basis.cols(); - const auto &U = mesh.structure.eigen_basis; + const auto& U = mesh.structure.eigen_basis; std::array data_coords; std::array immersion_coords; @@ -251,7 +248,7 @@ reconstruct_harmonic_ambient(const DiffusionMesh &mesh, for (int b = 0; b < 3; ++b) { gamma_data_imm[a][b].resize(static_cast(n_verts)); ops::diffusion::carre_du_champ(mesh, data_coords[a], immersion_coords[b], 0.0f, - gamma_data_imm[a][b]); + gamma_data_imm[a][b]); } } @@ -266,14 +263,11 @@ reconstruct_harmonic_ambient(const DiffusionMesh &mesh, std::vector field(n_verts, {0.0f, 0.0f, 0.0f}); for (size_t i = 0; i < n_verts; ++i) { const int idx = static_cast(i); - const float vx = gamma_data_imm[0][0][idx] * q(idx, 0) + - gamma_data_imm[0][1][idx] * q(idx, 1) + + const float vx = gamma_data_imm[0][0][idx] * q(idx, 0) + gamma_data_imm[0][1][idx] * q(idx, 1) + gamma_data_imm[0][2][idx] * q(idx, 2); - const float vy = gamma_data_imm[1][0][idx] * q(idx, 0) + - gamma_data_imm[1][1][idx] * q(idx, 1) + + const float vy = gamma_data_imm[1][0][idx] * q(idx, 0) + gamma_data_imm[1][1][idx] * q(idx, 1) + gamma_data_imm[1][2][idx] * q(idx, 2); - const float vz = gamma_data_imm[2][0][idx] * q(idx, 0) + - gamma_data_imm[2][1][idx] * q(idx, 1) + + const float vz = gamma_data_imm[2][0][idx] * q(idx, 0) + gamma_data_imm[2][1][idx] * q(idx, 1) + gamma_data_imm[2][2][idx] * q(idx, 2); field[i] = {vx, vy, vz}; } @@ -281,8 +275,7 @@ reconstruct_harmonic_ambient(const DiffusionMesh &mesh, return field; } -static double orientation_score(const DiffusionMesh &mesh, - const std::vector &field) { +static double orientation_score(const DiffusionMesh& mesh, const std::vector& field) { const size_t n_verts = mesh.num_points(); if (n_verts == 0) { return 0.0; @@ -300,16 +293,15 @@ static double orientation_score(const DiffusionMesh &mesh, return accum / static_cast(n_verts); } -static bool canonicalize_form_sign(const DiffusionMesh &mesh, - Eigen::Ref coeffs, - std::vector &ambient_field) { +static bool canonicalize_form_sign(const DiffusionMesh& mesh, Eigen::Ref coeffs, + std::vector& ambient_field) { ambient_field = reconstruct_harmonic_ambient(mesh, coeffs); if (orientation_score(mesh, ambient_field) >= 0.0) { return false; } coeffs *= -1.0f; - for (auto &v : ambient_field) { + for (auto& v : ambient_field) { v.x *= -1.0f; v.y *= -1.0f; v.z *= -1.0f; @@ -317,9 +309,8 @@ static bool canonicalize_form_sign(const DiffusionMesh &mesh, return true; } -static void export_vector_field(const std::string &filename, - const DiffusionMesh &mesh, - const std::vector &vectors) { +static void export_vector_field(const std::string& filename, const DiffusionMesh& mesh, + const std::vector& vectors) { std::ofstream file(filename); const size_t n = mesh.num_points(); @@ -332,14 +323,12 @@ static void export_vector_field(const std::string &filename, for (size_t i = 0; i < n; ++i) { const auto p = mesh.get_vec3(i); const auto v = vectors[i]; - file << p.x << " " << p.y << " " << p.z << " " << v.x << " " << v.y - << " " << v.z << "\n"; + file << p.x << " " << p.y << " " << p.z << " " << v.x << " " << v.y << " " << v.z << "\n"; } } -static void write_harmonic_ambient_csv(const std::string &filename, - const DiffusionMesh &mesh, - const std::vector> &fields) { +static void write_harmonic_ambient_csv(const std::string& filename, const DiffusionMesh& mesh, + const std::vector>& fields) { std::ofstream file(filename); file << "x,y,z"; for (size_t form = 0; form < fields.size(); ++form) { @@ -361,35 +350,30 @@ static void write_harmonic_ambient_csv(const std::string &filename, } } -static void write_circular_csv(const std::string &filename, - const DiffusionMesh &mesh, - const Eigen::VectorXf &theta_0, - const Eigen::VectorXf &theta_1) { +static void write_circular_csv(const std::string& filename, const DiffusionMesh& mesh, + const Eigen::VectorXf& theta_0, const Eigen::VectorXf& theta_1) { std::ofstream file(filename); file << "x,y,z,theta_0,theta_1\n"; const size_t n = mesh.num_points(); for (size_t i = 0; i < n; ++i) { const auto p = mesh.get_vec3(i); const int idx = static_cast(i); - file << p.x << "," << p.y << "," << p.z << "," << theta_0[idx] << "," - << theta_1[idx] << "\n"; + file << p.x << "," << p.y << "," << p.z << "," << theta_0[idx] << "," << theta_1[idx] << "\n"; } } -static void write_circular_modes_csv(const std::string &filename, - std::complex eval_0, - std::complex eval_1, - int mode_0, int mode_1, +static void write_circular_modes_csv(const std::string& filename, std::complex eval_0, + std::complex eval_1, int mode_0, int mode_1, float circular_lambda) { std::ofstream file(filename); file << "name,mode,lambda,eigenvalue_real,eigenvalue_imag\n"; - file << "theta_0," << mode_0 << "," << circular_lambda << "," << eval_0.real() - << "," << eval_0.imag() << "\n"; - file << "theta_1," << mode_1 << "," << circular_lambda << "," << eval_1.real() - << "," << eval_1.imag() << "\n"; + file << "theta_0," << mode_0 << "," << circular_lambda << "," << eval_0.real() << "," + << eval_0.imag() << "\n"; + file << "theta_1," << mode_1 << "," << circular_lambda << "," << eval_1.real() << "," + << eval_1.imag() << "\n"; } -int main(int argc, char **argv) { +int main(int argc, char** argv) { Config cfg; if (!parse_args(argc, argv, cfg)) { return 0; @@ -409,14 +393,8 @@ int main(int argc, char **argv) { generate_torus(mesh, cfg.n_points, cfg.major_radius, cfg.minor_radius, cfg.seed); } - mesh.structure.build({mesh.x_span(), - mesh.y_span(), - mesh.z_span(), - cfg.k_neighbors, - cfg.knn_bandwidth, - cfg.bandwidth_variability, - cfg.c, - true}); + mesh.structure.build({mesh.x_span(), mesh.y_span(), mesh.z_span(), cfg.k_neighbors, + cfg.knn_bandwidth, cfg.bandwidth_variability, cfg.c, true}); ops::diffusion::compute_eigenbasis(mesh, cfg.n_basis); @@ -446,11 +424,9 @@ int main(int argc, char **argv) { std::complex selected_eval_0(0.0f, 0.0f); std::complex selected_eval_1(0.0f, 0.0f); const auto theta_0 = ops::diffusion::compute_circular_coordinates( - mesh, evecs.col(0), 0.0f, cfg.circular_lambda, cfg.circular_mode_0, - &selected_eval_0); + mesh, evecs.col(0), 0.0f, cfg.circular_lambda, cfg.circular_mode_0, &selected_eval_0); const auto theta_1 = ops::diffusion::compute_circular_coordinates( - mesh, evecs.col(1), 0.0f, cfg.circular_lambda, cfg.circular_mode_1, - &selected_eval_1); + mesh, evecs.col(1), 0.0f, cfg.circular_lambda, cfg.circular_mode_1, &selected_eval_1); std::cout << "HODGE SPECTRUM (first 12 modes)\n"; for (int i = 0; i < 12 && i < evals.size(); ++i) { @@ -460,16 +436,12 @@ int main(int argc, char **argv) { write_points_csv(cfg.output_dir + "/points.csv", mesh); write_spectrum_csv(cfg.output_dir + "/hodge_spectrum.csv", evals); - write_harmonic_coeffs_csv(cfg.output_dir + "/harmonic_coeffs.csv", evecs, - export_forms); - write_harmonic_ambient_csv(cfg.output_dir + "/harmonic_ambient.csv", mesh, - harmonic_fields); + write_harmonic_coeffs_csv(cfg.output_dir + "/harmonic_coeffs.csv", evecs, export_forms); + write_harmonic_ambient_csv(cfg.output_dir + "/harmonic_ambient.csv", mesh, harmonic_fields); - write_circular_csv(cfg.output_dir + "/circular_coordinates.csv", mesh, theta_0, - theta_1); - write_circular_modes_csv(cfg.output_dir + "/circular_modes.csv", selected_eval_0, - selected_eval_1, cfg.circular_mode_0, - cfg.circular_mode_1, cfg.circular_lambda); + write_circular_csv(cfg.output_dir + "/circular_coordinates.csv", mesh, theta_0, theta_1); + write_circular_modes_csv(cfg.output_dir + "/circular_modes.csv", selected_eval_0, selected_eval_1, + cfg.circular_mode_0, cfg.circular_mode_1, cfg.circular_lambda); if (export_ply) { std::vector field_0(static_cast(theta_0.size())); @@ -479,14 +451,11 @@ int main(int argc, char **argv) { field_1[static_cast(i)] = theta_1[i]; } - io::export_ply_solid(mesh, field_0, - cfg.output_dir + "/torus_angle_0.ply", 0.01); - io::export_ply_solid(mesh, field_1, - cfg.output_dir + "/torus_angle_1.ply", 0.01); + io::export_ply_solid(mesh, field_0, cfg.output_dir + "/torus_angle_0.ply", 0.01); + io::export_ply_solid(mesh, field_1, cfg.output_dir + "/torus_angle_1.ply", 0.01); for (int i = 0; i < export_forms; ++i) { - const std::string fname = - std::format("{}/harmonic_form_{}.ply", cfg.output_dir, i); + const std::string fname = std::format("{}/harmonic_form_{}.ply", cfg.output_dir, i); export_vector_field(fname, mesh, harmonic_fields[static_cast(i)]); } } diff --git a/src/main_mesh.cpp b/src/main_mesh.cpp index 3946505..e204dbd 100644 --- a/src/main_mesh.cpp +++ b/src/main_mesh.cpp @@ -10,7 +10,7 @@ using namespace igneous; using Space = data::Space; -int main(int argc, char **argv) { +int main(int argc, char** argv) { if (argc < 2) { return 1; } diff --git a/src/main_point.cpp b/src/main_point.cpp index 3d3a8b5..adeb8f9 100644 --- a/src/main_point.cpp +++ b/src/main_point.cpp @@ -12,7 +12,7 @@ using namespace igneous; using PointCloud = data::Space; using SurfaceSpace = data::Space; -int main(int argc, char **argv) { +int main(int argc, char** argv) { if (argc < 2) { std::cout << "Usage: ./test_points \n"; return 1; diff --git a/src/main_spectral.cpp b/src/main_spectral.cpp index 4289cf3..91710cd 100644 --- a/src/main_spectral.cpp +++ b/src/main_spectral.cpp @@ -17,7 +17,7 @@ using namespace igneous; using DiffusionMesh = data::Space; -int main(int argc, char **argv) { +int main(int argc, char** argv) { if (argc < 2) { return 1; } @@ -29,8 +29,7 @@ int main(int argc, char **argv) { ops::normalize(mesh); const float bandwidth = 0.005f; - mesh.structure.build({mesh.x_span(), mesh.y_span(), - mesh.z_span(), 32}); + mesh.structure.build({mesh.x_span(), mesh.y_span(), mesh.z_span(), 32}); const int n_basis = 16; ops::diffusion::compute_eigenbasis(mesh, n_basis); @@ -50,8 +49,7 @@ int main(int argc, char **argv) { const std::string out_dir = "output_spectral"; std::filesystem::create_directory(out_dir); - const int to_export = - std::min(4, static_cast(mesh.structure.eigen_basis.cols())); + const int to_export = std::min(4, static_cast(mesh.structure.eigen_basis.cols())); for (int i = 0; i < to_export; ++i) { const Eigen::VectorXf phi = mesh.structure.eigen_basis.col(i); std::vector field(static_cast(phi.size())); diff --git a/tests/test_io_meshes.cpp b/tests/test_io_meshes.cpp index cefb81a..86daff2 100644 --- a/tests/test_io_meshes.cpp +++ b/tests/test_io_meshes.cpp @@ -26,8 +26,7 @@ TEST_CASE("OBJ import is load-only and structures build explicitly") { CHECK(surface_mesh.structure.num_faces() > 0); CHECK(surface_mesh.structure.vertex_face_offsets.empty()); surface_mesh.structure.build({surface_mesh.num_points(), true}); - CHECK(surface_mesh.structure.vertex_face_offsets.size() == - surface_mesh.num_points() + 1); + CHECK(surface_mesh.structure.vertex_face_offsets.size() == surface_mesh.num_points() + 1); igneous::data::Space point_mesh; igneous::io::load_obj(point_mesh, path); @@ -41,8 +40,7 @@ TEST_CASE("OBJ import is load-only and structures build explicitly") { CHECK(diffusion_mesh.structure.markov_row_offsets.empty()); diffusion_mesh.structure.build( {diffusion_mesh.x_span(), diffusion_mesh.y_span(), diffusion_mesh.z_span(), 32}); - CHECK(diffusion_mesh.structure.markov_row_offsets.size() == - diffusion_mesh.num_points() + 1); + CHECK(diffusion_mesh.structure.markov_row_offsets.size() == diffusion_mesh.num_points() + 1); CHECK(diffusion_mesh.structure.markov_values.size() > 0); CHECK(diffusion_mesh.structure.mu.sum() == doctest::Approx(1.0f).epsilon(1e-3f)); } diff --git a/tests/test_ops_curvature_flow.cpp b/tests/test_ops_curvature_flow.cpp index afc9b53..2a8ca21 100644 --- a/tests/test_ops_curvature_flow.cpp +++ b/tests/test_ops_curvature_flow.cpp @@ -7,8 +7,7 @@ #include #include -static igneous::data::Space -make_grid(int side) { +static igneous::data::Space make_grid(int side) { using Mesh = igneous::data::Space; Mesh mesh; mesh.reserve(static_cast(side * side)); @@ -28,7 +27,8 @@ make_grid(int side) { const uint32_t i2 = static_cast((y + 1) * side + x); const uint32_t i3 = static_cast((y + 1) * side + x + 1); - mesh.structure.faces_to_vertices.insert(mesh.structure.faces_to_vertices.end(), {i0, i1, i2, i1, i3, i2}); + mesh.structure.faces_to_vertices.insert(mesh.structure.faces_to_vertices.end(), + {i0, i1, i2, i1, i3, i2}); } } @@ -63,7 +63,6 @@ TEST_CASE("Curvature and flow kernels produce finite values") { CHECK(std::isfinite(p_after.x)); CHECK(std::isfinite(p_after.y)); CHECK(std::isfinite(p_after.z)); - CHECK((p_before.x != doctest::Approx(p_after.x) || - p_before.y != doctest::Approx(p_after.y) || + CHECK((p_before.x != doctest::Approx(p_after.x) || p_before.y != doctest::Approx(p_after.y) || p_before.z != doctest::Approx(p_after.z))); } diff --git a/tests/test_ops_diffusion_basis.cpp b/tests/test_ops_diffusion_basis.cpp index 987baaa..9792aaf 100644 --- a/tests/test_ops_diffusion_basis.cpp +++ b/tests/test_ops_diffusion_basis.cpp @@ -25,7 +25,7 @@ TEST_CASE("kp1 children and signs follow Laplace expansion semantics") { CHECK(info.signs[2] == 1); for (size_t row = 0; row < info.idx_kp1.size(); ++row) { - const auto &parent = info.idx_kp1[row]; + const auto& parent = info.idx_kp1[row]; for (int r = 0; r < 3; ++r) { std::vector child; child.reserve(2); diff --git a/tests/test_ops_diffusion_forms.cpp b/tests/test_ops_diffusion_forms.cpp index 4e0cc00..372f65a 100644 --- a/tests/test_ops_diffusion_forms.cpp +++ b/tests/test_ops_diffusion_forms.cpp @@ -8,26 +8,21 @@ #include #include -static igneous::data::Space -make_torus_cloud(size_t n_points) { - using Mesh = - igneous::data::Space; +static igneous::data::Space make_torus_cloud(size_t n_points) { + using Mesh = igneous::data::Space; Mesh mesh; mesh.reserve(n_points); for (size_t i = 0; i < n_points; ++i) { const float u = static_cast(i % 32) / 32.0f * 6.283185f; - const float v = - static_cast(i / 32) / std::max(1, n_points / 32) * 6.283185f; + const float v = static_cast(i / 32) / std::max(1, n_points / 32) * 6.283185f; const float R = 2.0f; const float r = 0.8f; - mesh.push_point( - {(R + r * std::cos(v)) * std::cos(u), (R + r * std::cos(v)) * std::sin(u), - r * std::sin(v)}); + mesh.push_point({(R + r * std::cos(v)) * std::cos(u), (R + r * std::cos(v)) * std::sin(u), + r * std::sin(v)}); } - mesh.structure.build({mesh.x_span(), mesh.y_span(), - mesh.z_span(), 24}); + mesh.structure.build({mesh.x_span(), mesh.y_span(), mesh.z_span(), 24}); return mesh; } @@ -72,7 +67,8 @@ TEST_CASE("Generic diffusion form operators are finite and shape-consistent") { CHECK(std::isfinite(evals2[i])); } - const auto harmonic_idx = igneous::ops::diffusion::extract_harmonic_mode_indices(evals2, 1e-2f, 3); + const auto harmonic_idx = + igneous::ops::diffusion::extract_harmonic_mode_indices(evals2, 1e-2f, 3); CHECK(!harmonic_idx.empty()); } @@ -84,8 +80,8 @@ TEST_CASE("Up-Laplacian(2) entry matches manual Schur-determinant assembly") { igneous::ops::diffusion::DiffusionFormWorkspace ws; const auto up2 = igneous::ops::diffusion::compute_up_laplacian_matrix(mesh, 2, n_coeff, ws); - const auto &U = mesh.structure.eigen_basis; - const auto &mu = mesh.structure.mu; + const auto& U = mesh.structure.eigen_basis; + const auto& mu = mesh.structure.mu; igneous::ops::diffusion::ensure_gamma_coords(mesh, ws); igneous::ops::diffusion::ensure_gamma_mixed(mesh, n_coeff, ws); @@ -96,8 +92,8 @@ TEST_CASE("Up-Laplacian(2) entry matches manual Schur-determinant assembly") { Eigen::VectorXf gamma_phi_phi(mesh.num_points()); igneous::ops::diffusion::carre_du_champ(mesh, U.col(0), U.col(0), 0.0f, gamma_phi_phi); - const auto &row_combo = idx2[0]; - const auto &col_combo = idx2[0]; + const auto& row_combo = idx2[0]; + const auto& col_combo = idx2[0]; for (int p = 0; p < gamma_phi_phi.size(); ++p) { Eigen::Matrix2f Dm; Dm(0, 0) = ws.gamma_coords[row_combo[0]][col_combo[0]][p]; diff --git a/tests/test_ops_diffusion_wedge.cpp b/tests/test_ops_diffusion_wedge.cpp index aa542fa..b878cbe 100644 --- a/tests/test_ops_diffusion_wedge.cpp +++ b/tests/test_ops_diffusion_wedge.cpp @@ -9,10 +9,8 @@ #include #include -static igneous::data::Space -make_cloud(size_t n_points) { - using Mesh = - igneous::data::Space; +static igneous::data::Space make_cloud(size_t n_points) { + using Mesh = igneous::data::Space; Mesh mesh; mesh.reserve(n_points); @@ -21,12 +19,10 @@ make_cloud(size_t n_points) { const float a = t * 6.283185f; const float b = (t * 7.0f) * 6.283185f; mesh.push_point({(2.0f + 0.7f * std::cos(b)) * std::cos(a), - (2.0f + 0.7f * std::cos(b)) * std::sin(a), - 0.7f * std::sin(b)}); + (2.0f + 0.7f * std::cos(b)) * std::sin(a), 0.7f * std::sin(b)}); } - mesh.structure.build({mesh.x_span(), mesh.y_span(), - mesh.z_span(), 24}); + mesh.structure.build({mesh.x_span(), mesh.y_span(), mesh.z_span(), 24}); return mesh; } @@ -47,11 +43,9 @@ TEST_CASE("Wedge product for 1-forms is anti-commutative") { igneous::ops::diffusion::DiffusionFormWorkspace ws; const Eigen::VectorXf ab = - igneous::ops::diffusion::compute_wedge_product_coeffs(mesh, alpha, 1, beta, 1, - n_coeff, ws); + igneous::ops::diffusion::compute_wedge_product_coeffs(mesh, alpha, 1, beta, 1, n_coeff, ws); const Eigen::VectorXf ba = - igneous::ops::diffusion::compute_wedge_product_coeffs(mesh, beta, 1, alpha, 1, - n_coeff, ws); + igneous::ops::diffusion::compute_wedge_product_coeffs(mesh, beta, 1, alpha, 1, n_coeff, ws); REQUIRE(ab.size() == n_coeff * 3); REQUIRE(ba.size() == n_coeff * 3); @@ -80,8 +74,7 @@ TEST_CASE("Linearized wedge operator matches direct wedge product") { const Eigen::MatrixXf op = igneous::ops::diffusion::compute_wedge_operator_matrix(mesh, alpha, 1, 1, n_coeff, ws); const Eigen::VectorXf direct = - igneous::ops::diffusion::compute_wedge_product_coeffs(mesh, alpha, 1, beta, 1, - n_coeff, ws); + igneous::ops::diffusion::compute_wedge_product_coeffs(mesh, alpha, 1, beta, 1, n_coeff, ws); REQUIRE(op.cols() == beta.size()); REQUIRE(op.rows() == direct.size()); diff --git a/tests/test_ops_hodge.cpp b/tests/test_ops_hodge.cpp index cc23134..2e65814 100644 --- a/tests/test_ops_hodge.cpp +++ b/tests/test_ops_hodge.cpp @@ -8,8 +8,7 @@ #include #include -static igneous::data::Space -make_torus_cloud(size_t n_points) { +static igneous::data::Space make_torus_cloud(size_t n_points) { using Mesh = igneous::data::Space; Mesh mesh; mesh.reserve(n_points); @@ -25,8 +24,7 @@ make_torus_cloud(size_t n_points) { mesh.push_point({x, y, z}); } - mesh.structure.build({mesh.x_span(), mesh.y_span(), - mesh.z_span(), 24}); + mesh.structure.build({mesh.x_span(), mesh.y_span(), mesh.z_span(), 24}); return mesh; } @@ -59,12 +57,12 @@ TEST_CASE("Hodge operators produce finite spectrum and circular coordinates") { CHECK(std::isfinite(evals[i])); } - const auto theta_0 = igneous::ops::diffusion::compute_circular_coordinates( - mesh, evecs.col(0), 0.0f, 1.0f, 0); - const auto theta_1 = igneous::ops::diffusion::compute_circular_coordinates( - mesh, evecs.col(1), 0.0f, 1.0f, 1); + const auto theta_0 = + igneous::ops::diffusion::compute_circular_coordinates(mesh, evecs.col(0), 0.0f, 1.0f, 0); + const auto theta_1 = + igneous::ops::diffusion::compute_circular_coordinates(mesh, evecs.col(1), 0.0f, 1.0f, 1); - const auto check_theta = [&](const Eigen::VectorXf &theta) { + const auto check_theta = [&](const Eigen::VectorXf& theta) { CHECK(theta.size() == static_cast(mesh.num_points())); float mean = 0.0f; diff --git a/tests/test_ops_spectral_geometry.cpp b/tests/test_ops_spectral_geometry.cpp index 6fcd0d3..6cae618 100644 --- a/tests/test_ops_spectral_geometry.cpp +++ b/tests/test_ops_spectral_geometry.cpp @@ -18,8 +18,7 @@ make_diffusion_cloud(size_t n_points) { mesh.push_point({std::cos(t * 6.283185f), std::sin(t * 6.283185f), t}); } - mesh.structure.build( - {mesh.x_span(), mesh.y_span(), mesh.z_span(), 24}); + mesh.structure.build({mesh.x_span(), mesh.y_span(), mesh.z_span(), 24}); return mesh; } diff --git a/tests/test_structure_dec.cpp b/tests/test_structure_dec.cpp index 3bc2d26..55dc8d9 100644 --- a/tests/test_structure_dec.cpp +++ b/tests/test_structure_dec.cpp @@ -7,8 +7,7 @@ TEST_CASE("DiscreteExteriorCalculus builds face and neighbor CSR") { igneous::data::DiscreteExteriorCalculus topo; topo.faces_to_vertices = { - 0, 1, 2, - 0, 2, 3, + 0, 1, 2, 0, 2, 3, }; topo.build({4, true}); diff --git a/tests/test_structure_diffusion_geometry.cpp b/tests/test_structure_diffusion_geometry.cpp index 8bb4090..e3dc792 100644 --- a/tests/test_structure_diffusion_geometry.cpp +++ b/tests/test_structure_diffusion_geometry.cpp @@ -7,9 +7,8 @@ #include #include -static Eigen::VectorXf csr_markov_reference( - const igneous::data::DiffusionGeometry &topo, - Eigen::Ref input) { +static Eigen::VectorXf csr_markov_reference(const igneous::data::DiffusionGeometry& topo, + Eigen::Ref input) { const int n = static_cast(topo.markov_row_offsets.size()) - 1; Eigen::VectorXf output = Eigen::VectorXf::Zero(n); @@ -69,19 +68,16 @@ TEST_CASE("DiffusionGeometry produces stochastic Markov matrix and valid measure } TEST_CASE("Diffusion CSR markov step matches sparse matrix product") { - using DiffusionMesh = - igneous::data::Space; + using DiffusionMesh = igneous::data::Space; DiffusionMesh mesh; mesh.reserve(24); for (int i = 0; i < 24; ++i) { const float t = static_cast(i) / 24.0f; - mesh.push_point( - {std::cos(t * 6.283185f), std::sin(t * 6.283185f), 0.5f * t}); + mesh.push_point({std::cos(t * 6.283185f), std::sin(t * 6.283185f), 0.5f * t}); } - mesh.structure.build({mesh.x_span(), mesh.y_span(), - mesh.z_span(), 8}); + mesh.structure.build({mesh.x_span(), mesh.y_span(), mesh.z_span(), 8}); const int n = static_cast(mesh.num_points()); Eigen::VectorXf u = Eigen::VectorXf::LinSpaced(n, -1.0f, 1.0f); @@ -96,19 +92,16 @@ TEST_CASE("Diffusion CSR markov step matches sparse matrix product") { } TEST_CASE("Diffusion multi-step markov matches repeated single steps") { - using DiffusionMesh = - igneous::data::Space; + using DiffusionMesh = igneous::data::Space; DiffusionMesh mesh; mesh.reserve(24); for (int i = 0; i < 24; ++i) { const float t = static_cast(i) / 24.0f; - mesh.push_point( - {std::cos(t * 6.283185f), std::sin(t * 6.283185f), 0.5f * t}); + mesh.push_point({std::cos(t * 6.283185f), std::sin(t * 6.283185f), 0.5f * t}); } - mesh.structure.build({mesh.x_span(), mesh.y_span(), - mesh.z_span(), 8}); + mesh.structure.build({mesh.x_span(), mesh.y_span(), mesh.z_span(), 8}); const int n = static_cast(mesh.num_points()); Eigen::VectorXf u0 = Eigen::VectorXf::LinSpaced(n, -1.0f, 1.0f); From a4d13221af17bfafcab4cfd703659c5868bbfa7a Mon Sep 17 00:00:00 2001 From: Colin Roberts Date: Sun, 15 Feb 2026 08:49:11 -0700 Subject: [PATCH 6/9] fix(dev): fallback to find when ripgrep is unavailable --- notes/structure_refactor/journal.md | 10 ++++++++++ scripts/dev/format.sh | 13 ++++++++++--- scripts/dev/lint.sh | 24 +++++++++++++++++------- 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/notes/structure_refactor/journal.md b/notes/structure_refactor/journal.md index 3317bcd..acee074 100644 --- a/notes/structure_refactor/journal.md +++ b/notes/structure_refactor/journal.md @@ -161,3 +161,13 @@ - Verification: - `make format-check` -> pass. - `ctest --test-dir build --output-on-failure` -> `14/14` passed after formatting sweep. + +## Entry 0011 +- Timestamp: 2026-02-15 +- Structural Difference Targeted: Make lint/format helpers resilient when ripgrep is unavailable. +- Fixes: + - Updated `scripts/dev/format.sh` to fall back to `find` when `rg` is not installed. + - Updated `scripts/dev/lint.sh` to fall back to `find` for both translation units and header discovery. +- Verification: + - `PATH="/opt/homebrew/opt/llvm/bin:/usr/bin:/bin" /opt/homebrew/bin/bash ./scripts/dev/format.sh --check` -> pass (no ripgrep in PATH). + - `PATH="/opt/homebrew/opt/llvm/bin:/usr/bin:/bin" /opt/homebrew/bin/bash ./scripts/dev/lint.sh build` -> pass with expected lint warnings. diff --git a/scripts/dev/format.sh b/scripts/dev/format.sh index be8e2eb..ebdc0d9 100755 --- a/scripts/dev/format.sh +++ b/scripts/dev/format.sh @@ -20,10 +20,17 @@ CLANG_FORMAT_BIN="$(pick_tool clang-format clang-format-19 clang-format-18 clang exit 1 } -mapfile -t SOURCE_FILES < <( - cd "${ROOT_DIR}" && +collect_cpp_files() { + if command -v rg >/dev/null 2>&1; then rg --files include src tests benches | rg '\.(hpp|h|hh|cpp|cc|cxx|mm)$' -) + else + find include src tests benches -type f \ + \( -name '*.hpp' -o -name '*.h' -o -name '*.hh' -o -name '*.cpp' -o \ + -name '*.cc' -o -name '*.cxx' -o -name '*.mm' \) + fi +} + +mapfile -t SOURCE_FILES < <(cd "${ROOT_DIR}" && collect_cpp_files) if [[ ${#SOURCE_FILES[@]} -eq 0 ]]; then echo "No C++ source files found under include/src/tests/benches." diff --git a/scripts/dev/lint.sh b/scripts/dev/lint.sh index 9985904..43f33f6 100755 --- a/scripts/dev/lint.sh +++ b/scripts/dev/lint.sh @@ -41,20 +41,30 @@ case "${LINT_SCOPE}" in ;; esac -mapfile -t TRANSLATION_UNITS < <( - cd "${ROOT_DIR}" && +collect_translation_units() { + if command -v rg >/dev/null 2>&1; then rg --files "${SEARCH_DIRS[@]}" | rg '\.(cpp|cc|cxx)$' -) + else + find "${SEARCH_DIRS[@]}" -type f \( -name '*.cpp' -o -name '*.cc' -o -name '*.cxx' \) + fi +} + +collect_headers() { + if command -v rg >/dev/null 2>&1; then + rg --files include/igneous | rg '\.(hpp|h|hh)$' + else + find include/igneous -type f \( -name '*.hpp' -o -name '*.h' -o -name '*.hh' \) + fi +} + +mapfile -t TRANSLATION_UNITS < <(cd "${ROOT_DIR}" && collect_translation_units) if [[ ${#TRANSLATION_UNITS[@]} -eq 0 ]]; then echo "No translation units found under src/tests/benches." exit 0 fi -mapfile -t HEADER_FILES < <( - cd "${ROOT_DIR}" && - rg --files include/igneous | rg '\.(hpp|h|hh)$' -) +mapfile -t HEADER_FILES < <(cd "${ROOT_DIR}" && collect_headers) cd "${ROOT_DIR}" status=0 From 37b926da1b768971ed03b81d9ecf1352d17d7f6d Mon Sep 17 00:00:00 2001 From: Colin Roberts Date: Mon, 16 Feb 2026 06:24:09 -0700 Subject: [PATCH 7/9] fix: lint --- .github/workflows/ci.yml | 2 +- Makefile | 24 ++- README.md | 16 +- include/igneous/core/algebra.hpp | 7 +- include/igneous/core/parallel.hpp | 2 + include/igneous/data/space.hpp | 3 +- include/igneous/data/structure.hpp | 1 + .../data/structures/diffusion_geometry.hpp | 5 +- .../structures/discrete_exterior_calculus.hpp | 10 +- include/igneous/igneous.hpp | 3 + include/igneous/io/exporter.hpp | 2 + include/igneous/io/importer.hpp | 2 + include/igneous/ops/dec/curvature.hpp | 3 + include/igneous/ops/dec/flow.hpp | 3 + include/igneous/ops/diffusion/forms.hpp | 9 +- include/igneous/ops/diffusion/geometry.hpp | 5 +- include/igneous/ops/diffusion/hodge.hpp | 7 +- include/igneous/ops/diffusion/products.hpp | 3 +- include/igneous/ops/diffusion/spectral.hpp | 8 +- include/igneous/ops/transform.hpp | 1 + notes/structure_refactor/journal.md | 51 ++++++ scripts/dev/lint.sh | 169 ++++++++++++++++-- src/main_diffusion.cpp | 1 + src/main_diffusion_geometry.cpp | 3 + src/main_hodge.cpp | 1 + src/main_point.cpp | 2 + src/main_spectral.cpp | 1 + 27 files changed, 298 insertions(+), 46 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 68f62f7..51bf880 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -215,7 +215,7 @@ jobs: - name: Run clang-tidy (all) shell: bash - run: IGNEOUS_LINT_SCOPE=all ./scripts/dev/lint.sh build-lint + run: IGNEOUS_LINT_SCOPE=all IGNEOUS_LINT_HEADERS=1 IGNEOUS_LINT_JOBS=4 ./scripts/dev/lint.sh build-lint format-check: name: Format Check (clang-format) diff --git a/Makefile b/Makefile index fa1424b..c63e633 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all debug release build docs lint lint-all format format-check clean test test-all test-algebra test-structure test-ops bench bench-memory bench-geometry bench-dod bench-deep run-mesh run-diffusion run-spectral run-hodge +.PHONY: all debug release build docs lint lint-fast lint-changed lint-all lint-strict lint-headers format format-check clean test test-all test-algebra test-structure test-ops bench bench-memory bench-geometry bench-dod bench-deep run-mesh run-diffusion run-spectral run-hodge all: build @@ -14,12 +14,30 @@ build: docs: debug cmake --build build --target docs -lint: debug +lint: + @if [ ! -f build/compile_commands.json ]; then $(MAKE) debug; fi ./scripts/dev/lint.sh build -lint-all: debug +lint-fast: + @if [ ! -f build/compile_commands.json ]; then $(MAKE) debug; fi + IGNEOUS_LINT_HEADERS=0 ./scripts/dev/lint.sh build + +lint-changed: + @if [ ! -f build/compile_commands.json ]; then $(MAKE) debug; fi + IGNEOUS_LINT_CHANGED_ONLY=1 IGNEOUS_LINT_HEADERS=0 ./scripts/dev/lint.sh build + +lint-all: + @if [ ! -f build/compile_commands.json ]; then $(MAKE) debug; fi IGNEOUS_LINT_SCOPE=all ./scripts/dev/lint.sh build +lint-strict: + @if [ ! -f build/compile_commands.json ]; then $(MAKE) debug; fi + IGNEOUS_LINT_SCOPE=all IGNEOUS_LINT_HEADERS=1 ./scripts/dev/lint.sh build + +lint-headers: + @if [ ! -f build/compile_commands.json ]; then $(MAKE) debug; fi + IGNEOUS_LINT_CHANGED_ONLY=1 IGNEOUS_LINT_HEADERS=1 ./scripts/dev/lint.sh build + format: ./scripts/dev/format.sh apply diff --git a/README.md b/README.md index b247b80..3d9a8a6 100644 --- a/README.md +++ b/README.md @@ -69,17 +69,27 @@ Run locally: ```bash make lint +make lint-fast +make lint-changed make lint-all +make lint-strict make format make format-check ``` Notes: -- `make lint` requires `build/compile_commands.json` and will run `cmake --preset default-local` via `make debug` first. -- `make lint` runs a default lint pass on C++ translation units under `src/` and also checks headers under `include/igneous/` for unused/incorrect includes via `misc-include-cleaner`. -- `make lint-all` extends lint coverage to `tests/` and `benches/` as well. +- `make lint` is the default local pass: `src/` translation units plus header include-cleaner checks. +- `make lint-fast` skips header include-cleaner checks (faster iterative loop). +- `make lint-changed` checks only changed C++ translation units in git working tree (fastest local loop). +- `make lint-all` extends lint coverage to `tests/` and `benches/`. +- `make lint-strict` is full-project strict lint (similar to CI lint settings). +- `make lint` and related targets auto-run `make debug` only when `build/compile_commands.json` is missing. - Tool binaries searched in `PATH`: `clang-tidy` and `clang-format` (version-suffixed variants are supported). +- `scripts/dev/lint.sh` knobs: + - `IGNEOUS_LINT_JOBS=` controls parallel workers (default: auto, capped at 8). + - `IGNEOUS_LINT_HEADERS=0|1` toggles header include-cleaner pass. + - `IGNEOUS_LINT_CHANGED_ONLY=0|1` restricts lint to changed files in git. ## Run Examples diff --git a/include/igneous/core/algebra.hpp b/include/igneous/core/algebra.hpp index 3b592f7..3f432d7 100644 --- a/include/igneous/core/algebra.hpp +++ b/include/igneous/core/algebra.hpp @@ -1,9 +1,11 @@ #pragma once + #include #include #include +#include #include -#include +#include // NOLINT(misc-include-cleaner) namespace igneous::core { @@ -218,6 +220,8 @@ template struct AlgebraKernels { /// \brief Fixed-size multivector value type. template struct Multivector { static constexpr size_t Size = Sig::size; + // xsimd symbols are provided by xsimd's umbrella include. + // NOLINTNEXTLINE(misc-include-cleaner) alignas(xsimd::default_arch::alignment()) std::array data; /// \brief Zero-initialized multivector. @@ -297,6 +301,7 @@ template struct Multivector { }; /// \brief SIMD packet scalar used by wide multivectors. +// NOLINTNEXTLINE(misc-include-cleaner) using Packet = xsimd::batch; /// \brief Multivector whose scalar components are SIMD packets. template using WideMultivector = Multivector; diff --git a/include/igneous/core/parallel.hpp b/include/igneous/core/parallel.hpp index 1531e10..47d3fec 100644 --- a/include/igneous/core/parallel.hpp +++ b/include/igneous/core/parallel.hpp @@ -4,11 +4,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include namespace igneous::core { diff --git a/include/igneous/data/space.hpp b/include/igneous/data/space.hpp index dc98927..87273f1 100644 --- a/include/igneous/data/space.hpp +++ b/include/igneous/data/space.hpp @@ -1,13 +1,14 @@ #pragma once #include +#include #include #include #include #include #include -#include +#include // NOLINT(misc-include-cleaner) #include namespace igneous::data { diff --git a/include/igneous/data/structure.hpp b/include/igneous/data/structure.hpp index cb357f2..ce6eaa5 100644 --- a/include/igneous/data/structure.hpp +++ b/include/igneous/data/structure.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include diff --git a/include/igneous/data/structures/diffusion_geometry.hpp b/include/igneous/data/structures/diffusion_geometry.hpp index 65eaeb4..5b8bd50 100644 --- a/include/igneous/data/structures/diffusion_geometry.hpp +++ b/include/igneous/data/structures/diffusion_geometry.hpp @@ -1,16 +1,17 @@ #pragma once +#include #include -#include +#include #include #include -#include #include #include #include #include #include #include +#include #include #include diff --git a/include/igneous/data/structures/discrete_exterior_calculus.hpp b/include/igneous/data/structures/discrete_exterior_calculus.hpp index b10b8d3..ad7487d 100644 --- a/include/igneous/data/structures/discrete_exterior_calculus.hpp +++ b/include/igneous/data/structures/discrete_exterior_calculus.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -66,14 +67,13 @@ struct DiscreteExteriorCalculus { * \return Vertex index for the requested corner. */ [[nodiscard]] uint32_t get_vertex_for_face(size_t face_idx, int corner) const { - switch (corner) { - case 0: + if (corner == 0) { return face_v0[face_idx]; - case 1: + } + if (corner == 1) { return face_v1[face_idx]; - default: - return face_v2[face_idx]; } + return face_v2[face_idx]; } /** diff --git a/include/igneous/igneous.hpp b/include/igneous/igneous.hpp index 90f075b..06e90c9 100644 --- a/include/igneous/igneous.hpp +++ b/include/igneous/igneous.hpp @@ -3,6 +3,8 @@ /// \file /// \brief Umbrella header for the public igneous API. +// This header intentionally re-exports submodules as a convenience include. +// NOLINTBEGIN(misc-include-cleaner) #include #include #include @@ -24,3 +26,4 @@ #include #include #include +// NOLINTEND(misc-include-cleaner) diff --git a/include/igneous/io/exporter.hpp b/include/igneous/io/exporter.hpp index 3f02b8b..474d2e3 100644 --- a/include/igneous/io/exporter.hpp +++ b/include/igneous/io/exporter.hpp @@ -2,12 +2,14 @@ #include #include +#include #include #include #include #include #include #include +#include #include #include diff --git a/include/igneous/io/importer.hpp b/include/igneous/io/importer.hpp index e782143..8000eda 100644 --- a/include/igneous/io/importer.hpp +++ b/include/igneous/io/importer.hpp @@ -1,5 +1,7 @@ #pragma once +#include +#include #include #include #include diff --git a/include/igneous/ops/dec/curvature.hpp b/include/igneous/ops/dec/curvature.hpp index e69d40e..4b8e653 100644 --- a/include/igneous/ops/dec/curvature.hpp +++ b/include/igneous/ops/dec/curvature.hpp @@ -2,6 +2,8 @@ #include #include +#include +#include #include #include #include @@ -10,6 +12,7 @@ #include #include #include +#include namespace igneous::ops::dec { diff --git a/include/igneous/ops/dec/flow.hpp b/include/igneous/ops/dec/flow.hpp index 0f3b222..76c09fe 100644 --- a/include/igneous/ops/dec/flow.hpp +++ b/include/igneous/ops/dec/flow.hpp @@ -1,5 +1,7 @@ #pragma once +#include +#include #include #include @@ -7,6 +9,7 @@ #include #include #include +#include namespace igneous::ops::dec { diff --git a/include/igneous/ops/diffusion/forms.hpp b/include/igneous/ops/diffusion/forms.hpp index 7125d1d..ed4e27f 100644 --- a/include/igneous/ops/diffusion/forms.hpp +++ b/include/igneous/ops/diffusion/forms.hpp @@ -1,16 +1,17 @@ #pragma once +#include #include +#include +#include #include #include #include -#include -#include -#include +#include #include #include -#include +#include #include #include #include diff --git a/include/igneous/ops/diffusion/geometry.hpp b/include/igneous/ops/diffusion/geometry.hpp index 2b904e7..43e840b 100644 --- a/include/igneous/ops/diffusion/geometry.hpp +++ b/include/igneous/ops/diffusion/geometry.hpp @@ -1,16 +1,15 @@ #pragma once +#include #include -#include #include #include #include +#include #include -#include #include #include -#include namespace igneous::ops::diffusion { diff --git a/include/igneous/ops/diffusion/hodge.hpp b/include/igneous/ops/diffusion/hodge.hpp index bec28d4..4563d79 100644 --- a/include/igneous/ops/diffusion/hodge.hpp +++ b/include/igneous/ops/diffusion/hodge.hpp @@ -1,20 +1,19 @@ #pragma once +#include #include -#include +#include #include #include #include #include -#include +#include #include #include #include -#include #include #include -#include namespace igneous::ops::diffusion { diff --git a/include/igneous/ops/diffusion/products.hpp b/include/igneous/ops/diffusion/products.hpp index c3ff389..a89b0d9 100644 --- a/include/igneous/ops/diffusion/products.hpp +++ b/include/igneous/ops/diffusion/products.hpp @@ -1,8 +1,9 @@ #pragma once +#include #include #include -#include +#include #include #include diff --git a/include/igneous/ops/diffusion/spectral.hpp b/include/igneous/ops/diffusion/spectral.hpp index 150fb45..84f5058 100644 --- a/include/igneous/ops/diffusion/spectral.hpp +++ b/include/igneous/ops/diffusion/spectral.hpp @@ -1,14 +1,16 @@ #pragma once -#include +#include #include -#include #include +#include +#include +#include #include #include #include -#include #include #include +#include namespace igneous::ops::diffusion { diff --git a/include/igneous/ops/transform.hpp b/include/igneous/ops/transform.hpp index a283bd3..e69c64f 100644 --- a/include/igneous/ops/transform.hpp +++ b/include/igneous/ops/transform.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include diff --git a/notes/structure_refactor/journal.md b/notes/structure_refactor/journal.md index acee074..67c6659 100644 --- a/notes/structure_refactor/journal.md +++ b/notes/structure_refactor/journal.md @@ -171,3 +171,54 @@ - Verification: - `PATH="/opt/homebrew/opt/llvm/bin:/usr/bin:/bin" /opt/homebrew/bin/bash ./scripts/dev/format.sh --check` -> pass (no ripgrep in PATH). - `PATH="/opt/homebrew/opt/llvm/bin:/usr/bin:/bin" /opt/homebrew/bin/bash ./scripts/dev/lint.sh build` -> pass with expected lint warnings. + +## Entry 0012 +- Timestamp: 2026-02-16 +- Structural Difference Targeted: Burn down lint diagnostics to zero for the structure-first API surface. +- Fixes: + - Applied include-cleaner corrections across data/core/io/ops headers (missing direct includes + unused include removals). + - Added explicit structure includes in CLI sources that reference structure concrete types directly: + - `src/main_diffusion.cpp` + - `src/main_diffusion_geometry.cpp` + - `src/main_hodge.cpp` + - `src/main_point.cpp` + - `src/main_spectral.cpp` + - Annotated `include/igneous/igneous.hpp` as an intentional umbrella header with: + - `NOLINTBEGIN(misc-include-cleaner)` + - `NOLINTEND(misc-include-cleaner)` + - Preserved `DiffusionGeometry` discoverability for downstream code by re-exporting it from + `include/igneous/data/space.hpp` with a targeted include-cleaner suppression. + - Addressed a non-include lint diagnostic in DEC: + - rewrote `DiscreteExteriorCalculus::get_vertex_for_face(...)` branch form to avoid `bugprone-branch-clone`. + - Fixed `performance-inefficient-vector-operation` warnings by reserving output capacity before push loops in: + - `src/main_diffusion_geometry.cpp` + - Stabilized xsimd lint behavior in `include/igneous/core/algebra.hpp`: + - restored xsimd umbrella include and added targeted `NOLINTNEXTLINE(misc-include-cleaner)` for xsimd symbol lines. +- Verification: + - `make format` -> pass. + - `./scripts/dev/lint.sh build` -> pass (`0` exit, no warnings/errors). + - `make lint` -> pass (`0` exit, no warnings/errors). + - `cmake --build build --parallel` -> pass. + - `ctest --test-dir build --output-on-failure` -> `14/14` passed. + +## Entry 0013 +- Timestamp: 2026-02-16 +- Structural Difference Targeted: Reduce local/CI lint latency without reducing strict coverage options. +- Lint Pipeline Changes: + - `scripts/dev/lint.sh` now supports parallel clang-tidy workers: + - `IGNEOUS_LINT_JOBS=` (default auto, capped at 8). + - Added optional header-pass toggle: + - `IGNEOUS_LINT_HEADERS=0|1` (default `1`). + - Added changed-files-only mode: + - `IGNEOUS_LINT_CHANGED_ONLY=0|1` (default `0`). + - Added runtime lint configuration banner for visibility. +- Make Targets: + - Added `make lint-fast` (skip header include-cleaner pass). + - Added `make lint-changed` (changed translation units only). + - Added `make lint-strict` (all + headers, CI-like). + - Added `make lint-headers` (changed-files run with header checks enabled). + - Updated lint targets to only run `make debug` when `build/compile_commands.json` is missing. +- CI: + - Updated CI lint step to pin worker count (`IGNEOUS_LINT_JOBS=4`) for stable parallel runtime. +- Documentation: + - Updated `README.md` lint section with new commands and env knobs. diff --git a/scripts/dev/lint.sh b/scripts/dev/lint.sh index 43f33f6..00c0930 100755 --- a/scripts/dev/lint.sh +++ b/scripts/dev/lint.sh @@ -4,6 +4,9 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" BUILD_DIR="${1:-${ROOT_DIR}/build}" LINT_SCOPE="${IGNEOUS_LINT_SCOPE:-src}" +LINT_HEADERS="${IGNEOUS_LINT_HEADERS:-1}" +LINT_CHANGED_ONLY="${IGNEOUS_LINT_CHANGED_ONLY:-0}" +LINT_JOBS="${IGNEOUS_LINT_JOBS:-}" pick_tool() { local tool @@ -16,12 +19,50 @@ pick_tool() { return 1 } +compute_default_jobs() { + local cores=1 + if command -v nproc >/dev/null 2>&1; then + cores="$(nproc)" + elif [[ "$(uname -s)" == "Darwin" ]]; then + cores="$(sysctl -n hw.logicalcpu 2>/dev/null || echo 1)" + fi + + if [[ "${cores}" -gt 8 ]]; then + echo 8 + else + echo "${cores}" + fi +} + +validate_bool() { + local name="$1" + local value="$2" + case "${value}" in + 0 | 1) ;; + *) + echo "error: ${name} must be 0 or 1 (got '${value}')." + exit 1 + ;; + esac +} + if [[ ! -f "${BUILD_DIR}/compile_commands.json" ]]; then echo "error: ${BUILD_DIR}/compile_commands.json not found." echo "hint: run 'cmake --preset default-local' first." exit 1 fi +validate_bool "IGNEOUS_LINT_HEADERS" "${LINT_HEADERS}" +validate_bool "IGNEOUS_LINT_CHANGED_ONLY" "${LINT_CHANGED_ONLY}" + +if [[ -z "${LINT_JOBS}" ]]; then + LINT_JOBS="$(compute_default_jobs)" +fi +if ! [[ "${LINT_JOBS}" =~ ^[0-9]+$ ]] || [[ "${LINT_JOBS}" -lt 1 ]]; then + echo "error: IGNEOUS_LINT_JOBS must be a positive integer (got '${LINT_JOBS}')." + exit 1 +fi + CLANG_TIDY_BIN="$(pick_tool clang-tidy clang-tidy-19 clang-tidy-18 clang-tidy-17)" || { echo "error: clang-tidy not found in PATH." exit 1 @@ -57,14 +98,69 @@ collect_headers() { fi } -mapfile -t TRANSLATION_UNITS < <(cd "${ROOT_DIR}" && collect_translation_units) +collect_changed_paths() { + ( + cd "${ROOT_DIR}" || exit 1 + if ! command -v git >/dev/null 2>&1 || ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then + return 1 + fi + { + git diff --name-only --diff-filter=ACMRTUXB HEAD -- "${SEARCH_DIRS[@]}" include/igneous + git ls-files --others --exclude-standard -- "${SEARCH_DIRS[@]}" include/igneous + } | sort -u + ) +} -if [[ ${#TRANSLATION_UNITS[@]} -eq 0 ]]; then - echo "No translation units found under src/tests/benches." - exit 0 +filter_cpp_paths() { + while IFS= read -r path; do + case "${path}" in + *.cpp | *.cc | *.cxx) + echo "${path}" + ;; + esac + done +} + +filter_header_paths() { + while IFS= read -r path; do + case "${path}" in + include/igneous/*) + case "${path}" in + *.h | *.hh | *.hpp) + echo "${path}" + ;; + esac + ;; + esac + done +} + +TRANSLATION_UNITS=() +HEADER_FILES=() + +if [[ "${LINT_CHANGED_ONLY}" == "1" ]]; then + mapfile -t CHANGED_PATHS < <(collect_changed_paths || true) + if [[ ${#CHANGED_PATHS[@]} -gt 0 ]]; then + mapfile -t TRANSLATION_UNITS < <(printf '%s\n' "${CHANGED_PATHS[@]}" | filter_cpp_paths) + if [[ "${LINT_HEADERS}" == "1" ]]; then + mapfile -t HEADER_FILES < <(printf '%s\n' "${CHANGED_PATHS[@]}" | filter_header_paths) + fi + fi +else + mapfile -t TRANSLATION_UNITS < <(cd "${ROOT_DIR}" && collect_translation_units) + if [[ "${LINT_HEADERS}" == "1" ]]; then + mapfile -t HEADER_FILES < <(cd "${ROOT_DIR}" && collect_headers) + fi fi -mapfile -t HEADER_FILES < <(cd "${ROOT_DIR}" && collect_headers) +if [[ ${#TRANSLATION_UNITS[@]} -eq 0 && ( "${LINT_HEADERS}" != "1" || ${#HEADER_FILES[@]} -eq 0 ) ]]; then + if [[ "${LINT_CHANGED_ONLY}" == "1" ]]; then + echo "No changed C++ files found for lint." + else + echo "No translation units found under src/tests/benches." + fi + exit 0 +fi cd "${ROOT_DIR}" status=0 @@ -74,19 +170,62 @@ if [[ "$(uname -s)" == "Darwin" ]]; then EXTRA_ARGS+=(--extra-arg=-isysroot --extra-arg="${SDKROOT}") fi -for file in "${TRANSLATION_UNITS[@]}"; do - if ! "${CLANG_TIDY_BIN}" -quiet "${EXTRA_ARGS[@]}" -p "${BUILD_DIR}" "${file}" 2>&1 | - sed '/^[0-9][0-9]* warnings generated\.$/d'; then - status=1 +lint_file() { + local kind="$1" + local file="$2" + local -a cmd=("${CLANG_TIDY_BIN}" -quiet "${EXTRA_ARGS[@]}") + if [[ "${kind}" == "header" ]]; then + cmd+=(-checks='misc-include-cleaner') fi -done + cmd+=(-p "${BUILD_DIR}" "${file}") + + "${cmd[@]}" 2>&1 | sed '/^[0-9][0-9]* warnings generated\.$/d' +} + +run_parallel_lint() { + local kind="$1" + shift + local -a files=("$@") + local -a pids=() + local failed=0 + + if [[ ${#files[@]} -eq 0 ]]; then + return 0 + fi + + for file in "${files[@]}"; do + ( + lint_file "${kind}" "${file}" + ) & + pids+=("$!") -for header in "${HEADER_FILES[@]}"; do - if ! "${CLANG_TIDY_BIN}" -quiet "${EXTRA_ARGS[@]}" -checks='misc-include-cleaner' \ - -p "${BUILD_DIR}" "${header}" 2>&1 | - sed '/^[0-9][0-9]* warnings generated\.$/d'; then + if [[ ${#pids[@]} -ge ${LINT_JOBS} ]]; then + if ! wait "${pids[0]}"; then + failed=1 + fi + pids=("${pids[@]:1}") + fi + done + + for pid in "${pids[@]}"; do + if ! wait "${pid}"; then + failed=1 + fi + done + + return "${failed}" +} + +echo "Lint config: scope=${LINT_SCOPE} jobs=${LINT_JOBS} headers=${LINT_HEADERS} changed_only=${LINT_CHANGED_ONLY}" + +if ! run_parallel_lint "tu" "${TRANSLATION_UNITS[@]}"; then + status=1 +fi + +if [[ "${LINT_HEADERS}" == "1" ]]; then + if ! run_parallel_lint "header" "${HEADER_FILES[@]}"; then status=1 fi -done +fi exit "${status}" diff --git a/src/main_diffusion.cpp b/src/main_diffusion.cpp index 1c2eb4c..c258e71 100644 --- a/src/main_diffusion.cpp +++ b/src/main_diffusion.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include #include diff --git a/src/main_diffusion_geometry.cpp b/src/main_diffusion_geometry.cpp index 9c683db..65f9973 100644 --- a/src/main_diffusion_geometry.cpp +++ b/src/main_diffusion_geometry.cpp @@ -12,6 +12,7 @@ #include #include +#include #include #include #include @@ -410,6 +411,7 @@ int main(int argc, char** argv) { const auto gamma_data_immersion = build_gamma_data_immersion(mesh); std::vector> harmonic1_fields; + harmonic1_fields.reserve(static_cast(harmonic1_coeffs.cols())); for (int c = 0; c < harmonic1_coeffs.cols(); ++c) { harmonic1_fields.push_back( reconstruct_1form_ambient(mesh, harmonic1_coeffs.col(c), n_coeff, gamma_data_immersion)); @@ -446,6 +448,7 @@ int main(int argc, char** argv) { } std::vector> harmonic2_fields; + harmonic2_fields.reserve(static_cast(harmonic2_coeffs.cols())); for (int c = 0; c < harmonic2_coeffs.cols(); ++c) { harmonic2_fields.push_back(reconstruct_2form_dual_ambient(mesh, harmonic2_coeffs.col(c), n_coeff, gamma_data_immersion)); diff --git a/src/main_hodge.cpp b/src/main_hodge.cpp index c2483e5..088b29b 100644 --- a/src/main_hodge.cpp +++ b/src/main_hodge.cpp @@ -12,6 +12,7 @@ #include #include +#include #include #include #include diff --git a/src/main_point.cpp b/src/main_point.cpp index adeb8f9..b1d449c 100644 --- a/src/main_point.cpp +++ b/src/main_point.cpp @@ -1,5 +1,7 @@ #include #include +#include +#include #include #include #include diff --git a/src/main_spectral.cpp b/src/main_spectral.cpp index 91710cd..103f167 100644 --- a/src/main_spectral.cpp +++ b/src/main_spectral.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include #include From beee42529adc54ebe6ffb548aaf754dbc8458d9b Mon Sep 17 00:00:00 2001 From: Colin Roberts Date: Mon, 16 Feb 2026 06:29:14 -0700 Subject: [PATCH 8/9] fix: format --- .github/workflows/ci.yml | 8 ++++++-- README.md | 2 ++ notes/structure_refactor/journal.md | 13 +++++++++++++ scripts/dev/format.sh | 17 +++++++++++++---- 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 51bf880..f1f198e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -229,11 +229,15 @@ jobs: shell: bash run: | sudo apt-get update - sudo apt-get install -y clang-format ripgrep + sudo apt-get install -y python3-pip ripgrep + python3 -m pip install --user clang-format==20.1.3 - name: Run clang-format check shell: bash - run: ./scripts/dev/format.sh --check + run: | + export IGNEOUS_CLANG_FORMAT_BIN="$HOME/.local/bin/clang-format" + "$IGNEOUS_CLANG_FORMAT_BIN" --version + ./scripts/dev/format.sh --check docs: name: API Docs (doxygen) diff --git a/README.md b/README.md index 3d9a8a6..04ab34b 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,8 @@ Notes: - `IGNEOUS_LINT_JOBS=` controls parallel workers (default: auto, capped at 8). - `IGNEOUS_LINT_HEADERS=0|1` toggles header include-cleaner pass. - `IGNEOUS_LINT_CHANGED_ONLY=0|1` restricts lint to changed files in git. +- `scripts/dev/format.sh` knob: + - `IGNEOUS_CLANG_FORMAT_BIN=` selects an explicit formatter binary (useful to match CI version exactly). ## Run Examples diff --git a/notes/structure_refactor/journal.md b/notes/structure_refactor/journal.md index 67c6659..64f36c7 100644 --- a/notes/structure_refactor/journal.md +++ b/notes/structure_refactor/journal.md @@ -222,3 +222,16 @@ - Updated CI lint step to pin worker count (`IGNEOUS_LINT_JOBS=4`) for stable parallel runtime. - Documentation: - Updated `README.md` lint section with new commands and env knobs. + +## Entry 0014 +- Timestamp: 2026-02-16 +- Structural Difference Targeted: Eliminate formatter drift between local and CI environments. +- Problem: + - CI `format-check` flagged `src/main_hodge.cpp` while local checks passed, indicating formatter version skew. +- Fixes: + - Added formatter binary override support in `scripts/dev/format.sh`: + - `IGNEOUS_CLANG_FORMAT_BIN=` + - Updated formatter binary discovery order to prefer explicit versioned binaries (`clang-format-21..17`) before generic `clang-format`. + - Pinned CI format job to Python-packaged `clang-format==20.1.3` and invoked format check through: + - `IGNEOUS_CLANG_FORMAT_BIN="$HOME/.local/bin/clang-format"` + - Updated `README.md` with formatter override usage note. diff --git a/scripts/dev/format.sh b/scripts/dev/format.sh index ebdc0d9..a14aa17 100755 --- a/scripts/dev/format.sh +++ b/scripts/dev/format.sh @@ -3,6 +3,7 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" MODE="${1:-apply}" +CLANG_FORMAT_BIN="${IGNEOUS_CLANG_FORMAT_BIN:-}" pick_tool() { local tool @@ -15,10 +16,18 @@ pick_tool() { return 1 } -CLANG_FORMAT_BIN="$(pick_tool clang-format clang-format-19 clang-format-18 clang-format-17)" || { - echo "error: clang-format not found in PATH." - exit 1 -} +if [[ -n "${CLANG_FORMAT_BIN}" ]]; then + if ! command -v "${CLANG_FORMAT_BIN}" >/dev/null 2>&1; then + echo "error: IGNEOUS_CLANG_FORMAT_BIN='${CLANG_FORMAT_BIN}' not found in PATH." + exit 1 + fi + CLANG_FORMAT_BIN="$(command -v "${CLANG_FORMAT_BIN}")" +else + CLANG_FORMAT_BIN="$(pick_tool clang-format-21 clang-format-20 clang-format-19 clang-format-18 clang-format-17 clang-format)" || { + echo "error: clang-format not found in PATH." + exit 1 + } +fi collect_cpp_files() { if command -v rg >/dev/null 2>&1; then From f21b21e01656c9296eafdc889aeda8872bea78b8 Mon Sep 17 00:00:00 2001 From: Colin Roberts Date: Mon, 16 Feb 2026 10:31:41 -0700 Subject: [PATCH 9/9] fix: lint --- notes/structure_refactor/journal.md | 16 ++++++ scripts/dev/lint.sh | 86 ++++++++++++++++++++++++++++- 2 files changed, 101 insertions(+), 1 deletion(-) diff --git a/notes/structure_refactor/journal.md b/notes/structure_refactor/journal.md index 64f36c7..9d27c45 100644 --- a/notes/structure_refactor/journal.md +++ b/notes/structure_refactor/journal.md @@ -235,3 +235,19 @@ - Pinned CI format job to Python-packaged `clang-format==20.1.3` and invoked format check through: - `IGNEOUS_CLANG_FORMAT_BIN="$HOME/.local/bin/clang-format"` - Updated `README.md` with formatter override usage note. + +## Entry 0015 +- Timestamp: 2026-02-16 +- Structural Difference Targeted: Make CI header lint robust when third-party headers are only available via vcpkg include paths. +- Problem: + - CI lint failed on header pass with: + - `include/igneous/core/algebra.hpp: 'xsimd/xsimd.hpp' file not found` + - Cause: direct header linting for `misc-include-cleaner` does not always inherit compile flags for non-translation-unit files. +- Fixes: + - Extended `scripts/dev/lint.sh` to extract header compile flags from `compile_commands.json` (preferring entries with vcpkg/system include flags). + - Header lint now applies extracted `-I`, `-isystem`, `-D`, `-U`, and `-std` flags via `--extra-arg` for each header invocation. + - Kept translation-unit lint behavior unchanged except for shared argument plumbing. +- Verification: + - `bash -n scripts/dev/lint.sh` -> pass. + - `make lint-strict` -> pass. + - `./scripts/dev/lint.sh build` -> pass (`0` exit, no warnings/errors). diff --git a/scripts/dev/lint.sh b/scripts/dev/lint.sh index 00c0930..1508e7a 100755 --- a/scripts/dev/lint.sh +++ b/scripts/dev/lint.sh @@ -98,6 +98,81 @@ collect_headers() { fi } +extract_header_compile_args() { + python3 - "$1" <<'PY' +import json +import shlex +import sys + +path = sys.argv[1] +try: + with open(path, "r", encoding="utf-8") as f: + db = json.load(f) +except Exception: + sys.exit(0) + +def command_tokens(entry): + if "arguments" in entry and isinstance(entry["arguments"], list): + return entry["arguments"] + if "command" in entry and isinstance(entry["command"], str): + try: + return shlex.split(entry["command"]) + except ValueError: + return [] + return [] + +def score(tokens): + text = " ".join(tokens) + s = 0 + if "vcpkg_installed" in text: + s += 8 + if any(tok.startswith("-std=") for tok in tokens): + s += 4 + if "-std" in tokens: + s += 4 + if any(tok == "-isystem" or tok.startswith("-isystem") for tok in tokens): + s += 2 + if any(tok == "-I" or tok.startswith("-I") for tok in tokens): + s += 1 + return s + +best = [] +best_score = -1 +for entry in db: + tokens = command_tokens(entry) + if not tokens: + continue + s = score(tokens) + if s > best_score: + best = tokens + best_score = s + +if not best: + sys.exit(0) + +keep = [] +i = 0 +while i < len(best): + tok = best[i] + if tok in ("-I", "-isystem", "-D", "-U", "-std"): + if i + 1 < len(best): + keep.append(tok) + keep.append(best[i + 1]) + i += 2 + continue + if tok.startswith(("-I", "-D", "-U", "-std=", "-isystem")): + keep.append(tok) + i += 1 + +seen = set() +for tok in keep: + if tok in seen: + continue + seen.add(tok) + print(tok) +PY +} + collect_changed_paths() { ( cd "${ROOT_DIR}" || exit 1 @@ -170,12 +245,21 @@ if [[ "$(uname -s)" == "Darwin" ]]; then EXTRA_ARGS+=(--extra-arg=-isysroot --extra-arg="${SDKROOT}") fi +HEADER_EXTRA_ARGS=("${EXTRA_ARGS[@]}") +mapfile -t HEADER_COMPILE_ARGS < <(extract_header_compile_args "${BUILD_DIR}/compile_commands.json") +for arg in "${HEADER_COMPILE_ARGS[@]}"; do + HEADER_EXTRA_ARGS+=(--extra-arg="${arg}") +done + lint_file() { local kind="$1" local file="$2" - local -a cmd=("${CLANG_TIDY_BIN}" -quiet "${EXTRA_ARGS[@]}") + local -a cmd=("${CLANG_TIDY_BIN}" -quiet) if [[ "${kind}" == "header" ]]; then + cmd+=("${HEADER_EXTRA_ARGS[@]}") cmd+=(-checks='misc-include-cleaner') + else + cmd+=("${EXTRA_ARGS[@]}") fi cmd+=(-p "${BUILD_DIR}" "${file}")